601 lines
27 KiB
TypeScript
601 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import {
|
|
Baby,
|
|
Contact,
|
|
Mail,
|
|
Phone,
|
|
Search,
|
|
ShieldCheck,
|
|
SlidersHorizontal,
|
|
UserRound,
|
|
Home,
|
|
MapPin,
|
|
} from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
|
|
export type DirectoryParentDto = {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
hasDirectoryOptIn: boolean;
|
|
email?: string;
|
|
phone?: string;
|
|
street?: string;
|
|
postalCode?: string;
|
|
city?: string;
|
|
duties: {
|
|
id: string;
|
|
name: string;
|
|
}[];
|
|
};
|
|
|
|
export type DirectoryFamilyDto = {
|
|
id: string;
|
|
name: string;
|
|
children: {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
dateOfBirth?: Date | null;
|
|
}[];
|
|
parents: DirectoryParentDto[];
|
|
};
|
|
|
|
export function DirectoryClient({
|
|
initialFamilies,
|
|
}: {
|
|
initialFamilies: DirectoryFamilyDto[];
|
|
}) {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());
|
|
const [selectedFamily, setSelectedFamily] = useState<DirectoryFamilyDto | null>(null);
|
|
const [privacyDialogOpen, setPrivacyDialogOpen] = useState(false);
|
|
|
|
const toggleFilter = (filterKey: string) => {
|
|
setActiveFilters((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(filterKey)) {
|
|
next.delete(filterKey);
|
|
} else {
|
|
next.add(filterKey);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const filteredFamilies = useMemo(() => {
|
|
return initialFamilies.filter((family) => {
|
|
// 1. Search Query filter
|
|
const query = searchQuery.trim().toLowerCase();
|
|
if (query) {
|
|
const familyNameMatch = family.name.toLowerCase().includes(query);
|
|
const childMatch = family.children.some(
|
|
(c) =>
|
|
c.firstName.toLowerCase().includes(query) ||
|
|
c.lastName.toLowerCase().includes(query)
|
|
);
|
|
const parentMatch = family.parents.some(
|
|
(p) =>
|
|
p.firstName.toLowerCase().includes(query) ||
|
|
p.lastName.toLowerCase().includes(query) ||
|
|
(p.email && p.email.toLowerCase().includes(query))
|
|
);
|
|
if (!familyNameMatch && !childMatch && !parentMatch) return false;
|
|
}
|
|
|
|
// 2. Custom filters
|
|
if (activeFilters.has("telefon")) {
|
|
const hasPhone = family.parents.some((p) => p.hasDirectoryOptIn && p.phone);
|
|
if (!hasPhone) return false;
|
|
}
|
|
|
|
if (activeFilters.has("aemter")) {
|
|
const hasDuties = family.parents.some((p) => p.duties.length > 0);
|
|
if (!hasDuties) return false;
|
|
}
|
|
|
|
if (activeFilters.has("optIn")) {
|
|
// Show households where all parents in the system have opted in
|
|
const allOptIn = family.parents.every((p) => p.hasDirectoryOptIn);
|
|
if (!allOptIn) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}, [initialFamilies, searchQuery, activeFilters]);
|
|
|
|
// Recalculate metrics for filtered subset
|
|
const visibleParentCount = useMemo(() => {
|
|
return filteredFamilies.reduce(
|
|
(count, fam) => count + fam.parents.filter((p) => p.hasDirectoryOptIn).length,
|
|
0
|
|
);
|
|
}, [filteredFamilies]);
|
|
|
|
const childCount = useMemo(() => {
|
|
return filteredFamilies.reduce((count, fam) => count + fam.children.length, 0);
|
|
}, [filteredFamilies]);
|
|
|
|
const totalVisibleParentCount = useMemo(() => {
|
|
return initialFamilies.reduce(
|
|
(count, fam) => count + fam.parents.filter((p) => p.hasDirectoryOptIn).length,
|
|
0
|
|
);
|
|
}, [initialFamilies]);
|
|
|
|
const groupedFamilies = useMemo(() => {
|
|
const groups = new Map<string, DirectoryFamilyDto[]>();
|
|
filteredFamilies.forEach((family) => {
|
|
const initial = family.name.trim().slice(0, 1).toUpperCase();
|
|
const key = initial || "#";
|
|
groups.set(key, [...(groups.get(key) ?? []), family]);
|
|
});
|
|
return Array.from(groups.entries())
|
|
.map(([initial, families]) => ({ initial, families }))
|
|
.sort((a, b) => a.initial.localeCompare(b.initial));
|
|
}, [filteredFamilies]);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-[1280px] px-11 py-9 pb-16 max-[760px]:px-5">
|
|
{/* Header */}
|
|
<header className="mb-7 flex items-end justify-between gap-8 max-[900px]:flex-col max-[900px]:items-start">
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase text-[var(--accent-deep)] [letter-spacing:0.18em]">
|
|
KONTAKTE · ADRESSBUCH
|
|
</p>
|
|
<h1 className="mt-2 text-[36px] font-normal leading-tight tracking-[-0.025em] text-[var(--ink)]">
|
|
Freigegebene{" "}
|
|
<em className="font-normal italic text-[var(--accent-deep)]">
|
|
Kontakte
|
|
</em>
|
|
</h1>
|
|
<p className="mt-2 max-w-2xl text-[15px] leading-6 text-[var(--ink-soft)]">
|
|
Haushalte und Kontaktdaten, die explizit für das interne
|
|
Kita-Adressbuch freigegeben wurden.
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 flex-wrap gap-2">
|
|
<span className="inline-flex h-9 items-center gap-2 rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-4 text-[13px] font-semibold text-[var(--ink-soft)] tabular-nums">
|
|
<ShieldCheck className="h-4 w-4 text-[var(--accent-deep)] [stroke-width:1.7]" />
|
|
{totalVisibleParentCount} sichtbar
|
|
</span>
|
|
<Button variant="ghost" size="sm" onClick={() => setPrivacyDialogOpen(true)}>
|
|
<SlidersHorizontal className="h-4 w-4 [stroke-width:1.7]" />
|
|
Sichtbarkeit
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Household List or Empty State */}
|
|
{initialFamilies.length === 0 ? (
|
|
<Card className="border-dashed">
|
|
<CardContent className="flex max-h-[280px] min-h-[260px] flex-col items-center justify-center px-6 py-12 text-center">
|
|
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--accent-soft)] text-[var(--accent-deep)]">
|
|
<Contact className="h-6 w-6 [stroke-width:1.7]" />
|
|
</div>
|
|
<h3 className="text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
|
|
Keine freigegebenen Kontakte
|
|
</h3>
|
|
<p className="mt-2 max-w-sm text-sm leading-6 text-[var(--ink-soft)]">
|
|
Sobald mindestens ein Elternteil eines Haushalts zustimmt,
|
|
erscheint die Familie hier.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-5">
|
|
{/* Metrics & Search Bar */}
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div className="flex flex-wrap gap-2">
|
|
<DirectoryMetric label="Haushalte" value={filteredFamilies.length} />
|
|
<DirectoryMetric label="Kontakte" value={visibleParentCount} />
|
|
<DirectoryMetric label="Kinder" value={childCount} />
|
|
</div>
|
|
<div className="relative min-w-72 max-[760px]:w-full max-[760px]:min-w-0">
|
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--ink-muted)] [stroke-width:1.7]" />
|
|
<Input
|
|
placeholder="Familien, Namen oder E-Mail suchen"
|
|
className="pl-9"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom Filter Chips */}
|
|
<div className="flex flex-wrap gap-1.5">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveFilters(new Set())}
|
|
className={[
|
|
"inline-flex h-9 items-center rounded-full border px-3 text-[13px] font-medium transition",
|
|
activeFilters.size === 0
|
|
? "border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
|
: "border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink-soft)] hover:bg-[var(--surface-2)]",
|
|
].join(" ")}
|
|
>
|
|
Alle anzeigen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleFilter("telefon")}
|
|
className={[
|
|
"inline-flex h-9 items-center rounded-full border px-3 text-[13px] font-medium transition",
|
|
activeFilters.has("telefon")
|
|
? "border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
|
: "border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink-soft)] hover:bg-[var(--surface-2)]",
|
|
].join(" ")}
|
|
>
|
|
Mit Telefon
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleFilter("aemter")}
|
|
className={[
|
|
"inline-flex h-9 items-center rounded-full border px-3 text-[13px] font-medium transition",
|
|
activeFilters.has("aemter")
|
|
? "border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
|
: "border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink-soft)] hover:bg-[var(--surface-2)]",
|
|
].join(" ")}
|
|
>
|
|
Mit Ämtern
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleFilter("optIn")}
|
|
className={[
|
|
"inline-flex h-9 items-center rounded-full border px-3 text-[13px] font-medium transition",
|
|
activeFilters.has("optIn")
|
|
? "border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
|
: "border-[var(--hairline)] bg-[var(--surface)] text-[var(--ink-soft)] hover:bg-[var(--surface-2)]",
|
|
].join(" ")}
|
|
>
|
|
Haushalt komplett freigegeben
|
|
</button>
|
|
</div>
|
|
|
|
{/* Household list cards wrapper */}
|
|
{filteredFamilies.length === 0 ? (
|
|
<Card className="border-dashed">
|
|
<CardContent className="flex max-h-[280px] min-h-[260px] flex-col items-center justify-center px-6 py-12 text-center">
|
|
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--accent-soft)] text-[var(--accent-deep)]">
|
|
<SlidersHorizontal className="h-6 w-6 [stroke-width:1.7]" />
|
|
</div>
|
|
<h3 className="text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
|
|
Keine passenden Kontakte gefunden
|
|
</h3>
|
|
<p className="mt-2 max-w-sm text-sm leading-6 text-[var(--ink-soft)]">
|
|
Passe deine Suche oder die Filterchips an, um Einträge zu sehen.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="overflow-hidden p-0">
|
|
{groupedFamilies.map((group) => (
|
|
<section key={group.initial}>
|
|
<div className="flex items-center justify-between border-b border-[var(--hairline-soft)] bg-[var(--surface-2)] px-5 py-3">
|
|
<h2 className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.14em]">
|
|
{group.initial}
|
|
</h2>
|
|
<span className="text-[11px] font-semibold text-[var(--ink-muted)] tabular-nums">
|
|
{group.families.length}
|
|
</span>
|
|
</div>
|
|
<div className="divide-y divide-[var(--hairline-soft)]">
|
|
{group.families.map((family) => {
|
|
const visibleParents = family.parents.filter(
|
|
(parent) => parent.hasDirectoryOptIn
|
|
);
|
|
const duties = family.parents.flatMap((parent) => parent.duties);
|
|
|
|
return (
|
|
<div
|
|
key={family.id}
|
|
className="grid grid-cols-[minmax(220px,0.8fr)_minmax(0,1.4fr)_minmax(220px,1fr)] gap-5 px-5 py-4 transition-colors hover:bg-[var(--surface-2)] max-[1050px]:grid-cols-1"
|
|
>
|
|
{/* Left: Family Name & Children */}
|
|
<div className="flex min-w-0 gap-3">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-sm font-semibold uppercase text-[var(--accent-deep)]">
|
|
{family.name.slice(0, 1)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 className="truncate text-[15px] font-semibold text-[var(--ink)]">
|
|
{family.name}
|
|
</h3>
|
|
<p className="mt-1 flex flex-wrap items-center gap-1.5 text-[13px] text-[var(--ink-soft)]">
|
|
<Baby className="h-3.5 w-3.5 [stroke-width:1.7]" />
|
|
{family.children.length === 0
|
|
? "Keine Kinder hinterlegt"
|
|
: family.children
|
|
.map((child) => `${child.firstName} ${child.lastName}`)
|
|
.join(", ")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Middle: Parents */}
|
|
<div className="grid gap-3">
|
|
{family.parents.map((user) => (
|
|
<div key={user.id} className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<UserRound className="h-3.5 w-3.5 text-[var(--ink-muted)] [stroke-width:1.7]" />
|
|
<p className="text-sm font-medium text-[var(--ink)]">
|
|
{user.firstName} {user.lastName}
|
|
</p>
|
|
{user.hasDirectoryOptIn ? (
|
|
<Badge variant="success">Opt-in</Badge>
|
|
) : (
|
|
<span className="text-xs italic text-[var(--ink-faint)]">
|
|
nicht freigegeben
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{user.hasDirectoryOptIn && (
|
|
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 pl-5 text-[13px] text-[var(--ink-soft)]">
|
|
{user.email && (
|
|
<a
|
|
href={`mailto:${user.email}`}
|
|
className="inline-flex min-w-0 items-center gap-1.5 hover:text-[var(--accent-deep)]"
|
|
>
|
|
<Mail className="h-3.5 w-3.5 shrink-0 [stroke-width:1.7]" />
|
|
<span className="break-words [overflow-wrap:anywhere]">
|
|
{user.email}
|
|
</span>
|
|
</a>
|
|
)}
|
|
{user.phone ? (
|
|
<a
|
|
href={`tel:${user.phone}`}
|
|
className="inline-flex items-center gap-1.5 hover:text-[var(--accent-deep)]"
|
|
>
|
|
<Phone className="h-3.5 w-3.5 shrink-0 [stroke-width:1.7]" />
|
|
{user.phone}
|
|
</a>
|
|
) : (
|
|
<span className="italic text-[var(--ink-faint)]">
|
|
Keine Telefonnummer
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right: Badges & Details Action */}
|
|
<div className="flex flex-col items-end justify-between gap-3 max-[1050px]:items-start">
|
|
<div className="flex flex-wrap justify-end gap-1.5 max-[1050px]:justify-start">
|
|
<Badge variant="secondary">
|
|
{visibleParents.length} von {family.parents.length} sichtbar
|
|
</Badge>
|
|
{duties.map((duty) => (
|
|
<Badge
|
|
key={duty.id}
|
|
variant="outline"
|
|
className="border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
|
|
>
|
|
{duty.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSelectedFamily(family)}
|
|
>
|
|
Kontakt öffnen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
))}
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Household Detail Dialog */}
|
|
<Dialog
|
|
open={!!selectedFamily}
|
|
onOpenChange={(open) => !open && setSelectedFamily(null)}
|
|
>
|
|
<DialogContent className="max-w-xl max-h-[85vh] overflow-y-auto">
|
|
{selectedFamily && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[var(--accent-soft)] text-[var(--accent-deep)] text-xl font-bold uppercase mb-2">
|
|
{selectedFamily.name.slice(0, 1)}
|
|
</div>
|
|
<DialogTitle className="text-2xl">Familie {selectedFamily.name}</DialogTitle>
|
|
<DialogDescription>
|
|
Detaillierte Kontaktdaten und Haushalts-Zugehörigkeiten.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="mt-4 space-y-6">
|
|
{/* Children Details */}
|
|
<div>
|
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--ink-muted)] mb-3 flex items-center gap-1.5">
|
|
<Baby className="h-4 w-4" />
|
|
Kinder im Haushalt
|
|
</h4>
|
|
<div className="grid gap-2.5">
|
|
{selectedFamily.children.length === 0 ? (
|
|
<p className="text-sm italic text-[var(--ink-soft)]">Keine Kinder hinterlegt.</p>
|
|
) : (
|
|
selectedFamily.children.map((child) => (
|
|
<div
|
|
key={child.id}
|
|
className="flex items-center justify-between rounded-xl border border-[var(--hairline)] bg-[var(--surface-2)] px-4 py-3"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Baby className="h-4 w-4 text-[var(--accent-deep)]" />
|
|
<span className="font-semibold text-sm text-[var(--ink)]">
|
|
{child.firstName} {child.lastName}
|
|
</span>
|
|
</div>
|
|
{child.dateOfBirth && (
|
|
<span className="text-xs text-[var(--ink-soft)] font-medium">
|
|
Geboren: {format(new Date(child.dateOfBirth), "dd.MM.yyyy", { locale: de })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Parents/Users Details */}
|
|
<div>
|
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--ink-muted)] mb-3 flex items-center gap-1.5">
|
|
<UserRound className="h-4 w-4" />
|
|
Eltern & Kontakte
|
|
</h4>
|
|
<div className="space-y-4">
|
|
{selectedFamily.parents.map((parent) => (
|
|
<div
|
|
key={parent.id}
|
|
className="rounded-xl border border-[var(--hairline)] bg-[var(--surface)] p-4 shadow-sm"
|
|
>
|
|
<div className="flex items-center justify-between gap-2 border-b border-[var(--hairline-soft)] pb-2 mb-3">
|
|
<span className="font-semibold text-[15px] text-[var(--ink)] flex items-center gap-1.5">
|
|
<UserRound className="h-4 w-4 text-[var(--ink-muted)]" />
|
|
{parent.firstName} {parent.lastName}
|
|
</span>
|
|
{parent.hasDirectoryOptIn ? (
|
|
<Badge variant="success">Freigegeben</Badge>
|
|
) : (
|
|
<Badge variant="secondary">Privat</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{parent.hasDirectoryOptIn ? (
|
|
<div className="space-y-2.5 text-sm text-[var(--ink-soft)]">
|
|
{parent.email && (
|
|
<a
|
|
href={`mailto:${parent.email}`}
|
|
className="flex items-center gap-2.5 hover:text-[var(--accent-deep)]"
|
|
>
|
|
<Mail className="h-4 w-4 text-[var(--ink-muted)] shrink-0" />
|
|
<span className="break-all">{parent.email}</span>
|
|
</a>
|
|
)}
|
|
{parent.phone ? (
|
|
<a
|
|
href={`tel:${parent.phone}`}
|
|
className="flex items-center gap-2.5 hover:text-[var(--accent-deep)]"
|
|
>
|
|
<Phone className="h-4 w-4 text-[var(--ink-muted)] shrink-0" />
|
|
<span>{parent.phone}</span>
|
|
</a>
|
|
) : (
|
|
<div className="flex items-center gap-2.5 italic text-[var(--ink-faint)]">
|
|
<Phone className="h-4 w-4 text-[var(--ink-muted)] shrink-0" />
|
|
<span>Keine Telefonnummer hinterlegt</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Address details */}
|
|
{(parent.street || parent.postalCode || parent.city) && (
|
|
<div className="flex items-start gap-2.5 border-t border-[var(--hairline-soft)] pt-2.5 mt-2.5">
|
|
<MapPin className="h-4 w-4 text-[var(--ink-muted)] shrink-0 mt-0.5" />
|
|
<div>
|
|
{parent.street && <p>{parent.street}</p>}
|
|
{(parent.postalCode || parent.city) && (
|
|
<p>
|
|
{parent.postalCode} {parent.city}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs italic text-[var(--ink-faint)]">
|
|
Dieser Kontakt hat seine Daten nicht für das Adressbuch freigegeben.
|
|
</p>
|
|
)}
|
|
|
|
{parent.duties.length > 0 && (
|
|
<div className="mt-3 border-t border-[var(--hairline-soft)] pt-3 flex flex-wrap gap-1.5">
|
|
{parent.duties.map((duty) => (
|
|
<Badge
|
|
key={duty.id}
|
|
variant="outline"
|
|
className="border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)] text-[10px]"
|
|
>
|
|
{duty.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Global Privacy settings dialog */}
|
|
<Dialog open={privacyDialogOpen} onOpenChange={setPrivacyDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<ShieldCheck className="h-5 w-5 text-[var(--accent-deep)]" />
|
|
Sichtbarkeit & Datenschutz
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-2 text-sm leading-6">
|
|
Deine Privatsphäre ist uns wichtig. In diesem Adressbuch werden Kontaktdaten
|
|
nur für Haushalte angezeigt, die dem Opt-in für das interne Kita-Adressbuch
|
|
explizit zugestimmt haben.
|
|
<br />
|
|
<br />
|
|
Du kannst deine eigene Zustimmung sowie deine E-Mail-Adresse und Telefonnummer
|
|
jederzeit in deinen persönlichen Profileinstellungen einsehen und anpassen.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex justify-end gap-3 mt-4">
|
|
<Button variant="ghost" onClick={() => setPrivacyDialogOpen(false)}>
|
|
Schließen
|
|
</Button>
|
|
<a href="/dashboard/profil">
|
|
<Button>Zu den Profileinstellungen</Button>
|
|
</a>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DirectoryMetric({ label, value }: { label: string; value: number }) {
|
|
return (
|
|
<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">{value}</span>
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|