Files
alpenwerk-hr/components/hire/HireWizard.tsx

179 lines
6.4 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { hireEmployee } from "@/actions/employees";
import { deleteHireDraft, saveHireDraft } from "@/actions/hireDrafts";
import { Modal } from "@/components/ui/Modal";
import { useToast } from "@/components/ui/Toast";
import type { OpenPositionResolved } from "@/lib/positions";
import { StepPerson } from "./StepPerson";
import { StepPosition } from "./StepPosition";
import { StepSummary } from "./StepSummary";
import { StepVertrag } from "./StepVertrag";
import { EMPTY_HIRE_DRAFT, type HireDraftData } from "./types";
const STEP_LABELS = ["Person", "Position", "Vertrag", "Zusammenfassung"];
type HireWizardProps = {
open: boolean;
onClose: () => void;
openPositions: OpenPositionResolved[];
locations: { id: string; name: string; country: string }[];
resumeDraft: { id: string; step: number; payload: Record<string, unknown> } | null;
initialPositionId?: string;
};
export function HireWizard({ open, onClose, openPositions, locations, resumeDraft, initialPositionId }: HireWizardProps) {
const { showToast } = useToast();
const router = useRouter();
const [step, setStep] = useState(0);
const [draft, setDraft] = useState<HireDraftData>(EMPTY_HIRE_DRAFT);
const [draftId, setDraftId] = useState<string | undefined>(undefined);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (!open) return;
if (resumeDraft) {
setDraft({ ...EMPTY_HIRE_DRAFT, ...(resumeDraft.payload as Partial<HireDraftData>) });
setStep(resumeDraft.step);
setDraftId(resumeDraft.id);
} else {
setDraft({ ...EMPTY_HIRE_DRAFT, positionId: initialPositionId ?? "" });
setStep(0);
setDraftId(undefined);
}
}, [open, resumeDraft, initialPositionId]);
const selectedPosition = useMemo(
() => openPositions.find((p) => p.id === draft.positionId) ?? null,
[openPositions, draft.positionId]
);
function update(patch: Partial<HireDraftData>) {
setDraft((prev) => ({ ...prev, ...patch }));
}
const stepValid = [
Boolean(draft.firstName && draft.lastName && draft.birthDate && draft.locationId),
Boolean(draft.positionId && draft.besetzung),
Boolean(draft.entryDate && draft.salary),
true,
][step];
async function handleSaveDraft() {
const result = await saveHireDraft({ id: draftId, step, data: draft });
if (result.success) {
showToast("Entwurf gespeichert.");
router.refresh();
onClose();
} else {
showToast(result.error ?? "Fehler beim Speichern.", "error");
}
}
async function handleSubmit() {
if (!selectedPosition || !draft.besetzung) return;
setSubmitting(true);
const result = await hireEmployee({
first_name: draft.firstName,
last_name: draft.lastName,
gender: draft.gender,
birth_date: draft.birthDate,
sv_nummer: draft.svNummer || undefined,
phone: draft.phone || undefined,
position_id: draft.positionId,
location_id: draft.locationId,
entry_date: draft.entryDate,
contract_type: draft.contractType,
contract_end_date: draft.contractType === "befristet" ? draft.contractEndDate : undefined,
employment_type: draft.employmentType,
weekly_hours: Number(draft.weeklyHours),
monthly_salary_gross: Number(draft.salary),
paygrade: draft.paygrade,
source: draft.besetzung,
});
setSubmitting(false);
if (result.success) {
showToast(`${draft.firstName} ${draft.lastName} wurde eingestellt.`);
if (draftId) await deleteHireDraft(draftId);
router.refresh();
onClose();
} else {
showToast(result.error ?? "Fehler beim Anlegen.", "error");
}
}
return (
<Modal
open={open}
onClose={onClose}
title="Neueinstellung"
widthClassName="max-w-2xl"
footer={
<div className="flex w-full items-center justify-between">
<div className="flex gap-2">
<button onClick={onClose} className="rounded px-4 py-2 text-sm font-semibold text-ink-body hover:bg-surface">
Abbrechen
</button>
<button onClick={handleSaveDraft} className="rounded px-4 py-2 text-sm font-semibold text-ink-body hover:bg-surface">
Als Entwurf speichern
</button>
</div>
<div className="flex gap-2">
{step > 0 && (
<button
onClick={() => setStep((s) => s - 1)}
className="rounded border border-border px-4 py-2 text-sm font-semibold text-ink-body hover:bg-surface"
>
Zurück
</button>
)}
{step < 3 && (
<button
onClick={() => setStep((s) => s + 1)}
disabled={!stepValid}
className="rounded bg-brand-500 px-4 py-2 text-sm font-semibold text-white disabled:opacity-40"
>
Weiter
</button>
)}
{step === 3 && (
<button
onClick={handleSubmit}
disabled={submitting}
className="rounded bg-brand-500 px-4 py-2 text-sm font-semibold text-white disabled:opacity-50"
>
Anlegen
</button>
)}
</div>
</div>
}
>
<div className="mb-6 flex items-center justify-center gap-3">
{STEP_LABELS.map((label, i) => (
<button
key={label}
type="button"
onClick={() => i < step && setStep(i)}
className={`flex items-center gap-2 text-xs font-semibold ${
i === step ? "text-brand-700" : i < step ? "text-ink-body" : "text-ink-muted"
}`}
>
<span className={`flex h-6 w-6 items-center justify-center rounded-full ${i <= step ? "bg-brand-500 text-white" : "bg-surface text-ink-muted"}`}>
{i + 1}
</span>
{label}
</button>
))}
</div>
{step === 0 && <StepPerson draft={draft} update={update} locations={locations} />}
{step === 1 && <StepPosition draft={draft} update={update} openPositions={openPositions} />}
{step === 2 && <StepVertrag draft={draft} update={update} />}
{step === 3 && <StepSummary draft={draft} selectedPosition={selectedPosition} locations={locations} />}
</Modal>
);
}