import Link from "next/link"; import { DraftsCard } from "@/components/dashboard/DraftsCard"; import { actionBadgeStyle } from "@/lib/colors"; import { fmtDate } from "@/lib/format"; import { createClient } from "@/lib/supabase/server"; function isoDate(d: Date): string { return d.toISOString().slice(0, 10); } const TONE_TEXT: Record = { default: "text-ink", success: "text-success-text", danger: "text-danger-text", warning: "text-warning-text", brand: "text-brand-700", }; const DOT_STYLES: Record = { Eintritt: "bg-success-text", Wiedereintritt: "bg-success-text", Rückkehr: "bg-success-text", Austritt: "bg-danger-text", Versetzung: "bg-info-text", Beförderung: "bg-purple-text", Reorganisation: "bg-purple-text", Karenz: "bg-warning-text", Vertragsänderung: "bg-warning-text", Stammdatenänderung: "bg-warning-text", Gehaltsanpassung: "bg-warning-text", }; const KIND_LABEL = { hire: "Eintritt", exit: "Austritt", return: "Karenz-Rückkehr" } as const; export default async function DashboardPage() { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); const { data: drafts } = user ? await supabase .from("hire_drafts") .select("id, step, payload, updated_at") .eq("created_by", user.id) .order("updated_at", { ascending: false }) : { data: [] }; const today = new Date(); const todayIso = isoDate(today); const yearStart = isoDate(new Date(today.getFullYear(), 0, 1)); const yearEnd = isoDate(new Date(today.getFullYear(), 11, 31)); const in60 = new Date(today); in60.setDate(in60.getDate() + 60); const in60Iso = isoDate(in60); const [ activeCountRes, karenzCountRes, hiresYtdRes, exitsYtdRes, openPositionsRes, fteRowsRes, divisionsRes, headcountRowsRes, upcomingHiresRes, upcomingExitsRes, upcomingReturnsRes, historyRes, ] = await Promise.all([ supabase.from("employees").select("id", { count: "exact", head: true }).in("status", ["Aktiv", "Karenz"]), supabase.from("employees").select("id", { count: "exact", head: true }).eq("status", "Karenz"), supabase .from("employees") .select("id", { count: "exact", head: true }) .gte("entry_date", yearStart) .lte("entry_date", yearEnd), supabase .from("employees") .select("id", { count: "exact", head: true }) .gte("exit_date", yearStart) .lte("exit_date", yearEnd), supabase.from("positions").select("id", { count: "exact", head: true }).eq("status", "open"), supabase.from("employees").select("weekly_hours").in("status", ["Aktiv", "Karenz"]), supabase.from("divisions").select("id, name"), supabase.from("employees").select("division_id").in("status", ["Aktiv", "Karenz"]), supabase .from("employees") .select("id, first_name, last_name, entry_date") .eq("status", "Geplant") .gte("entry_date", todayIso) .lte("entry_date", in60Iso), supabase .from("employees") .select("id, first_name, last_name, exit_date") .not("exit_date", "is", null) .gte("exit_date", todayIso) .lte("exit_date", in60Iso), supabase .from("employees") .select("id, first_name, last_name, karenz_return_date") .eq("status", "Karenz") .not("karenz_return_date", "is", null) .gte("karenz_return_date", todayIso) .lte("karenz_return_date", in60Iso), supabase .from("employee_history") .select("id, employee_id, event_date, event_type, description") .order("event_date", { ascending: false }) .order("created_at", { ascending: false }) .limit(10), ]); const fte = (fteRowsRes.data ?? []).reduce((sum, row) => sum + Number(row.weekly_hours), 0) / 38.5; const headcountByDivision = new Map(); for (const row of headcountRowsRes.data ?? []) { if (!row.division_id) continue; headcountByDivision.set(row.division_id, (headcountByDivision.get(row.division_id) ?? 0) + 1); } const divisionBars = (divisionsRes.data ?? []) .map((d) => ({ name: d.name, count: headcountByDivision.get(d.id) ?? 0 })) .sort((a, b) => b.count - a.count); const maxDivisionCount = Math.max(1, ...divisionBars.map((d) => d.count)); type UpcomingItem = { id: string; label: string; date: string; kind: keyof typeof KIND_LABEL }; const upcoming: UpcomingItem[] = [ ...(upcomingHiresRes.data ?? []).map((e) => ({ id: e.id, label: `${e.first_name} ${e.last_name}`, date: e.entry_date, kind: "hire" as const, })), ...(upcomingExitsRes.data ?? []).map((e) => ({ id: e.id, label: `${e.first_name} ${e.last_name}`, date: e.exit_date!, kind: "exit" as const, })), ...(upcomingReturnsRes.data ?? []).map((e) => ({ id: e.id, label: `${e.first_name} ${e.last_name}`, date: e.karenz_return_date!, kind: "return" as const, })), ] .sort((a, b) => a.date.localeCompare(b.date)) .slice(0, 8); const historyEmployeeIds = Array.from(new Set((historyRes.data ?? []).map((h) => h.employee_id))); const historyEmployeesRes = historyEmployeeIds.length ? await supabase.from("employees").select("id, first_name, last_name").in("id", historyEmployeeIds) : { data: [] as { id: string; first_name: string; last_name: string }[] }; const employeeNameById = new Map((historyEmployeesRes.data ?? []).map((e) => [e.id, `${e.first_name} ${e.last_name}`])); const kpis = [ { label: "Aktive Mitarbeiter:innen", value: activeCountRes.count ?? 0, tone: "default" }, { label: "FTE", value: fte.toFixed(1), tone: "default" }, { label: "Eintritte (Jahr)", value: hiresYtdRes.count ?? 0, tone: "success" }, { label: "Austritte (Jahr)", value: exitsYtdRes.count ?? 0, tone: "danger" }, { label: "In Karenz", value: karenzCountRes.count ?? 0, tone: "warning" }, { label: "Offene Positionen", value: openPositionsRes.count ?? 0, tone: "brand" }, ]; return (
{drafts && drafts.length > 0 && }
{kpis.map((kpi) => (
{kpi.label}
{kpi.value}
))}

Headcount nach Bereich

{divisionBars.map((d) => (
{d.name} {d.count}
))} {divisionBars.length === 0 &&

Keine Daten vorhanden.

}

Anstehend (60 Tage)

    {upcoming.map((item) => (
  • {item.label} {KIND_LABEL[item.kind]} {fmtDate(item.date)}
  • ))} {upcoming.length === 0 &&

    Keine anstehenden Ereignisse.

    }

Letzte Aktivitäten

    {(historyRes.data ?? []).map((h) => (
  • {employeeNameById.get(h.employee_id) ?? "Unbekannt"} {h.event_type}

    {h.description}

  • ))} {(historyRes.data ?? []).length === 0 &&

    Keine Aktivitäten vorhanden.

    }
); }