diff --git a/actions/employees.ts b/actions/employees.ts new file mode 100644 index 0000000..1076fac --- /dev/null +++ b/actions/employees.ts @@ -0,0 +1,111 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { createClient } from "@/lib/supabase/server"; +import type { Database } from "@/lib/supabase/types"; + +type ActionResult = { success: boolean; error?: string }; +type MutationFn = keyof Database["public"]["Functions"]; + +async function callRpc(fn: MutationFn, payload: Record, revalidate: string[]): Promise { + const supabase = await createClient(); + const { error } = await supabase.rpc(fn, { payload }); + if (error) return { success: false, error: error.message }; + for (const path of revalidate) revalidatePath(path); + return { success: true }; +} + +export async function hireEmployee(payload: { + first_name: string; + last_name: string; + gender: "m" | "w"; + birth_date: string; + sv_nummer?: string; + phone?: string; + position_id?: string; + team_id?: string; + job_title?: string; + location_id: string; + entry_date: string; + contract_type?: "unbefristet" | "befristet"; + contract_end_date?: string; + employment_type?: "Vollzeit" | "Teilzeit"; + weekly_hours?: number; + monthly_salary_gross: number; + paygrade?: "A" | "B" | "C" | "D" | "E" | "F"; + source: "Intern" | "Extern"; +}): Promise { + const supabase = await createClient(); + const { data, error } = await supabase.rpc("hire_employee", { payload }); + if (error) return { success: false, error: error.message }; + revalidatePath("/employees"); + revalidatePath("/"); + revalidatePath("/positions"); + return { success: true, employeeId: data as string }; +} + +export async function terminateEmployee(payload: { + employee_id: string; + exit_date: string; + exit_reason: string; + note?: string; +}): Promise { + return callRpc("terminate_employee", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]); +} + +export async function transferEmployee(payload: { + employee_id: string; + effective_date: string; + new_team_id: string; + new_title?: string; +}): Promise { + return callRpc("transfer_employee", payload, [`/employees/${payload.employee_id}`, "/employees"]); +} + +export async function promoteEmployee(payload: { + employee_id: string; + effective_date: string; + new_title: string; + new_salary: number; + new_paygrade?: "A" | "B" | "C" | "D" | "E" | "F"; +}): Promise { + return callRpc("promote_employee", payload, [`/employees/${payload.employee_id}`, "/employees"]); +} + +export async function startKarenz(payload: { + employee_id: string; + karenz_start_date: string; + planned_return_date: string; + note?: string; +}): Promise { + return callRpc("start_karenz", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]); +} + +export async function adjustKarenzReturn(payload: { + employee_id: string; + new_return_date: string; + note?: string; +}): Promise { + return callRpc("adjust_karenz_return", payload, [`/employees/${payload.employee_id}`]); +} + +export async function recordKarenzReturn(payload: { + employee_id: string; + return_date: string; + employment_mode: "unverändert" | "Vollzeit" | "Teilzeit"; + weekly_hours?: number; +}): Promise { + return callRpc("record_karenz_return", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]); +} + +export async function changeEmployeeData(payload: { + employee_id: string; + person: Record; + contract: Record; +}): Promise { + return callRpc("change_employee_data", payload, [`/employees/${payload.employee_id}`, "/employees"]); +} + +export async function rehireEmployee(payload: { employee_id: string; rehire_date: string }): Promise { + return callRpc("rehire_employee", payload, [`/employees/${payload.employee_id}`, "/employees", "/"]); +} diff --git a/actions/positions.ts b/actions/positions.ts new file mode 100644 index 0000000..7267801 --- /dev/null +++ b/actions/positions.ts @@ -0,0 +1,36 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { createClient } from "@/lib/supabase/server"; + +type ActionResult = { success: boolean; error?: string }; + +export async function createPosition(payload: { + title: string; + superior_employee_id: string; + is_lead: boolean; + team_id?: string; +}): Promise { + const supabase = await createClient(); + const { error } = await supabase.rpc("create_position", { payload }); + if (error) return { success: false, error: error.message }; + revalidatePath("/positions"); + revalidatePath("/orgchart"); + revalidatePath("/"); + return { success: true }; +} + +export async function staffPositionInternally(payload: { + position_id: string; + employee_id: string; +}): Promise { + const supabase = await createClient(); + const { error } = await supabase.rpc("staff_position_internally", { payload }); + if (error) return { success: false, error: error.message }; + revalidatePath("/positions"); + revalidatePath("/orgchart"); + revalidatePath("/employees"); + revalidatePath(`/employees/${payload.employee_id}`); + revalidatePath("/"); + return { success: true }; +} diff --git a/actions/reorg.ts b/actions/reorg.ts new file mode 100644 index 0000000..cfc6b84 --- /dev/null +++ b/actions/reorg.ts @@ -0,0 +1,37 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { createClient } from "@/lib/supabase/server"; + +type ActionResult = { success: boolean; error?: string }; + +export type ReorgMovePayload = { + kind: "emp" | "team" | "abt" | "dept"; + label: string; + employee_ids: string[]; + target_team_id: string; +}; + +export async function applyReorg(payload: { + name: string; + effective_date: string; + moves: ReorgMovePayload[]; +}): Promise { + const supabase = await createClient(); + const { data, error } = await supabase.rpc("apply_reorg", { payload }); + if (error) return { success: false, error: error.message }; + revalidatePath("/orgchart"); + revalidatePath("/employees"); + revalidatePath("/"); + return { success: true, scenarioId: data as string }; +} + +export async function undoReorg(payload: { scenario_id: string }): Promise { + const supabase = await createClient(); + const { error } = await supabase.rpc("undo_reorg", { payload }); + if (error) return { success: false, error: error.message }; + revalidatePath("/orgchart"); + revalidatePath("/employees"); + revalidatePath("/"); + return { success: true }; +} diff --git a/app/(app)/employees/[id]/page.tsx b/app/(app)/employees/[id]/page.tsx new file mode 100644 index 0000000..0e7bd22 --- /dev/null +++ b/app/(app)/employees/[id]/page.tsx @@ -0,0 +1,52 @@ +import { notFound } from "next/navigation"; +import { EmployeeDetail } from "@/components/employees/EmployeeDetail"; +import { createClient } from "@/lib/supabase/server"; + +type PageProps = { params: Promise<{ id: string }> }; + +export default async function EmployeeDetailPage({ params }: PageProps) { + const { id } = await params; + const supabase = await createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + const { data: profile } = await supabase.from("profiles").select("role").eq("id", user!.id).single(); + const canEdit = profile?.role === "hr_admin"; + + const { data: employee } = await supabase.from("employees_directory").select("*").eq("id", id).single(); + if (!employee) notFound(); + + const [{ data: manager }, { data: directReports }, { data: history }, { data: divisions }, { data: departments }, { data: teams }, { data: locations }, { data: openPositions }] = + await Promise.all([ + employee.manager_id + ? supabase.from("employees_directory").select("id, first_name, last_name, job_title").eq("id", employee.manager_id).single() + : Promise.resolve({ data: null }), + supabase + .from("employees_directory") + .select("id, first_name, last_name, job_title, status") + .eq("manager_id", id) + .order("last_name"), + supabase.from("employee_history").select("*").eq("employee_id", id).order("event_date", { ascending: false }).order("created_at", { ascending: false }), + supabase.from("divisions").select("*").order("name"), + supabase.from("departments").select("*"), + supabase.from("teams").select("*"), + supabase.from("locations").select("*").order("name"), + supabase.from("positions").select("id, position_number, title, team_id, is_lead").eq("status", "open"), + ]); + + return ( + + ); +} diff --git a/app/(app)/employees/page.tsx b/app/(app)/employees/page.tsx new file mode 100644 index 0000000..73dcbad --- /dev/null +++ b/app/(app)/employees/page.tsx @@ -0,0 +1,142 @@ +import Link from "next/link"; +import { Suspense } from "react"; +import { EmployeeFilters } from "@/components/employees/EmployeeFilters"; +import { Avatar } from "@/components/ui/Avatar"; +import { StatusChip } from "@/components/ui/StatusChip"; +import { fmtDate } from "@/lib/format"; +import { breadcrumbFor, loadOrgMaps } from "@/lib/org"; +import { createClient } from "@/lib/supabase/server"; +import type { EmploymentStatus } from "@/lib/supabase/types"; + +const PAGE_SIZE = 15; + +type SearchParams = { q?: string; division?: string; status?: string; location?: string; page?: string }; + +type EmployeesPageProps = { + searchParams: Promise; +}; + +function pageHref(params: SearchParams, page: number): string { + const sp = new URLSearchParams(); + if (params.q) sp.set("q", params.q); + if (params.division) sp.set("division", params.division); + if (params.status) sp.set("status", params.status); + if (params.location) sp.set("location", params.location); + sp.set("page", String(page)); + return `/employees?${sp.toString()}`; +} + +export default async function EmployeesPage({ searchParams }: EmployeesPageProps) { + const params = await searchParams; + const supabase = await createClient(); + const orgMaps = await loadOrgMaps(supabase); + + const page = Math.max(1, Number(params.page ?? "1") || 1); + const from = (page - 1) * PAGE_SIZE; + const to = from + PAGE_SIZE - 1; + + let query = supabase + .from("employees_directory") + .select( + "id, first_name, last_name, personnel_number, job_title, team_id, division_id, location_id, entry_date, employment_type, weekly_hours, status", + { count: "exact" } + ) + .order("last_name", { ascending: true }) + .range(from, to); + + if (params.q) { + const q = params.q.trim(); + if (/^\d+$/.test(q)) { + query = query.eq("personnel_number", Number(q)); + } else { + query = query.or(`first_name.ilike.%${q}%,last_name.ilike.%${q}%,job_title.ilike.%${q}%`); + } + } + if (params.division) query = query.eq("division_id", params.division); + if (params.status) query = query.eq("status", params.status as EmploymentStatus); + if (params.location) query = query.eq("location_id", params.location); + + const { data: employeesData, count } = await query; + const employees = employeesData ?? []; + const totalPages = Math.max(1, Math.ceil((count ?? 0) / PAGE_SIZE)); + + return ( +
+ + + +

{count ?? 0} Mitarbeiter:innen gefunden

+ +
+ + + + + + + + + + + + + + {employees.map((e) => { + const { division, team } = breadcrumbFor(orgMaps, e.division_id, e.team_id); + const location = e.location_id ? orgMaps.locations.get(e.location_id) : undefined; + return ( + + + + + + + + + + ); + })} + {employees.length === 0 && ( + + + + )} + +
Mitarbeiter:inPers.-Nr.Bereich/TeamStandortEintrittBeschäftigungStatus
+ + +
+
+ {e.first_name} {e.last_name} +
+
{e.job_title}
+
+ +
{e.personnel_number} +
{division?.name ?? "–"}
+
{team?.name ?? "–"}
+
{location?.name ?? "–"}{fmtDate(e.entry_date)} + {e.employment_type} · {e.weekly_hours}h + + +
+ Keine Mitarbeiter:innen gefunden. +
+
+ + {totalPages > 1 && ( +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( + + {p} + + ))} +
+ )} +
+ ); +} diff --git a/components/employees/EmployeeDetail.tsx b/components/employees/EmployeeDetail.tsx new file mode 100644 index 0000000..d497435 --- /dev/null +++ b/components/employees/EmployeeDetail.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { ArrowRightLeft, Clock, Pencil, RotateCcw, TrendingUp, XCircle } from "lucide-react"; +import { useState } from "react"; +import { Avatar } from "@/components/ui/Avatar"; +import { StatusChip } from "@/components/ui/StatusChip"; +import { tenure } from "@/lib/format"; +import type { Database } from "@/lib/supabase/types"; +import { DatenAendernPanel } from "./panels/DatenAendernPanel"; +import { KarenzPanel } from "./panels/KarenzPanel"; +import { PromotePanel } from "./panels/PromotePanel"; +import { RehirePanel } from "./panels/RehirePanel"; +import { TerminatePanel } from "./panels/TerminatePanel"; +import { TransferPanel } from "./panels/TransferPanel"; +import { HistorieTab } from "./tabs/HistorieTab"; +import { OrganisationTab } from "./tabs/OrganisationTab"; +import { StammdatenTab } from "./tabs/StammdatenTab"; +import { VertragTab } from "./tabs/VertragTab"; + +type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; +type Division = Database["public"]["Tables"]["divisions"]["Row"]; +type Department = Database["public"]["Tables"]["departments"]["Row"]; +type Team = Database["public"]["Tables"]["teams"]["Row"]; +type Location = Database["public"]["Tables"]["locations"]["Row"]; +type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"]; +type MiniEmployee = { id: string; first_name: string; last_name: string; job_title: string; status?: string }; +type OpenPosition = { id: string; position_number: string; title: string; team_id: string; is_lead: boolean }; + +type EmployeeDetailProps = { + employee: EmployeeRow; + manager: MiniEmployee | null; + directReports: MiniEmployee[]; + history: HistoryRow[]; + divisions: Division[]; + departments: Department[]; + teams: Team[]; + locations: Location[]; + openPositions: OpenPosition[]; + canEdit: boolean; +}; + +type PanelType = "transfer" | "promote" | "karenz" | "daten" | "terminate" | "rehire" | null; +const TABS = ["Stammdaten", "Vertrag & Gehalt", "Organisation", "Historie"] as const; + +export function EmployeeDetail(props: EmployeeDetailProps) { + const { employee, manager, directReports, history, divisions, departments, teams, locations, canEdit } = props; + const [tab, setTab] = useState<(typeof TABS)[number]>("Stammdaten"); + const [panel, setPanel] = useState(null); + + const division = divisions.find((d) => d.id === employee.division_id); + const team = employee.team_id ? teams.find((t) => t.id === employee.team_id) : undefined; + const department = team ? departments.find((d) => d.id === team.department_id) : undefined; + const breadcrumb = [division?.name, department?.name, team?.name].filter(Boolean).join(" › ") || "–"; + const location = locations.find((l) => l.id === employee.location_id); + + const isActive = employee.status === "Aktiv" || employee.status === "Karenz"; + + return ( +
+
+
+
+ +
+
+

