continued the kita-planer

This commit is contained in:
t.indorf
2026-05-08 14:32:14 +02:00
parent b686e714ff
commit 7aff691803
85 changed files with 9434 additions and 588 deletions
@@ -1,7 +1,6 @@
"use client";
import { useState, useTransition } from "react";
import { MitbringselItem } from "@prisma/client";
import { Trash2, Plus, Utensils } from "lucide-react";
import { toast } from "sonner";
@@ -9,7 +8,10 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { addMitbringsel, deleteMitbringsel } from "../actions";
type ItemWithUser = MitbringselItem & {
type MitbringselItemDto = {
id: string;
userId: string;
content: string;
user: { firstName: string; lastName: string };
};
@@ -20,7 +22,7 @@ export function MitbringselList({
isAdmin,
}: {
terminId: string;
items: ItemWithUser[];
items: MitbringselItemDto[];
currentUserId: string;
isAdmin: boolean;
}) {
@@ -5,17 +5,20 @@ import { format } from "date-fns";
import { de } from "date-fns/locale";
import { Check, X, Clock, CalendarIcon } from "lucide-react";
import { toast } from "sonner";
import { Termin, User } from "@prisma/client";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { approveTermin, rejectTermin } from "../actions";
type PendingTermin = Termin & {
export type PendingTerminDto = {
id: string;
title: string;
startDate: Date;
allDay: boolean;
createdBy: { firstName: string; lastName: string } | null;
};
export function PendingAnfragen({ termine }: { termine: PendingTermin[] }) {
export function PendingAnfragen({ termine }: { termine: PendingTerminDto[] }) {
if (termine.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center animate-in fade-in-50">
@@ -37,7 +40,7 @@ export function PendingAnfragen({ termine }: { termine: PendingTermin[] }) {
);
}
function PendingTerminCard({ termin }: { termin: PendingTermin }) {
function PendingTerminCard({ termin }: { termin: PendingTerminDto }) {
const [isPending, startTransition] = useTransition();
const handleApprove = () => {
@@ -1,11 +1,11 @@
"use client";
import { Termin, MitbringselItem, TerminType, TerminStatus } from "@prisma/client";
import { DutyAssignmentStatus, TerminStatus, TerminType } from "@prisma/client";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { Calendar, Clock, MapPin, AlignLeft } from "lucide-react";
import { AlignLeft, Calendar, Clock, WashingMachine } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { MitbringselList } from "./mitbringsel-list";
import { toggleMitbringselList } from "../actions";
@@ -14,18 +14,43 @@ import { Label } from "@/components/ui/label";
import { useTransition } from "react";
import { toast } from "sonner";
type TerminWithItems = Termin & {
mitbringselItems?: (MitbringselItem & {
export type TerminListItemDto = {
kind: "termin";
id: string;
title: string;
description: string | null;
type: TerminType;
status: TerminStatus;
startDate: Date;
endDate: Date;
allDay: boolean;
mitbringselListEnabled: boolean;
mitbringselItems?: {
id: string;
userId: string;
content: string;
user: { firstName: string; lastName: string };
})[];
}[];
};
export type DutyCalendarItemDto = {
kind: "duty";
id: string;
title: string;
familyName: string;
status: DutyAssignmentStatus;
startDate: Date;
endDate: Date;
};
export type CalendarListItemDto = TerminListItemDto | DutyCalendarItemDto;
export function TerminList({
termine,
userId,
isAdmin,
}: {
termine: TerminWithItems[];
termine: CalendarListItemDto[];
userId: string;
isAdmin: boolean;
}) {
@@ -46,23 +71,58 @@ export function TerminList({
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{termine.map((termin) => (
<TerminCard
key={termin.id}
termin={termin}
userId={userId}
isAdmin={isAdmin}
/>
termin.kind === "duty" ? (
<DutyCard key={`duty-${termin.id}`} duty={termin} />
) : (
<TerminCard
key={`termin-${termin.id}`}
termin={termin}
userId={userId}
isAdmin={isAdmin}
/>
)
))}
</div>
);
}
function DutyCard({ duty }: { duty: DutyCalendarItemDto }) {
return (
<Card className="flex flex-col overflow-hidden border-emerald-200 bg-emerald-50/60">
<CardHeader className="pb-3">
<div className="mb-2 flex items-start justify-between gap-2">
<Badge className="border-transparent bg-emerald-600 text-white hover:bg-emerald-700">
Elterndienst
</Badge>
</div>
<CardTitle className="line-clamp-2 leading-tight">
{duty.title}
</CardTitle>
<CardDescription className="mt-1 flex items-center gap-1 text-sm">
<Clock className="h-3.5 w-3.5" />
{format(duty.startDate, "dd.MM.", { locale: de })} -{" "}
{format(duty.endDate, "dd.MM.yyyy", { locale: de })}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-4">
<div className="flex items-start gap-2 text-sm text-emerald-900">
<WashingMachine className="mt-0.5 h-4 w-4 shrink-0" />
<p>
Eingeteilt: <span className="font-medium">{duty.familyName}</span>
</p>
</div>
</CardContent>
</Card>
);
}
function TerminCard({
termin,
userId,
isAdmin,
}: {
termin: TerminWithItems;
termin: TerminListItemDto;
userId: string;
isAdmin: boolean;
}) {
+20 -4
View File
@@ -55,6 +55,10 @@ export async function createTerminRequest(rawPayload: unknown) {
export async function createTerminAdmin(rawPayload: unknown) {
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
const kitaId = session.user.kitaId;
if (!kitaId) {
return { error: "Kein Mandant zugeordnet." };
}
const parsed = terminSchema.safeParse(rawPayload);
if (!parsed.success) {
@@ -66,7 +70,7 @@ export async function createTerminAdmin(rawPayload: unknown) {
try {
await prisma.termin.create({
data: {
kitaId: session.user.kitaId,
kitaId,
createdById: session.user.id,
title: data.title,
description: data.description,
@@ -90,12 +94,16 @@ export async function createTerminAdmin(rawPayload: unknown) {
export async function approveTermin(terminId: string) {
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
const kitaId = session.user.kitaId;
if (!kitaId) {
return { error: "Kein Mandant zugeordnet." };
}
try {
await prisma.termin.update({
where: {
id: terminId,
kitaId: session.user.kitaId,
kitaId,
},
data: {
status: TerminStatus.CONFIRMED,
@@ -114,12 +122,16 @@ export async function approveTermin(terminId: string) {
export async function rejectTermin(terminId: string, reason?: string) {
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
const kitaId = session.user.kitaId;
if (!kitaId) {
return { error: "Kein Mandant zugeordnet." };
}
try {
await prisma.termin.update({
where: {
id: terminId,
kitaId: session.user.kitaId,
kitaId,
},
data: {
status: TerminStatus.REJECTED,
@@ -139,12 +151,16 @@ export async function rejectTermin(terminId: string, reason?: string) {
export async function toggleMitbringselList(terminId: string, enabled: boolean) {
const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]);
const kitaId = session.user.kitaId;
if (!kitaId) {
return { error: "Kein Mandant zugeordnet." };
}
try {
await prisma.termin.update({
where: {
id: terminId,
kitaId: session.user.kitaId,
kitaId,
},
data: {
mitbringselListEnabled: enabled,
+110 -22
View File
@@ -1,11 +1,11 @@
import { DutyAssignmentStatus, TerminStatus, UserRole } from "@prisma/client";
import { requireKitaSession } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import { UserRole, TerminStatus } from "@prisma/client";
import { TerminList } from "./_components/termin-list";
import { PendingAnfragen } from "./_components/pending-anfragen";
import { TerminRequestModal } from "./_components/termin-request-modal";
import { AdminTerminModal } from "./_components/admin-termin-modal";
import { CalendarDays } from "lucide-react";
import { PendingAnfragen, type PendingTerminDto } from "./_components/pending-anfragen";
import { TerminList, type CalendarListItemDto } from "./_components/termin-list";
import { TerminRequestModal } from "./_components/termin-request-modal";
export default async function KalenderPage({
searchParams,
@@ -14,51 +14,139 @@ export default async function KalenderPage({
}) {
const session = await requireKitaSession();
const isAdmin =
session.user.role === UserRole.ADMIN || session.user.role === UserRole.KOORDINATOR;
session.user.role === UserRole.ADMIN ||
session.user.role === UserRole.KOORDINATOR;
const currentTab = searchParams.tab || "übersicht";
// Fetch confirmed events
const confirmedTermine = await prisma.termin.findMany({
const confirmedTermineRows = await prisma.termin.findMany({
where: {
kitaId: session.user.kitaId,
status: TerminStatus.CONFIRMED,
},
include: {
select: {
id: true,
title: true,
description: true,
type: true,
status: true,
startDate: true,
endDate: true,
allDay: true,
mitbringselListEnabled: true,
mitbringselItems: {
include: {
user: { select: { firstName: true, lastName: true } },
select: {
id: true,
userId: true,
content: true,
user: {
select: {
firstName: true,
lastName: true,
},
},
},
},
},
orderBy: { startDate: "asc" },
});
// Fetch user's own pending events
const myPendingTermine = await prisma.termin.findMany({
const myPendingTermineRows = await prisma.termin.findMany({
where: {
kitaId: session.user.kitaId,
status: TerminStatus.PENDING,
createdById: session.user.id,
},
select: {
id: true,
title: true,
description: true,
type: true,
status: true,
startDate: true,
endDate: true,
allDay: true,
mitbringselListEnabled: true,
mitbringselItems: {
select: {
id: true,
userId: true,
content: true,
user: {
select: {
firstName: true,
lastName: true,
},
},
},
},
},
orderBy: { startDate: "asc" },
});
// Combine for general view
const allUserTermine = [...confirmedTermine, ...myPendingTermine].sort(
(a, b) => a.startDate.getTime() - b.startDate.getTime()
);
const dutyAssignmentRows = await prisma.dutyAssignment.findMany({
where: {
kitaId: session.user.kitaId,
status: DutyAssignmentStatus.PLANNED,
endDate: { gte: new Date() },
},
select: {
id: true,
startDate: true,
endDate: true,
status: true,
dutyType: {
select: {
name: true,
},
},
family: {
select: {
name: true,
},
},
},
orderBy: { startDate: "asc" },
});
// If admin, fetch all pending events
let allPendingTermine: any[] = [];
const allUserTermine: CalendarListItemDto[] = [
...confirmedTermineRows.map((termin) => ({
...termin,
kind: "termin" as const,
})),
...myPendingTermineRows.map((termin) => ({
...termin,
kind: "termin" as const,
})),
...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,
})),
].sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
let allPendingTermine: PendingTerminDto[] = [];
if (isAdmin) {
allPendingTermine = await prisma.termin.findMany({
where: {
kitaId: session.user.kitaId,
status: TerminStatus.PENDING,
},
include: {
createdBy: { select: { firstName: true, lastName: true } },
select: {
id: true,
title: true,
startDate: true,
allDay: true,
createdBy: {
select: {
firstName: true,
lastName: true,
},
},
},
orderBy: { createdAt: "desc" },
});
@@ -94,7 +182,7 @@ export default async function KalenderPage({
</a>
<a
href="/dashboard/kalender?tab=anfragen"
className={`pb-2 text-sm font-medium transition-colors hover:text-primary flex items-center gap-2 ${
className={`flex items-center gap-2 pb-2 text-sm font-medium transition-colors hover:text-primary ${
currentTab === "anfragen"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground"