feat: default to Month view and implement household services filter toggle for parents
This commit is contained in:
@@ -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,23 +187,54 @@ 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="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
|
<div className="flex items-center flex-wrap gap-3">
|
||||||
{(["liste", "monat", "agenda"] as const).map((v) => (
|
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
|
||||||
<button
|
{(["liste", "monat", "agenda"] as const).map((v) => (
|
||||||
key={v}
|
<button
|
||||||
type="button"
|
key={v}
|
||||||
onClick={() => setView(v)}
|
type="button"
|
||||||
className={[
|
onClick={() => setView(v)}
|
||||||
"h-9 px-4 text-[13px] font-semibold transition",
|
className={[
|
||||||
v !== "liste" ? "border-l border-[var(--hairline-soft)]" : "",
|
"h-9 px-4 text-[13px] font-semibold transition",
|
||||||
view === v
|
v !== "liste" ? "border-l border-[var(--hairline-soft)]" : "",
|
||||||
? "bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
view === v
|
||||||
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
|
? "bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
||||||
].join(" ")}
|
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)] hover:text-[var(--ink)]",
|
||||||
>
|
].join(" ")}
|
||||||
{v.charAt(0).toUpperCase() + v.slice(1)}
|
>
|
||||||
</button>
|
{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>
|
||||||
|
|
||||||
<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">
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
kind: "duty" as const,
|
const isOwn = assignment.familyId === familyIdForParticipation;
|
||||||
id: assignment.id,
|
return {
|
||||||
title: assignment.dutyType.name,
|
kind: "duty" as const,
|
||||||
familyName: assignment.family.name,
|
id: assignment.id,
|
||||||
status: assignment.status,
|
title: isOwn
|
||||||
startDate: assignment.startDate,
|
? `Mein Elterndienst (${assignment.dutyType.name})`
|
||||||
endDate: assignment.endDate,
|
: `${assignment.dutyType.name} (${assignment.family.name})`,
|
||||||
})),
|
familyName: assignment.family.name,
|
||||||
|
status: assignment.status,
|
||||||
|
startDate: assignment.startDate,
|
||||||
|
endDate: assignment.endDate,
|
||||||
|
isOwn,
|
||||||
|
};
|
||||||
|
}),
|
||||||
...notdienstAssignmentRows.map((assignment) => {
|
...notdienstAssignmentRows.map((assignment) => {
|
||||||
const isOwn = assignment.child.familyId === familyIdForParticipation;
|
const isOwn = assignment.child.familyId === familyIdForParticipation;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user