feat: display published Notdienst assignments in appointment calendar with custom styling for own assignments
This commit is contained in:
@@ -62,6 +62,8 @@ const categories = [
|
|||||||
{ label: "Fest", type: TerminType.KITA_FEST, color: "var(--good)" },
|
{ label: "Fest", type: TerminType.KITA_FEST, color: "var(--good)" },
|
||||||
{ label: "Geburtstag", type: TerminType.GEBURTSTAG_INTERN, color: "oklch(0.50 0.10 320)" },
|
{ label: "Geburtstag", type: TerminType.GEBURTSTAG_INTERN, color: "oklch(0.50 0.10 320)" },
|
||||||
{ label: "Elterndienst", type: "elterndienst", color: "oklch(0.60 0.15 160)" },
|
{ label: "Elterndienst", type: "elterndienst", color: "oklch(0.60 0.15 160)" },
|
||||||
|
{ label: "Mein Notdienst", type: "mein-notdienst", color: "oklch(0.60 0.18 45)" },
|
||||||
|
{ label: "Notdienst", type: "notdienst", color: "oklch(0.75 0.10 50)" },
|
||||||
{ label: "Sonstiges", type: TerminType.SONSTIGES, color: "var(--ink-muted)" },
|
{ label: "Sonstiges", type: TerminType.SONSTIGES, color: "var(--ink-muted)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -113,15 +115,26 @@ export function CalendarOverviewClient({
|
|||||||
? item.description.toLowerCase().includes(query)
|
? item.description.toLowerCase().includes(query)
|
||||||
: false;
|
: false;
|
||||||
const familyMatch =
|
const familyMatch =
|
||||||
item.kind === "duty" && item.familyName
|
(item.kind === "duty" || item.kind === "notdienst") && item.familyName
|
||||||
? item.familyName.toLowerCase().includes(query)
|
? item.familyName.toLowerCase().includes(query)
|
||||||
: false;
|
: false;
|
||||||
if (!titleMatch && !descMatch && !familyMatch) return false;
|
const childMatch =
|
||||||
|
item.kind === "notdienst" && item.childName
|
||||||
|
? item.childName.toLowerCase().includes(query)
|
||||||
|
: false;
|
||||||
|
if (!titleMatch && !descMatch && !familyMatch && !childMatch) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Category filter
|
// 2. Category filter
|
||||||
if (activeTypes.size > 0) {
|
if (activeTypes.size > 0) {
|
||||||
const itemType = item.kind === "duty" ? "elterndienst" : item.type;
|
const itemType =
|
||||||
|
item.kind === "duty"
|
||||||
|
? "elterndienst"
|
||||||
|
: item.kind === "notdienst"
|
||||||
|
? item.isOwn
|
||||||
|
? "mein-notdienst"
|
||||||
|
: "notdienst"
|
||||||
|
: item.type;
|
||||||
if (!activeTypes.has(itemType)) return false;
|
if (!activeTypes.has(itemType)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +322,59 @@ export function CalendarOverviewClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : item.kind === "notdienst" ? (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-3 left-0 w-[4px] rounded-r-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="pl-2 flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h4 className="font-semibold text-[15px] text-[var(--ink)] truncate">
|
||||||
|
{item.title}
|
||||||
|
</h4>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
style={{
|
||||||
|
color: item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)",
|
||||||
|
borderColor:
|
||||||
|
(item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)") + "44",
|
||||||
|
backgroundColor:
|
||||||
|
(item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)") + "11",
|
||||||
|
}}
|
||||||
|
className="border text-[10px]"
|
||||||
|
>
|
||||||
|
{item.isOwn ? "Mein Notdienst" : "Notdienst"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-[var(--ink-soft)]">
|
||||||
|
Kind: {item.childName} (Familie: {item.familyName})
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-[var(--ink-muted)] flex items-center gap-1">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
{format(new Date(item.startDate), "dd.MM.yyyy")}
|
||||||
|
</p>
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="mt-3 flex gap-2">
|
||||||
|
<a href="/dashboard/notdienst/plan">
|
||||||
|
<Button size="sm" variant="outline" className="h-7 text-xs px-2.5">
|
||||||
|
Im Planer bearbeiten
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -539,6 +605,10 @@ function MonthView({
|
|||||||
const color =
|
const color =
|
||||||
item.kind === "duty"
|
item.kind === "duty"
|
||||||
? "oklch(0.60 0.15 160)"
|
? "oklch(0.60 0.15 160)"
|
||||||
|
: item.kind === "notdienst"
|
||||||
|
? item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)"
|
||||||
: getCategoryColor(item.type);
|
: getCategoryColor(item.type);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -632,8 +702,14 @@ function AgendaView({
|
|||||||
{/* Items stack on right */}
|
{/* Items stack on right */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{group.items.map((item) => {
|
{group.items.map((item) => {
|
||||||
const isTermin = item.kind === "termin";
|
const color =
|
||||||
const color = isTermin ? getCategoryColor(item.type) : "oklch(0.60 0.15 160)";
|
item.kind === "termin"
|
||||||
|
? getCategoryColor(item.type)
|
||||||
|
: item.kind === "notdienst"
|
||||||
|
? item.isOwn
|
||||||
|
? "oklch(0.60 0.18 45)"
|
||||||
|
: "oklch(0.75 0.10 50)"
|
||||||
|
: "oklch(0.60 0.15 160)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -650,7 +726,7 @@ function AgendaView({
|
|||||||
<h4 className="font-bold text-[15px] text-[var(--ink)] truncate">
|
<h4 className="font-bold text-[15px] text-[var(--ink)] truncate">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h4>
|
</h4>
|
||||||
{isTermin ? (
|
{item.kind === "termin" ? (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-bold border"
|
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-bold border"
|
||||||
style={{
|
style={{
|
||||||
@@ -661,34 +737,52 @@ function AgendaView({
|
|||||||
>
|
>
|
||||||
{getTypeLabel(item.type)}
|
{getTypeLabel(item.type)}
|
||||||
</span>
|
</span>
|
||||||
|
) : item.kind === "notdienst" ? (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px]"
|
||||||
|
style={{
|
||||||
|
color,
|
||||||
|
borderColor: color + "44",
|
||||||
|
backgroundColor: color + "11",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.isOwn ? "Mein Notdienst" : "Notdienst"}
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="secondary" className="bg-[oklch(0.95_0.02_160)] text-[oklch(0.40_0.10_160)] text-[10px]">
|
<Badge variant="secondary" className="bg-[oklch(0.95_0.02_160)] text-[oklch(0.40_0.10_160)] text-[10px]">
|
||||||
Elterndienst
|
Elterndienst
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{isTermin && item.status === TerminStatus.PENDING && (
|
{item.kind === "termin" && item.status === TerminStatus.PENDING && (
|
||||||
<Badge variant="warning" className="text-[10px]">Ausstehend</Badge>
|
<Badge variant="warning" className="text-[10px]">Ausstehend</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-1 text-xs text-[var(--ink-muted)] flex items-center gap-1.5">
|
<p className="mt-1 text-xs text-[var(--ink-muted)] flex items-center gap-1.5">
|
||||||
<Clock className="h-3.5 w-3.5" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
{isTermin
|
{item.kind === "termin"
|
||||||
? item.allDay
|
? item.allDay
|
||||||
? "Ganztägig"
|
? "Ganztägig"
|
||||||
: `${format(new Date(item.startDate), "HH:mm")} - ${format(
|
: `${format(new Date(item.startDate), "HH:mm")} - ${format(
|
||||||
new Date(item.endDate),
|
new Date(item.endDate),
|
||||||
"HH:mm",
|
"HH:mm",
|
||||||
)}`
|
)}`
|
||||||
|
: item.kind === "notdienst"
|
||||||
|
? `${format(new Date(item.startDate), "dd.MM.yyyy")} (ganztägig)`
|
||||||
: `${format(new Date(item.startDate), "dd.MM.")} - ${format(
|
: `${format(new Date(item.startDate), "dd.MM.")} - ${format(
|
||||||
new Date(item.endDate),
|
new Date(item.endDate),
|
||||||
"dd.MM.yyyy",
|
"dd.MM.yyyy",
|
||||||
)}`}
|
)}`}
|
||||||
{isTermin && <MapPin className="ml-1 h-3.5 w-3.5" />}
|
{item.kind === "termin" && <MapPin className="ml-1 h-3.5 w-3.5" />}
|
||||||
{isTermin ? "Kita" : `Familie: ${item.familyName}`}
|
{item.kind === "termin"
|
||||||
|
? "Kita"
|
||||||
|
: item.kind === "notdienst"
|
||||||
|
? `Kind: ${item.childName} · Familie: ${item.familyName}`
|
||||||
|
: `Familie: ${item.familyName}`}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isTermin && item.description && (
|
{item.kind === "termin" && item.description && (
|
||||||
<p className="mt-2 text-sm text-[var(--ink-soft)] line-clamp-2 leading-relaxed">
|
<p className="mt-2 text-sm text-[var(--ink-soft)] line-clamp-2 leading-relaxed">
|
||||||
{item.description}
|
{item.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -697,7 +791,7 @@ function AgendaView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions for admins */}
|
{/* Actions for admins */}
|
||||||
{isAdmin && isTermin && (
|
{isAdmin && item.kind === "termin" && (
|
||||||
<div className="flex gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="flex gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<AdminTerminModal
|
<AdminTerminModal
|
||||||
existingTermin={item}
|
existingTermin={item}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
WashingMachine,
|
WashingMachine,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
ShieldAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -64,7 +65,18 @@ export type DutyCalendarItemDto = {
|
|||||||
endDate: Date;
|
endDate: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CalendarListItemDto = TerminListItemDto | DutyCalendarItemDto;
|
export type NotdienstCalendarItemDto = {
|
||||||
|
kind: "notdienst";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
familyName: string;
|
||||||
|
childName: string;
|
||||||
|
isOwn: boolean;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarListItemDto = TerminListItemDto | DutyCalendarItemDto | NotdienstCalendarItemDto;
|
||||||
|
|
||||||
export function TerminList({
|
export function TerminList({
|
||||||
termine,
|
termine,
|
||||||
@@ -123,6 +135,8 @@ export function TerminList({
|
|||||||
{group.items.map((item) =>
|
{group.items.map((item) =>
|
||||||
item.kind === "duty" ? (
|
item.kind === "duty" ? (
|
||||||
<DutyRow key={`duty-${item.id}`} duty={item} isAdmin={isAdmin} />
|
<DutyRow key={`duty-${item.id}`} duty={item} isAdmin={isAdmin} />
|
||||||
|
) : item.kind === "notdienst" ? (
|
||||||
|
<NotdienstRow key={`notdienst-${item.id}`} notdienst={item} isAdmin={isAdmin} />
|
||||||
) : (
|
) : (
|
||||||
<TerminRow key={`termin-${item.id}`} termin={item} userId={userId} isAdmin={isAdmin} />
|
<TerminRow key={`termin-${item.id}`} termin={item} userId={userId} isAdmin={isAdmin} />
|
||||||
),
|
),
|
||||||
@@ -474,3 +488,64 @@ function getTypeLabel(type: TerminType): string {
|
|||||||
return "Sonstiges";
|
return "Sonstiges";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NotdienstRow({ notdienst, isAdmin }: { notdienst: NotdienstCalendarItemDto; isAdmin: boolean }) {
|
||||||
|
const color = notdienst.isOwn ? "oklch(0.60 0.18 45)" : "oklch(0.75 0.10 50)";
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative grid grid-cols-[56px_minmax(0,1fr)_auto] items-center gap-4 px-5 py-4 transition-colors hover:bg-[var(--surface-2)]">
|
||||||
|
<div className="absolute inset-y-3 left-0 w-[3px] rounded-r-full" style={{ backgroundColor: color }} />
|
||||||
|
<DateTile date={notdienst.startDate} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="truncate text-[15px] font-semibold text-[var(--ink)]">
|
||||||
|
{notdienst.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 flex flex-wrap items-center gap-2 text-[13px] text-[var(--ink-soft)]">
|
||||||
|
<Clock className="h-3.5 w-3.5 [stroke-width:1.7]" />
|
||||||
|
{format(notdienst.startDate, "dd.MM.yyyy", { locale: de })} (ganztägig)
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[var(--ink-muted)]" />
|
||||||
|
Kind: {notdienst.childName}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 relative">
|
||||||
|
<CategoryPill color={color}>
|
||||||
|
{notdienst.isOwn ? "Mein Notdienst" : "Notdienst"}
|
||||||
|
</CategoryPill>
|
||||||
|
{isAdmin ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Mehr Optionen"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setMenuOpen(!menuOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4 [stroke-width:1.7]" />
|
||||||
|
</Button>
|
||||||
|
{menuOpen && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-40" onClick={() => setMenuOpen(false)} />
|
||||||
|
<div className="absolute right-0 mt-1 w-56 rounded-xl border border-[var(--hairline)] bg-[var(--surface)] p-1.5 shadow-lg z-50 animate-in fade-in-50 slide-in-from-top-1 duration-150">
|
||||||
|
<a
|
||||||
|
href="/dashboard/notdienst/plan"
|
||||||
|
className="flex w-full items-center rounded-lg px-2.5 py-2 text-sm text-[var(--ink)] hover:bg-[var(--surface-2)]"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
>
|
||||||
|
Notdienst im Planer bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-9 h-9" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,6 +148,32 @@ export default async function KalenderPage({
|
|||||||
orderBy: { startDate: "asc" },
|
orderBy: { startDate: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notdienstAssignmentRows = await prisma.notdienstAssignment.findMany({
|
||||||
|
where: {
|
||||||
|
kitaId: session.user.kitaId,
|
||||||
|
plan: {
|
||||||
|
status: "PUBLISHED",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
date: true,
|
||||||
|
child: {
|
||||||
|
select: {
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
familyId: true,
|
||||||
|
family: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { date: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
const allUserTermine: CalendarListItemDto[] = [
|
const allUserTermine: CalendarListItemDto[] = [
|
||||||
...confirmedTermineRows.map((termin) => ({
|
...confirmedTermineRows.map((termin) => ({
|
||||||
...termin,
|
...termin,
|
||||||
@@ -178,6 +204,21 @@ export default async function KalenderPage({
|
|||||||
startDate: assignment.startDate,
|
startDate: assignment.startDate,
|
||||||
endDate: assignment.endDate,
|
endDate: assignment.endDate,
|
||||||
})),
|
})),
|
||||||
|
...notdienstAssignmentRows.map((assignment) => {
|
||||||
|
const isOwn = assignment.child.familyId === familyIdForParticipation;
|
||||||
|
return {
|
||||||
|
kind: "notdienst" as const,
|
||||||
|
id: assignment.id,
|
||||||
|
title: isOwn
|
||||||
|
? `Mein Notdienst (${assignment.child.firstName})`
|
||||||
|
: `Notdienst (${assignment.child.family.name})`,
|
||||||
|
familyName: assignment.child.family.name,
|
||||||
|
childName: `${assignment.child.firstName} ${assignment.child.lastName}`,
|
||||||
|
isOwn,
|
||||||
|
startDate: assignment.date,
|
||||||
|
endDate: assignment.date,
|
||||||
|
};
|
||||||
|
}),
|
||||||
].sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
|
].sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
|
||||||
|
|
||||||
let allPendingTermine: PendingTerminDto[] = [];
|
let allPendingTermine: PendingTerminDto[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user