Let the same choice open notes and drafts
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m47s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s

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>
This commit is contained in:
2026-09-10 16:05:40 +02:00
parent e8e675fd07
commit eeaf210e78
17 changed files with 650 additions and 54 deletions

View File

@@ -6,11 +6,18 @@ 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";
type Draft = { id: string; step: number; payload: Record<string, unknown>; updated_at: string };
// 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: Draft[] }) {
export function DraftsCard({ drafts }: { drafts: Entwurf[] }) {
const { openWizard } = useHireWizard();
const { showToast } = useToast();
const router = useRouter();
@@ -39,25 +46,38 @@ export function DraftsCard({ drafts }: { drafts: Draft[] }) {
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">
<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>
<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>
{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>
);
})}