feat: show planning availability and manage Kita access roles
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { UserRole } from "@prisma/client";
|
||||
import { RoleChangeButton } from "../../zugriffsrechte/role-change-button";
|
||||
import { RoleBadge } from "@/components/ui/role-badge";
|
||||
import { useState, useTransition } from "react";
|
||||
import { LayoutGrid, Mail, Search, Table, Users } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,6 +32,8 @@ interface FamilyUser {
|
||||
email: string;
|
||||
emailVerifiedAt: Date | null;
|
||||
invitation: FamilyInvitationView;
|
||||
role: UserRole;
|
||||
staffProfile: { id: string } | null;
|
||||
dutyAssignments: {
|
||||
id: string;
|
||||
dutyId: string;
|
||||
@@ -60,12 +65,14 @@ interface DutyData {
|
||||
}
|
||||
|
||||
interface FamiliesListClientProps {
|
||||
currentUserId: string;
|
||||
initialFamilies: FamilyData[];
|
||||
allDuties: DutyData[];
|
||||
canManageDuties: boolean;
|
||||
}
|
||||
|
||||
export function FamiliesListClient({
|
||||
currentUserId,
|
||||
initialFamilies,
|
||||
allDuties,
|
||||
canManageDuties,
|
||||
@@ -85,7 +92,7 @@ export function FamiliesListClient({
|
||||
(user) =>
|
||||
user.firstName.toLowerCase().includes(q) ||
|
||||
user.lastName.toLowerCase().includes(q) ||
|
||||
user.email.toLowerCase().includes(q)
|
||||
user.email.toLowerCase().includes(q),
|
||||
);
|
||||
if (matchesParent) return true;
|
||||
|
||||
@@ -93,20 +100,20 @@ export function FamiliesListClient({
|
||||
const matchesChild = family.children.some(
|
||||
(child) =>
|
||||
child.firstName.toLowerCase().includes(q) ||
|
||||
child.lastName.toLowerCase().includes(q)
|
||||
child.lastName.toLowerCase().includes(q),
|
||||
);
|
||||
return matchesChild;
|
||||
});
|
||||
|
||||
const childCount = filteredFamilies.reduce(
|
||||
(acc, family) => acc + family.children.length,
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
const pendingInviteCount = filteredFamilies.reduce(
|
||||
(acc, family) =>
|
||||
acc + family.users.filter((user) => !user.emailVerifiedAt).length,
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -123,14 +130,17 @@ export function FamiliesListClient({
|
||||
</em>
|
||||
</h1>
|
||||
<p className="mt-2 max-w-2xl text-[15px] leading-6 text-[var(--ink-soft)]">
|
||||
Haushalte, Elternteile, Kinder und Ämter an einem ruhigen Ort pflegen.
|
||||
Haushalte, Elternteile, Kinder und Ämter an einem ruhigen Ort
|
||||
pflegen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setViewMode(viewMode === "table" ? "cards" : "table")}
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === "table" ? "cards" : "table")
|
||||
}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{viewMode === "table" ? (
|
||||
@@ -152,7 +162,9 @@ export function FamiliesListClient({
|
||||
<div className="mb-5 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex h-9 items-center gap-2 rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-3 text-[13px] font-semibold text-[var(--ink-soft)]">
|
||||
<span className="text-[var(--ink)] tabular-nums">{filteredFamilies.length}</span>
|
||||
<span className="text-[var(--ink)] tabular-nums">
|
||||
{filteredFamilies.length}
|
||||
</span>
|
||||
Familien
|
||||
</span>
|
||||
<span className="inline-flex h-9 items-center gap-2 rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-3 text-[13px] font-semibold text-[var(--ink-soft)]">
|
||||
@@ -160,7 +172,9 @@ export function FamiliesListClient({
|
||||
Kinder
|
||||
</span>
|
||||
<span className="inline-flex h-9 items-center gap-2 rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-3 text-[13px] font-semibold text-[var(--ink-soft)]">
|
||||
<span className="text-[var(--ink)] tabular-nums">{pendingInviteCount}</span>
|
||||
<span className="text-[var(--ink)] tabular-nums">
|
||||
{pendingInviteCount}
|
||||
</span>
|
||||
Offene Einladungen
|
||||
</span>
|
||||
</div>
|
||||
@@ -196,7 +210,8 @@ export function FamiliesListClient({
|
||||
<div>
|
||||
<CardTitle>Haushalte</CardTitle>
|
||||
<p className="mt-1 text-[13.5px] text-[var(--ink-soft)]">
|
||||
{filteredFamilies.length} {filteredFamilies.length === 1 ? "Familie" : "Familien"} ·{" "}
|
||||
{filteredFamilies.length}{" "}
|
||||
{filteredFamilies.length === 1 ? "Familie" : "Familien"} ·{" "}
|
||||
{childCount} Kinder · {pendingInviteCount} offene Einladungen
|
||||
</p>
|
||||
</div>
|
||||
@@ -218,7 +233,7 @@ export function FamiliesListClient({
|
||||
<TableBody>
|
||||
{filteredFamilies.map((family) => {
|
||||
const hasPendingInvite = family.users.some(
|
||||
(user) => !user.emailVerifiedAt
|
||||
(user) => !user.emailVerifiedAt,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -250,10 +265,29 @@ export function FamiliesListClient({
|
||||
<Mail className="h-3 w-3 shrink-0 [stroke-width:1.7]" />
|
||||
<span className="break-words [overflow-wrap:anywhere]">
|
||||
{parent.email}
|
||||
{!parent.emailVerifiedAt && <ResendInviteButton userId={parent.id} />}
|
||||
{!parent.emailVerifiedAt && (
|
||||
<ResendInviteButton userId={parent.id} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<InvitationStatus userId={parent.id} name={`${parent.firstName} ${parent.lastName}`} invitation={parent.invitation} />
|
||||
<InvitationStatus
|
||||
userId={parent.id}
|
||||
name={`${parent.firstName} ${parent.lastName}`}
|
||||
invitation={parent.invitation}
|
||||
/>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<RoleBadge role={parent.role} />
|
||||
<RoleChangeButton
|
||||
ownAccount={parent.id === currentUserId}
|
||||
person={{
|
||||
id: parent.id,
|
||||
name: `${parent.firstName} ${parent.lastName}`,
|
||||
role: parent.role,
|
||||
familyId: family.id,
|
||||
hasStaffProfile: !!parent.staffProfile,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -277,7 +311,9 @@ export function FamiliesListClient({
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={hasPendingInvite ? "warning" : "success"}>
|
||||
<Badge
|
||||
variant={hasPendingInvite ? "warning" : "success"}
|
||||
>
|
||||
{hasPendingInvite ? "Einladung offen" : "Aktiv"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -323,7 +359,7 @@ export function FamiliesListClient({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredFamilies.map((family) => {
|
||||
const hasPendingInvite = family.users.some(
|
||||
(user) => !user.emailVerifiedAt
|
||||
(user) => !user.emailVerifiedAt,
|
||||
);
|
||||
return (
|
||||
<Card key={family.id} className="flex flex-col h-full">
|
||||
@@ -333,9 +369,12 @@ export function FamiliesListClient({
|
||||
{family.name.slice(0, 1)}
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-semibold">{family.name}</CardTitle>
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{family.name}
|
||||
</CardTitle>
|
||||
<p className="text-xs text-[var(--ink-muted)]">
|
||||
{family.users.length} Elternteil{family.users.length === 1 ? "" : "e"}
|
||||
{family.users.length} Elternteil
|
||||
{family.users.length === 1 ? "" : "e"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,10 +398,29 @@ export function FamiliesListClient({
|
||||
<Mail className="h-3.5 w-3.5 shrink-0 [stroke-width:1.7]" />
|
||||
<span className="break-words [overflow-wrap:anywhere]">
|
||||
{parent.email}
|
||||
{!parent.emailVerifiedAt && <ResendInviteButton userId={parent.id} />}
|
||||
{!parent.emailVerifiedAt && (
|
||||
<ResendInviteButton userId={parent.id} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<InvitationStatus userId={parent.id} name={`${parent.firstName} ${parent.lastName}`} invitation={parent.invitation} />
|
||||
<InvitationStatus
|
||||
userId={parent.id}
|
||||
name={`${parent.firstName} ${parent.lastName}`}
|
||||
invitation={parent.invitation}
|
||||
/>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<RoleBadge role={parent.role} />
|
||||
<RoleChangeButton
|
||||
ownAccount={parent.id === currentUserId}
|
||||
person={{
|
||||
id: parent.id,
|
||||
name: `${parent.firstName} ${parent.lastName}`,
|
||||
role: parent.role,
|
||||
familyId: family.id,
|
||||
hasStaffProfile: !!parent.staffProfile,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -430,8 +488,21 @@ export function FamiliesListClient({
|
||||
|
||||
function ResendInviteButton({ userId }: { userId: string }) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
return <Button type="button" size="sm" variant="ghost" disabled={pending} onClick={() => startTransition(async () => {
|
||||
const result = await resendParentInvite(userId);
|
||||
if (result.error) toast.error(result.error); else toast.success("Einladung erneut gesendet.");
|
||||
})}>{pending ? "Wird gesendet…" : "Einladung erneut senden"}</Button>;
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={pending}
|
||||
onClick={() =>
|
||||
startTransition(async () => {
|
||||
const result = await resendParentInvite(userId);
|
||||
if (result.error) toast.error(result.error);
|
||||
else toast.success("Einladung erneut gesendet.");
|
||||
})
|
||||
}
|
||||
>
|
||||
{pending ? "Wird gesendet…" : "Einladung erneut senden"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user