feat: add parent announcements and family or child event responses

This commit is contained in:
t.indorf
2026-10-09 21:44:19 +02:00
parent 2d3b82eb80
commit 979f795341
29 changed files with 2031 additions and 472 deletions
+48 -17
View File
@@ -1,5 +1,7 @@
"use client";
import Link from "next/link";
import { toast } from "sonner";
import { useState, useTransition } from "react";
import { Bell, FileText, Megaphone } from "lucide-react";
@@ -26,6 +28,7 @@ export type NewsTickerItemDto = {
id: string;
title: string;
content: string;
excerpt: string;
createdAt: string;
authorName: string;
isUnread: boolean;
@@ -37,22 +40,32 @@ export type NewsTickerItemDto = {
}[];
};
export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
export function NewsTicker({
items,
archive = false,
}: {
items: NewsTickerItemDto[];
archive?: boolean;
}) {
const [selected, setSelected] = useState<NewsTickerItemDto | null>(null);
const [readIds, setReadIds] = useState<Set<string>>(new Set());
const [, startTransition] = useTransition();
if (items.length === 0) {
return null;
}
function openAnnouncement(item: NewsTickerItemDto) {
setSelected(item);
if (!item.isUnread || readIds.has(item.id)) return;
setReadIds((current) => new Set(current).add(item.id));
startTransition(async () => {
await markAnnouncementRead(item.id);
try {
const result = await markAnnouncementRead(item.id);
if (result.error) {
toast.error(result.error);
return;
}
setReadIds((current) => new Set(current).add(item.id));
} catch {
toast.error("Lesestatus konnte nicht gespeichert werden.");
}
});
}
@@ -60,20 +73,33 @@ export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
<>
<Card className="mb-8 border-[var(--good)] bg-[var(--good-soft)]">
<CardHeader className="pb-3">
<div className="flex items-center justify-between gap-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Megaphone className="h-5 w-5 text-[var(--good)] [stroke-width:1.7]" />
Schwarzes Brett
{archive ? "Mitteilungen" : "Neues aus der Kita"}
</CardTitle>
<CardDescription>
Die neuesten offiziellen Ankündigungen deiner Kita.
{archive
? "Veröffentlichte Ankündigungen deiner Kita."
: "Die neuesten offiziellen Ankündigungen deiner Kita."}
</CardDescription>
</div>
<Badge variant="secondary">{items.length}</Badge>
{archive ? (
<Badge variant="secondary">{items.length} Meldungen</Badge>
) : (
<Button asChild variant="outline" size="sm">
<Link href="/dashboard/mitteilungen">Alle Mitteilungen</Link>
</Button>
)}
</div>
</CardHeader>
<CardContent className="grid gap-3 lg:grid-cols-3">
{items.length === 0 ? (
<p className="col-span-full py-4 text-sm text-[var(--ink-soft)]">
Noch keine Mitteilungen für deine Empfängergruppe.
</p>
) : null}
{items.map((item) => {
const isUnread = item.isUnread && !readIds.has(item.id);
return (
@@ -88,15 +114,13 @@ export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
<Bell className="h-3.5 w-3.5 [stroke-width:1.7]" />
{item.createdAt}
</div>
{isUnread && (
<Badge variant="success">NEU</Badge>
)}
{isUnread && <Badge variant="success">NEU</Badge>}
</div>
<h3 className="line-clamp-2 font-semibold leading-snug text-[var(--ink)]">
{item.title}
</h3>
<p className="mt-2 line-clamp-3 text-sm leading-6 text-[var(--ink-soft)]">
{item.content}
{item.excerpt}
</p>
{item.attachments.length > 0 && (
<p className="mt-3 flex items-center gap-1 text-xs font-medium text-[var(--accent-deep)]">
@@ -111,7 +135,10 @@ export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
</CardContent>
</Card>
<Dialog open={!!selected} onOpenChange={(open) => !open && setSelected(null)}>
<Dialog
open={!!selected}
onOpenChange={(open) => !open && setSelected(null)}
>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
{selected && (
<>
@@ -137,7 +164,11 @@ export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
variant="outline"
className="justify-start"
>
<a href={attachment.fileUrl} target="_blank" rel="noreferrer">
<a
href={attachment.fileUrl}
target="_blank"
rel="noreferrer"
>
<FileText className="h-4 w-4 [stroke-width:1.7]" />
{attachment.fileName}
</a>