Files
alpenwerk-hr/components/dashboard/DraftsCard.tsx
Maximilian Stubhan eeaf210e78
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m47s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Let the same choice open notes and drafts
The picker in the bell now governs both lists, so note_subscriptions is
renamed to colleague_subscriptions -- a name that only mentions notes would
mislead the next reader.

Reading and writing a draft now reach differently far. hire_drafts_owner
(for all) is split into four policies: select lets in your own drafts and
those of the people you added, while insert/update/delete stay with the
owner. A draft is unfinished work with no lock and no history; two people
writing into the same row would overwrite each other silently.

That split forces a change in the actions: a policy does not reject a write,
it lets it hit no rows. saveHireDraft and deleteHireDraft now read the row
count instead of reporting success over a row that never changed.

The card shows a foreign draft with its author and without Fortsetzen or
Loeschen -- offering a button that reliably ends in a database error is a
promise without cover.

check-schema-types.mjs learns `alter table ... rename to`; without it the
drift check reports one rename as two errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:05:40 +02:00

88 lines
3.6 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 type { Entwurf } from "@/lib/entwuerfe";
import { fmtDate } from "@/lib/format";
// Angefangene Neueinstellungen — die eigenen und, wer in der Glocke
// hinzugewählt ist, auch dessen.
//
// Fremde Entwürfe stehen hier zum Ansehen. Fortsetzen und Löschen fehlen bei
// ihnen nicht aus Vorsicht, sondern weil die Datenbank sie ohnehin abwiese
// (hire_drafts_update, hire_drafts_delete): eine Schaltfläche anzubieten, die
// verlässlich in eine Fehlermeldung führt, wäre ein Versprechen ohne Deckung.
export function DraftsCard({ drafts }: { drafts: Entwurf[] }) {
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 gap-3 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>
{/* Nur bei fremden Entwürfen. Der eigene Name stünde sonst an
jeder Zeile und sagte nichts. */}
{d.autor && <span className="ml-2 text-xs text-ink-muted">von {d.autor}</span>}
</div>
{d.vonMir ? (
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => openWizard({ draftId: 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>
) : (
// Warum hier nichts steht, ist sonst nicht zu erraten: die
// Zeile sähe aus wie eine, an der die Schaltflächen fehlen.
<span className="shrink-0 text-xs text-ink-muted">Nur zur Ansicht</span>
)}
</li>
);
})}
</ul>
</div>
);
}