+ {employee.first_name} {employee.last_name} +

+ +
+

{employee.job_title}

+

{breadcrumb}

+

+ Pers.-Nr. {employee.personnel_number} + {employee.status !== "Geplant" && <> · Zugehörigkeit: {tenure(employee.entry_date, employee.exit_date)}} +

+
+
+ + {canEdit && ( +
+ {isActive && ( + <> + setPanel("transfer")} /> + setPanel("promote")} /> + setPanel("karenz")} /> + setPanel("daten")} /> + + + )} + {employee.status === "Ausgetreten" && ( + + )} +
+ )} +
+
+ +
+ {TABS.map((t) => ( + + ))} +
+ +
+ {tab === "Stammdaten" && } + {tab === "Vertrag & Gehalt" && } + {tab === "Organisation" && } + {tab === "Historie" && } +
+ + {canEdit && ( + <> + setPanel(null)} + employee={employee} + divisions={divisions} + departments={departments} + teams={teams} + currentTeamId={employee.team_id} + /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} directReportCount={directReports.length} /> + setPanel(null)} employee={employee} /> + + )} +
+ ); +} + +function ActionButton({ icon: Icon, label, onClick }: { icon: typeof ArrowRightLeft; label: string; onClick: () => void }) { + return ( + + ); +} diff --git a/components/employees/EmployeeFilters.tsx b/components/employees/EmployeeFilters.tsx new file mode 100644 index 0000000..17ff347 --- /dev/null +++ b/components/employees/EmployeeFilters.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { Search } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +type EmployeeFiltersProps = { + divisions: { id: string; name: string }[]; + locations: { id: string; name: string }[]; +}; + +const STATUS_OPTIONS = ["Aktiv", "Karenz", "Geplant", "Ausgetreten"] as const; + +export function EmployeeFilters({ divisions, locations }: EmployeeFiltersProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [q, setQ] = useState(searchParams.get("q") ?? ""); + + useEffect(() => { + const handle = setTimeout(() => { + const current = new URLSearchParams(searchParams.toString()); + if (q) current.set("q", q); + else current.delete("q"); + current.delete("page"); + const next = current.toString(); + if (next !== searchParams.toString()) router.push(`${pathname}?${next}`); + }, 300); + return () => clearTimeout(handle); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [q]); + + function updateParam(key: string, value: string) { + const params = new URLSearchParams(searchParams.toString()); + if (value) params.set(key, value); + else params.delete(key); + params.delete("page"); + router.push(`${pathname}?${params.toString()}`); + } + + return ( +
+
+ + setQ(e.target.value)} + placeholder="Name, Pers.-Nr., Titel…" + className="w-full text-sm text-ink outline-none placeholder:text-ink-muted" + /> +
+ + + +
+ ); +} diff --git a/components/employees/panels/DatenAendernPanel.tsx b/components/employees/panels/DatenAendernPanel.tsx new file mode 100644 index 0000000..0176683 --- /dev/null +++ b/components/employees/panels/DatenAendernPanel.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { changeEmployeeData } from "@/actions/employees"; +import { SlideOver } from "@/components/ui/SlideOver"; +import { useToast } from "@/components/ui/Toast"; +import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/supabase/types"; + +type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; + +const NATIONALITIES = ["Österreich", "Deutschland", "Tschechien", "Slowenien", "Türkei", "Serbien", "Kroatien", "Bosnien", "Ungarn", "Andere"]; +const ADDRESS_COUNTRIES = ["Österreich", "Deutschland", "Tschechien", "Slowenien", "Andere"]; + +export function DatenAendernPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) { + const { showToast } = useToast(); + const router = useRouter(); + const [pending, setPending] = useState(false); + + const [firstName, setFirstName] = useState(employee.first_name); + const [lastName, setLastName] = useState(employee.last_name); + const [gender, setGender] = useState(employee.gender); + const [birthDate, setBirthDate] = useState(employee.birth_date); + const [svNummer, setSvNummer] = useState(employee.sv_nummer ?? ""); + const [nationality, setNationality] = useState(employee.nationality); + const [address, setAddress] = useState(employee.address ?? ""); + const [addressCountry, setAddressCountry] = useState(employee.address_country ?? "Österreich"); + const [email, setEmail] = useState(employee.email); + const [phone, setPhone] = useState(employee.phone ?? ""); + + const [employmentType, setEmploymentType] = useState(employee.employment_type); + const [weeklyHours, setWeeklyHours] = useState(String(employee.weekly_hours)); + const [contractType, setContractType] = useState(employee.contract_type); + const [contractEndDate, setContractEndDate] = useState(employee.contract_end_date ?? ""); + + function handleEmploymentTypeChange(value: EmploymentType) { + setEmploymentType(value); + if (value === "Vollzeit") setWeeklyHours("38.5"); + } + + async function handleSubmit() { + if (employmentType === "Teilzeit" && (Number(weeklyHours) <= 0 || Number(weeklyHours) >= 38.5)) { + showToast("Wochenstunden bei Teilzeit müssen zwischen 0 und 38,5 liegen.", "error"); + return; + } + if (contractType === "befristet" && !contractEndDate) { + showToast("Bei befristetem Vertrag ist ein Enddatum erforderlich.", "error"); + return; + } + setPending(true); + const result = await changeEmployeeData({ + employee_id: employee.id, + person: { + first_name: firstName, + last_name: lastName, + gender, + birth_date: birthDate, + sv_nummer: svNummer, + nationality, + address, + address_country: addressCountry, + email, + phone, + }, + contract: { + employment_type: employmentType, + weekly_hours: Number(weeklyHours), + contract_type: contractType, + contract_end_date: contractType === "befristet" ? contractEndDate : "", + }, + }); + setPending(false); + if (result.success) { + showToast("Daten aktualisiert."); + router.refresh(); + onClose(); + } else { + showToast(result.error ?? "Fehler beim Speichern.", "error"); + } + } + + return ( + + + + + } + > +
+
+

