Files
alpenwerk-hr/components/dashboard/DraftsCard.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

68 lines
2.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.

"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>
);
}