feat: default to Month view and implement household services filter toggle for parents

This commit is contained in:
t.indorf
2026-05-21 13:24:42 +02:00
parent 339563bf4d
commit f6acb3d271
3 changed files with 118 additions and 37 deletions
@@ -61,7 +61,8 @@ const categories = [
{ label: "Elternabend", type: TerminType.ELTERNABEND, color: "oklch(0.50 0.10 280)" }, { label: "Elternabend", type: TerminType.ELTERNABEND, color: "oklch(0.50 0.10 280)" },
{ 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: "Mein Elterndienst", type: "mein-elterndienst", color: "oklch(0.60 0.16 160)" },
{ label: "Elterndienst", type: "elterndienst", color: "oklch(0.75 0.08 160)" },
{ label: "Mein Notdienst", type: "mein-notdienst", color: "oklch(0.60 0.18 45)" }, { 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: "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)" },
@@ -76,9 +77,10 @@ export function CalendarOverviewClient({
userId: string; userId: string;
isAdmin: boolean; isAdmin: boolean;
}) { }) {
const [view, setView] = useState<"liste" | "monat" | "agenda">("liste"); const [view, setView] = useState<"liste" | "monat" | "agenda">("monat");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [activeTypes, setActiveTypes] = useState<Set<string>>(new Set()); const [activeTypes, setActiveTypes] = useState<Set<string>>(new Set());
const [filterScope, setFilterScope] = useState<"own" | "all">(isAdmin ? "all" : "own");
// Month navigation state // Month navigation state
const [currentMonthDate, setCurrentMonthDate] = useState(new Date()); const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
@@ -106,6 +108,12 @@ export function CalendarOverviewClient({
// Filter logic // Filter logic
const filteredEvents = useMemo(() => { const filteredEvents = useMemo(() => {
return termine.filter((item) => { return termine.filter((item) => {
// 0. Filter by scope (own vs all)
if (filterScope === "own") {
if (item.kind === "duty" && !item.isOwn) return false;
if (item.kind === "notdienst" && !item.isOwn) return false;
}
// 1. Search Query filter // 1. Search Query filter
const query = searchQuery.trim().toLowerCase(); const query = searchQuery.trim().toLowerCase();
if (query) { if (query) {
@@ -129,7 +137,9 @@ export function CalendarOverviewClient({
if (activeTypes.size > 0) { if (activeTypes.size > 0) {
const itemType = const itemType =
item.kind === "duty" item.kind === "duty"
? "elterndienst" ? item.isOwn
? "mein-elterndienst"
: "elterndienst"
: item.kind === "notdienst" : item.kind === "notdienst"
? item.isOwn ? item.isOwn
? "mein-notdienst" ? "mein-notdienst"
@@ -140,7 +150,7 @@ export function CalendarOverviewClient({
return true; return true;
}); });
}, [termine, searchQuery, activeTypes]); }, [termine, searchQuery, activeTypes, filterScope]);
// Check if an event falls on a specific day // Check if an event falls on a specific day
const isEventOnDay = (item: CalendarListItemDto, day: Date) => { const isEventOnDay = (item: CalendarListItemDto, day: Date) => {
@@ -177,6 +187,7 @@ export function CalendarOverviewClient({
<div className="space-y-5"> <div className="space-y-5">
{/* Search and Filters Header */} {/* Search and Filters Header */}
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center flex-wrap gap-3">
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]"> <div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
{(["liste", "monat", "agenda"] as const).map((v) => ( {(["liste", "monat", "agenda"] as const).map((v) => (
<button <button
@@ -196,6 +207,36 @@ export function CalendarOverviewClient({
))} ))}
</div> </div>
{!isAdmin && (
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)] p-0.5">
<button
type="button"
onClick={() => setFilterScope("own")}
className={[
"h-8 px-4 rounded-full text-[12px] font-semibold transition-all duration-200",
filterScope === "own"
? "bg-[var(--accent-deep)] text-[var(--surface)] shadow-sm"
: "text-[var(--ink-soft)] hover:text-[var(--ink)] hover:bg-[var(--surface-2)]",
].join(" ")}
>
Meine Dienste
</button>
<button
type="button"
onClick={() => setFilterScope("all")}
className={[
"h-8 px-4 rounded-full text-[12px] font-semibold transition-all duration-200",
filterScope === "all"
? "bg-[var(--accent-deep)] text-[var(--surface)] shadow-sm"
: "text-[var(--ink-soft)] hover:text-[var(--ink)] hover:bg-[var(--surface-2)]",
].join(" ")}
>
Alle Dienste
</button>
</div>
)}
</div>
<div className="flex flex-1 flex-wrap items-center justify-end gap-2"> <div className="flex flex-1 flex-wrap items-center justify-end gap-2">
{/* Category chips */} {/* Category chips */}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -291,16 +332,40 @@ export function CalendarOverviewClient({
> >
{item.kind === "duty" ? ( {item.kind === "duty" ? (
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--good-soft)] text-[var(--good)]"> <div
<WashingMachine className="h-5 w-5 [stroke-width:1.7]" /> className="absolute inset-y-3 left-0 w-[4px] rounded-r-full"
style={{
backgroundColor: item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)",
}}
/>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--surface-2)] text-[var(--ink-soft)] ml-2">
<WashingMachine className="h-5 w-5 [stroke-width:1.7]" style={{ color: item.isOwn ? "oklch(0.60 0.16 160)" : "oklch(0.75 0.08 160)" }} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h4 className="font-semibold text-[15px] text-[var(--ink)] truncate"> <h4 className="font-semibold text-[15px] text-[var(--ink)] truncate">
{item.title} {item.title}
</h4> </h4>
<Badge variant="secondary" className="bg-[oklch(0.95_0.02_160)] text-[oklch(0.40_0.10_160)]"> <Badge
Elterndienst variant="secondary"
style={{
color: item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)",
borderColor:
(item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)") + "44",
backgroundColor:
(item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)") + "11",
}}
className="border text-[10px]"
>
{item.isOwn ? "Mein Elterndienst" : "Elterndienst"}
</Badge> </Badge>
</div> </div>
<p className="mt-1 text-sm text-[var(--ink-soft)]"> <p className="mt-1 text-sm text-[var(--ink-soft)]">
@@ -604,7 +669,9 @@ function MonthView({
{dayEvents.slice(0, 3).map((item) => { {dayEvents.slice(0, 3).map((item) => {
const color = const color =
item.kind === "duty" item.kind === "duty"
? "oklch(0.60 0.15 160)" ? item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)"
: item.kind === "notdienst" : item.kind === "notdienst"
? item.isOwn ? item.isOwn
? "oklch(0.60 0.18 45)" ? "oklch(0.60 0.18 45)"
@@ -709,6 +776,10 @@ function AgendaView({
? item.isOwn ? item.isOwn
? "oklch(0.60 0.18 45)" ? "oklch(0.60 0.18 45)"
: "oklch(0.75 0.10 50)" : "oklch(0.75 0.10 50)"
: item.kind === "duty"
? item.isOwn
? "oklch(0.60 0.16 160)"
: "oklch(0.75 0.08 160)"
: "oklch(0.60 0.15 160)"; : "oklch(0.60 0.15 160)";
return ( return (
@@ -63,6 +63,7 @@ export type DutyCalendarItemDto = {
status: DutyAssignmentStatus; status: DutyAssignmentStatus;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
isOwn: boolean;
}; };
export type NotdienstCalendarItemDto = { export type NotdienstCalendarItemDto = {
@@ -150,7 +151,7 @@ export function TerminList({
} }
function DutyRow({ duty, isAdmin }: { duty: DutyCalendarItemDto; isAdmin: boolean }) { function DutyRow({ duty, isAdmin }: { duty: DutyCalendarItemDto; isAdmin: boolean }) {
const color = "var(--good)"; const color = duty.isOwn ? "oklch(0.60 0.16 160)" : "oklch(0.75 0.08 160)";
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
return ( return (
@@ -170,7 +171,9 @@ function DutyRow({ duty, isAdmin }: { duty: DutyCalendarItemDto; isAdmin: boolea
</p> </p>
</div> </div>
<div className="flex items-center gap-2 relative"> <div className="flex items-center gap-2 relative">
<CategoryPill color={color}>Elterndienst</CategoryPill> <CategoryPill color={color}>
{duty.isOwn ? "Mein Elterndienst" : "Elterndienst"}
</CategoryPill>
{isAdmin ? ( {isAdmin ? (
<div className="relative"> <div className="relative">
<Button <Button
+10 -3
View File
@@ -134,6 +134,7 @@ export default async function KalenderPage({
startDate: true, startDate: true,
endDate: true, endDate: true,
status: true, status: true,
familyId: true,
dutyType: { dutyType: {
select: { select: {
name: true, name: true,
@@ -195,15 +196,21 @@ export default async function KalenderPage({
participation.status === EventParticipationStatus.ATTENDING, participation.status === EventParticipationStatus.ATTENDING,
), ),
})), })),
...dutyAssignmentRows.map((assignment) => ({ ...dutyAssignmentRows.map((assignment) => {
const isOwn = assignment.familyId === familyIdForParticipation;
return {
kind: "duty" as const, kind: "duty" as const,
id: assignment.id, id: assignment.id,
title: assignment.dutyType.name, title: isOwn
? `Mein Elterndienst (${assignment.dutyType.name})`
: `${assignment.dutyType.name} (${assignment.family.name})`,
familyName: assignment.family.name, familyName: assignment.family.name,
status: assignment.status, status: assignment.status,
startDate: assignment.startDate, startDate: assignment.startDate,
endDate: assignment.endDate, endDate: assignment.endDate,
})), isOwn,
};
}),
...notdienstAssignmentRows.map((assignment) => { ...notdienstAssignmentRows.map((assignment) => {
const isOwn = assignment.child.familyId === familyIdForParticipation; const isOwn = assignment.child.familyId === familyIdForParticipation;
return { return {