- components/hire/: 4-step Hire Wizard (Person/Position/Vertrag/ Zusammenfassung) matching sec4.4, with a HireWizardProvider context so it can be opened both from the global "+ Neueinstellung" button and from a "Fortsetzen" link on a saved draft. - actions/hireDrafts.ts: save/delete hire_drafts (owner-scoped RLS already in place from Phase 1). Dashboard now shows the "Entwuerfe" card the Phase 1 plan deferred, since the wizard it depends on now exists. - lib/positions.ts: shared open-positions loader (position number, org breadcrumb, resolved manager name) used by both the wizard and (later) the Positions page. Two real bugs found via live testing and fixed in supabase/functions.sql: 1. hire_employee/rehire_employee: a two-branch CASE returning bare string literals defaults to `text`, not the target enum, so `status = case when ... then 'Geplant' else 'Aktiv' end` failed against the employment_status column. Fixed with an explicit ::employment_status cast on the whole CASE expression. 2. Postgres precedence gotcha: ->> and || sit at the *same* precedence tier and left-associate, so `payload->>'first_name' || ' ' || payload->>'last_name'` does not group the way it reads - it tries to apply ->> to an intermediate text value and fails with "operator does not exist: text ->> unknown". Fixed by parenthesizing every ->>'...' expression that participates in a || chain. Also fixed: hire_employee referenced v_position.title outside the branch that assigns v_position, raising "record not assigned" whenever a hire wasn't tied to a position_id; extracted a v_job_title variable instead. Verified live end-to-end: wizard search -> select position -> submit creates the employee, closes the position, and writes matching employee_history + audit_log rows atomically.
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { fmtDate, fmtEUR } from "@/lib/format";
|
||
import type { OpenPositionResolved } from "@/lib/positions";
|
||
import type { HireDraftData } from "./types";
|
||
|
||
const PAYGRADE_LABELS: Record<string, string> = {
|
||
A: "A – Einstieg",
|
||
B: "B – Qualifiziert",
|
||
C: "C – Erfahren",
|
||
D: "D – Spezialist:in",
|
||
E: "E – Teamleitung",
|
||
F: "F – Bereichsleitung / GF",
|
||
};
|
||
|
||
type StepSummaryProps = {
|
||
draft: HireDraftData;
|
||
selectedPosition: OpenPositionResolved | null;
|
||
locations: { id: string; name: string; country: string }[];
|
||
};
|
||
|
||
export function StepSummary({ draft, selectedPosition, locations }: StepSummaryProps) {
|
||
const location = locations.find((l) => l.id === draft.locationId);
|
||
const rows: [string, string][] = [
|
||
["Name", `${draft.firstName} ${draft.lastName}`],
|
||
["Geschlecht", draft.gender === "m" ? "männlich" : "weiblich"],
|
||
["Geburtsdatum", fmtDate(draft.birthDate)],
|
||
["SV-Nummer", draft.svNummer || "–"],
|
||
["E-Mail (privat)", draft.email || "–"],
|
||
["Telefon", draft.phone || "–"],
|
||
["Standort", location ? `${location.name} (${location.country})` : "–"],
|
||
["Position", selectedPosition ? `${selectedPosition.title} (${selectedPosition.position_number})` : "–"],
|
||
["Organisationseinheit", selectedPosition?.orgLabel ?? "–"],
|
||
["Führungskraft", selectedPosition?.managerName ?? "–"],
|
||
["Besetzung", draft.besetzung],
|
||
["Eintrittsdatum", fmtDate(draft.entryDate)],
|
||
["Vertragsart", draft.contractType === "befristet" ? `befristet bis ${fmtDate(draft.contractEndDate)}` : "unbefristet"],
|
||
["Beschäftigungsausmaß", `${draft.employmentType} (${draft.weeklyHours} h)`],
|
||
["Bruttogehalt (14x/Jahr)", fmtEUR(Number(draft.salary))],
|
||
["Paygrade", PAYGRADE_LABELS[draft.paygrade]],
|
||
];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||
{rows.map(([label, value]) => (
|
||
<div key={label}>
|
||
<dt className="text-xs font-semibold uppercase tracking-wide text-ink-muted">{label}</dt>
|
||
<dd className="mt-0.5 text-sm text-ink">{value}</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
<p className="rounded bg-info-bg px-3 py-2 text-sm text-info-text">
|
||
Personalnummer und Firmen-E-Mail-Adresse werden automatisch vergeben.
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|