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: "Fest", type: TerminType.KITA_FEST, color: "var(--good)" },
{ 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: "Notdienst", type: "notdienst", color: "oklch(0.75 0.10 50)" },
{ label: "Sonstiges", type: TerminType.SONSTIGES, color: "var(--ink-muted)" },
@@ -76,9 +77,10 @@ export function CalendarOverviewClient({
userId: string;
isAdmin: boolean;
}) {
const [view, setView] = useState<"liste" | "monat" | "agenda">("liste");
const [view, setView] = useState<"liste" | "monat" | "agenda">("monat");
const [searchQuery, setSearchQuery] = useState("");
const [activeTypes, setActiveTypes] = useState<Set<string>>(new Set());
const [filterScope, setFilterScope] = useState<"own" | "all">(isAdmin ? "all" : "own");
// Month navigation state
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
@@ -106,6 +108,12 @@ export function CalendarOverviewClient({
// Filter logic
const filteredEvents = useMemo(() => {
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
const query = searchQuery.trim().toLowerCase();
if (query) {
@@ -129,7 +137,9 @@ export function CalendarOverviewClient({
if (activeTypes.size > 0) {
const itemType =
item.kind === "duty"
? "elterndienst"
? item.isOwn
? "mein-elterndienst"
: "elterndienst"
: item.kind === "notdienst"
? item.isOwn
? "mein-notdienst"
@@ -140,7 +150,7 @@ export function CalendarOverviewClient({
return true;
});
}, [termine, searchQuery, activeTypes]);
}, [termine, searchQuery, activeTypes, filterScope]);
// Check if an event falls on a specific day
const isEventOnDay = (item: CalendarListItemDto, day: Date) => {
@@ -177,23 +187,54 @@ export function CalendarOverviewClient({
<div className="space-y-5">
{/* Search and Filters Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
{(["liste", "monat", "agenda"] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => setView(v)}
className={[
"h-9 px-4 text-[13px] font-semibold transition",
v !== "liste" ? "border-l border-[var(--hairline-soft)]" : "",
view === v
? "bg-[var(--accent-soft)] text-[var(--accent-deep)]"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
].join(" ")}
>
{v.charAt(0).toUpperCase() + v.slice(1)}
</button>
))}
<div className="flex items-center flex-wrap gap-3">
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
{(["liste", "monat", "agenda"] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => setView(v)}
className={[
"h-9 px-4 text-[13px] font-semibold transition",
v !== "liste" ? "border-l border-[var(--hairline-soft)]" : "",
view === v
? "bg-[var(--accent-soft)] text-[var(--accent-deep)]"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
].join(" ")}
>
{v.charAt(0).toUpperCase() + v.slice(1)}
</button>
))}
</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">
@@ -291,16 +332,40 @@ export function CalendarOverviewClient({
>
{item.kind === "duty" ? (
<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)]">
<WashingMachine className="h-5 w-5 [stroke-width:1.7]" />
<div
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 className="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" className="bg-[oklch(0.95_0.02_160)] text-[oklch(0.40_0.10_160)]">
Elterndienst
<Badge
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>
</div>
<p className="mt-1 text-sm text-[var(--ink-soft)]">
@@ -604,7 +669,9 @@ function MonthView({
{dayEvents.slice(0, 3).map((item) => {
const color =
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.isOwn
? "oklch(0.60 0.18 45)"
@@ -709,6 +776,10 @@ function AgendaView({
? item.isOwn
? "oklch(0.60 0.18 45)"
: "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)";
return (
@@ -63,6 +63,7 @@ export type DutyCalendarItemDto = {
status: DutyAssignmentStatus;
startDate: Date;
endDate: Date;
isOwn: boolean;
};
export type NotdienstCalendarItemDto = {
@@ -150,7 +151,7 @@ export function TerminList({
}
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);
return (
@@ -170,7 +171,9 @@ function DutyRow({ duty, isAdmin }: { duty: DutyCalendarItemDto; isAdmin: boolea
</p>
</div>
<div className="flex items-center gap-2 relative">
<CategoryPill color={color}>Elterndienst</CategoryPill>
<CategoryPill color={color}>
{duty.isOwn ? "Mein Elterndienst" : "Elterndienst"}
</CategoryPill>
{isAdmin ? (
<div className="relative">
<Button
+16 -9
View File
@@ -134,6 +134,7 @@ export default async function KalenderPage({
startDate: true,
endDate: true,
status: true,
familyId: true,
dutyType: {
select: {
name: true,
@@ -195,15 +196,21 @@ export default async function KalenderPage({
participation.status === EventParticipationStatus.ATTENDING,
),
})),
...dutyAssignmentRows.map((assignment) => ({
kind: "duty" as const,
id: assignment.id,
title: assignment.dutyType.name,
familyName: assignment.family.name,
status: assignment.status,
startDate: assignment.startDate,
endDate: assignment.endDate,
})),
...dutyAssignmentRows.map((assignment) => {
const isOwn = assignment.familyId === familyIdForParticipation;
return {
kind: "duty" as const,
id: assignment.id,
title: isOwn
? `Mein Elterndienst (${assignment.dutyType.name})`
: `${assignment.dutyType.name} (${assignment.family.name})`,
familyName: assignment.family.name,
status: assignment.status,
startDate: assignment.startDate,
endDate: assignment.endDate,
isOwn,
};
}),
...notdienstAssignmentRows.map((assignment) => {
const isOwn = assignment.child.familyId === familyIdForParticipation;
return {