Person

+
+
+
+ + setFirstName(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ + setLastName(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+
+
+ + +
+
+ + setBirthDate(e.target.value)} + className="w-full rounded border border-border px-3 py-2 text-sm" + /> +
+
+
+ + setSvNummer(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ + +
+
+
+ + setAddress(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ + +
+
+
+ + setEmail(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ + setPhone(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+
+ +
+

Vertrag

+
+
+ + +
+
+ + setWeeklyHours(e.target.value)} + className="w-full rounded border border-border px-3 py-2 text-sm disabled:bg-surface disabled:text-ink-muted" + /> +
+
+ + +
+ {contractType === "befristet" && ( +
+ + setContractEndDate(e.target.value)} + className="w-full rounded border border-border px-3 py-2 text-sm" + /> +
+ )} +
+
+
+
+ ); +} diff --git a/components/employees/panels/KarenzPanel.tsx b/components/employees/panels/KarenzPanel.tsx new file mode 100644 index 0000000..dfc16f8 --- /dev/null +++ b/components/employees/panels/KarenzPanel.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { adjustKarenzReturn, recordKarenzReturn, startKarenz } from "@/actions/employees"; +import { SegmentedControl } from "@/components/ui/SegmentedControl"; +import { SlideOver } from "@/components/ui/SlideOver"; +import { useToast } from "@/components/ui/Toast"; +import { fmtDate } from "@/lib/format"; +import type { Database } from "@/lib/supabase/types"; + +type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; +type Mode = "adjust" | "return"; +type EmploymentMode = "unverändert" | "Vollzeit" | "Teilzeit"; + +export function KarenzPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) { + const { showToast } = useToast(); + const router = useRouter(); + const isOnKarenz = employee.status === "Karenz"; + const [mode, setMode] = useState("adjust"); + const [pending, setPending] = useState(false); + + const [karenzStart, setKarenzStart] = useState(""); + const [plannedReturn, setPlannedReturn] = useState(""); + const [startNote, setStartNote] = useState(""); + + const [newReturnDate, setNewReturnDate] = useState(employee.karenz_return_date ?? ""); + const [adjustNote, setAdjustNote] = useState(""); + + const [returnDate, setReturnDate] = useState(""); + const [employmentMode, setEmploymentMode] = useState("unverändert"); + const [weeklyHours, setWeeklyHours] = useState("20"); + + const diffDays = + isOnKarenz && employee.karenz_return_date && newReturnDate + ? Math.round((new Date(newReturnDate).getTime() - new Date(employee.karenz_return_date).getTime()) / 86400000) + : 0; + + async function handleStart() { + if (!karenzStart || !plannedReturn) { + showToast("Bitte beide Daten angeben.", "error"); + return; + } + setPending(true); + const result = await startKarenz({ employee_id: employee.id, karenz_start_date: karenzStart, planned_return_date: plannedReturn, note: startNote }); + setPending(false); + if (result.success) { + showToast("Karenz erfasst."); + router.refresh(); + onClose(); + } else { + showToast(result.error ?? "Fehler.", "error"); + } + } + + async function handleAdjust() { + if (!newReturnDate || newReturnDate === employee.karenz_return_date) { + showToast("Bitte ein neues Rückkehrdatum angeben.", "error"); + return; + } + if (new Date(newReturnDate) <= new Date()) { + showToast("Rückkehrdatum muss in der Zukunft liegen.", "error"); + return; + } + setPending(true); + const result = await adjustKarenzReturn({ employee_id: employee.id, new_return_date: newReturnDate, note: adjustNote }); + setPending(false); + if (result.success) { + showToast("Rückkehrdatum angepasst."); + router.refresh(); + onClose(); + } else { + showToast(result.error ?? "Fehler.", "error"); + } + } + + async function handleReturn() { + if (!returnDate) { + showToast("Bitte Rückkehrdatum angeben.", "error"); + return; + } + if (employmentMode === "Teilzeit" && (Number(weeklyHours) <= 0 || Number(weeklyHours) >= 38.5)) { + showToast("Wochenstunden müssen zwischen 0 und 38,5 liegen.", "error"); + return; + } + setPending(true); + const result = await recordKarenzReturn({ + employee_id: employee.id, + return_date: returnDate, + employment_mode: employmentMode, + weekly_hours: employmentMode === "Teilzeit" ? Number(weeklyHours) : undefined, + }); + setPending(false); + if (result.success) { + showToast("Wiedereintritt erfasst."); + router.refresh(); + onClose(); + } else { + showToast(result.error ?? "Fehler.", "error"); + } + } + + return ( + + + {!isOnKarenz && ( + + )} + {isOnKarenz && mode === "adjust" && ( + + )} + {isOnKarenz && mode === "return" && ( + + )} + + } + > + {!isOnKarenz && ( +
+
+ + setKarenzStart(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ + setPlannedReturn(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> +
+
+ +