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.
This commit is contained in:
67
components/dashboard/DraftsCard.tsx
Normal file
67
components/dashboard/DraftsCard.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { deleteHireDraft } from "@/actions/hireDrafts";
|
||||
import { useHireWizard } from "@/components/hire/HireWizardContext";
|
||||
import { useToast } from "@/components/ui/Toast";
|
||||
import { fmtDate } from "@/lib/format";
|
||||
|
||||
type Draft = { id: string; step: number; payload: Record<string, unknown>; updated_at: string };
|
||||
|
||||
export function DraftsCard({ drafts }: { drafts: Draft[] }) {
|
||||
const { openWizard } = useHireWizard();
|
||||
const { showToast } = useToast();
|
||||
const router = useRouter();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
if (drafts.length === 0) return null;
|
||||
|
||||
function handleDelete(id: string) {
|
||||
startTransition(async () => {
|
||||
const result = await deleteHireDraft(id);
|
||||
if (result.success) {
|
||||
showToast("Entwurf gelöscht.");
|
||||
router.refresh();
|
||||
} else {
|
||||
showToast(result.error ?? "Fehler beim Löschen.", "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded border border-border bg-white p-4">
|
||||
<h2 className="mb-3 text-sm font-bold text-ink">Entwürfe – Neueinstellungen</h2>
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{drafts.map((d) => {
|
||||
const firstName = typeof d.payload.firstName === "string" ? d.payload.firstName : "";
|
||||
const lastName = typeof d.payload.lastName === "string" ? d.payload.lastName : "";
|
||||
const name = [firstName, lastName].filter(Boolean).join(" ") || "Ohne Namen";
|
||||
return (
|
||||
<li key={d.id} className="flex items-center justify-between py-2 text-sm">
|
||||
<div>
|
||||
<span className="font-semibold text-ink">{name}</span>
|
||||
<span className="ml-2 text-xs text-ink-muted">Gespeichert am {fmtDate(d.updated_at)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => openWizard(d.id)} className="text-xs font-semibold text-brand-700 hover:underline">
|
||||
Fortsetzen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(d.id)}
|
||||
disabled={pending}
|
||||
aria-label="Entwurf löschen"
|
||||
className="text-ink-muted hover:text-danger-solid"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user