Files
alpenwerk-hr/components/hire/StepVertrag.tsx
Maximilian Stubhan f91a69147e Phase 3: Hire wizard, draft resume, and two real SQL bugfixes
- 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.
2026-07-13 22:37:59 +02:00

98 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { PaygradeType } from "@/lib/supabase/types";
import type { HireDraftData } from "./types";
const PAYGRADES: { value: PaygradeType; label: string; description: string }[] = [
{ value: "A", label: "A Einstieg", description: "Berufseinsteiger:innen ohne einschlägige Erfahrung" },
{ value: "B", label: "B Qualifiziert", description: "Fachkräfte mit abgeschlossener Ausbildung" },
{ value: "C", label: "C Erfahren", description: "Mehrjährige einschlägige Berufserfahrung" },
{ value: "D", label: "D Spezialist:in", description: "Vertiefte Fachexpertise" },
{ value: "E", label: "E Teamleitung", description: "Fachliche und disziplinäre Führung eines Teams" },
{ value: "F", label: "F Bereichsleitung / GF", description: "Führung eines Bereichs bzw. Geschäftsführung" },
];
export function StepVertrag({ draft, update }: { draft: HireDraftData; update: (patch: Partial<HireDraftData>) => void }) {
function handleEmploymentTypeChange(value: HireDraftData["employmentType"]) {
update({
employmentType: value,
weeklyHours: value === "Vollzeit" ? "38.5" : draft.weeklyHours === "38.5" ? "20" : draft.weeklyHours,
});
}
return (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Eintrittsdatum*</label>
<input type="date" value={draft.entryDate} onChange={(e) => update({ entryDate: e.target.value })} className="w-full rounded border border-border px-3 py-2 text-sm" />
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Vertragsart</label>
<select
value={draft.contractType}
onChange={(e) => update({ contractType: e.target.value as HireDraftData["contractType"] })}
className="w-full rounded border border-border px-3 py-2 text-sm"
>
<option value="unbefristet">unbefristet</option>
<option value="befristet">befristet</option>
</select>
</div>
</div>
{draft.contractType === "befristet" && (
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Befristet bis*</label>
<input
type="date"
value={draft.contractEndDate}
onChange={(e) => update({ contractEndDate: e.target.value })}
className="w-full rounded border border-border px-3 py-2 text-sm"
/>
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Beschäftigungsausmaß</label>
<select
value={draft.employmentType}
onChange={(e) => handleEmploymentTypeChange(e.target.value as HireDraftData["employmentType"])}
className="w-full rounded border border-border px-3 py-2 text-sm"
>
<option value="Vollzeit">Vollzeit</option>
<option value="Teilzeit">Teilzeit</option>
</select>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Wochenstunden</label>
<input
type="number"
step="0.5"
value={draft.weeklyHours}
disabled={draft.employmentType === "Vollzeit"}
onChange={(e) => update({ weeklyHours: e.target.value })}
className="w-full rounded border border-border px-3 py-2 text-sm disabled:bg-surface disabled:text-ink-muted"
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Bruttogehalt/Monat (14x)*</label>
<input type="number" value={draft.salary} onChange={(e) => update({ salary: e.target.value })} className="w-full rounded border border-border px-3 py-2 text-sm" />
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Paygrade*</label>
<select
value={draft.paygrade}
onChange={(e) => update({ paygrade: e.target.value as PaygradeType })}
className="w-full rounded border border-border px-3 py-2 text-sm"
>
{PAYGRADES.map((p) => (
<option key={p.value} value={p.value}>
{p.label}
</option>
))}
</select>
<p className="mt-1 text-xs text-ink-muted">{PAYGRADES.find((p) => p.value === draft.paygrade)?.description}</p>
</div>
<p className="text-xs text-ink-muted">Es gilt eine Probezeit von 1 Monat gemäß Kollektivvertrag.</p>
</div>
);
}