Ideen: fertige Karten sammelweise wegraeumen
Ampel / ampel (push) Successful in 22s

Einzeln ging es schon (X je Karte). Bei fuenf abgeschlossenen Projekt-Ketten und
18 fertigen Karten waren das aber ~18 Klicks — praktisch raeumt dann niemand auf,
und die drei relevanten Karten gehen in der Liste unter.

Neu:
  POST /api/ideen/archivieren-alle {projekt?}  -> services.archive_done()
  Kopfzeile:      "18 fertige wegraeumen" (alle, auch die in Ketten)
  Je Projekt:     "wegraeumen" neben "N fertig"

Sicherheitseigenschaft, bewusst im Dienst statt in der UI: archive_done fasst
AUSSCHLIESSLICH status=done an — laufende, haengende, wartende und in Ausarbeitung
befindliche Karten nie, auch nicht ohne Projekt-Filter. Ein Sammel-Knopf darf
niemals versehentlich Arbeit abraeumen. Der Dialog sagt ausserdem ehrlich, dass
nichts geloescht wird: es ist Hermes' Kanban-Archiv, die Karten bleiben lesbar.

Getestet ohne Nebenwirkung: Filter auf ein nicht existierendes Projekt liefert
archiviert=0, die 19 Karten der Queue bleiben unangetastet. Das echte Wegraeumen
ist ein User-Klick, kein Deploy-Schritt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 22:54:35 +02:00
parent 991c9a2f82
commit 6c14705798
28 changed files with 178 additions and 65 deletions
+73 -9
View File
@@ -111,6 +111,36 @@ function IdeenSection() {
}
}
// Sammel-Aufräumen: fertige Karten wegräumen, ohne 18-mal auf das X zu klicken.
// Fragt vorher nach — und sagt dabei ehrlich, dass nichts gelöscht wird, sondern
// ins Kanban-Archiv wandert. Das Backend fasst ausschliesslich `done` an.
function archiveAlle(projekt: string, anzahl: number, was: string) {
showConfirm(
`${anzahl} fertige Karte${anzahl === 1 ? "" : "n"} wegräumen?`,
`${was} verschwindet aus der Liste. Gelöscht wird nichts — die Karten wandern ins ` +
`Archiv der Queue und bleiben dort lesbar. Laufende, hängende und wartende Karten ` +
`bleiben unberührt.`,
async () => {
const key = "arch:" + (projekt || "*")
setBusy((b) => ({ ...b, [key]: true }))
try {
const r = await api<{ archiviert: number; fehlgeschlagen: { titel: string }[] }>(
"/api/ideen/archivieren-alle", { method: "POST", body: JSON.stringify({ projekt }) })
if (r.fehlgeschlagen?.length) {
showAlert("Teilweise erledigt",
`${r.archiviert} weggeräumt, ${r.fehlgeschlagen.length} nicht: ` +
r.fehlgeschlagen.map((f) => f.titel).join(", "))
}
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, [key]: false }))
}
},
)
}
async function archive(id: string) {
setBusy((b) => ({ ...b, [id]: true }))
try {
@@ -349,7 +379,9 @@ function IdeenSection() {
return (
<section className="space-y-3">
{/* Kopfzeile sagt in einem Blick, was DICH betrifft — vorher stand hier nur eine
Gesamtzahl „offen", die nichts darüber verriet, ob etwas auf dich wartet. */}
Gesamtzahl „offen", die nichts darüber verriet, ob etwas auf dich wartet.
Rechts daneben das Sammel-Aufräumen, das nur erscheint, wenn es etwas zu räumen gibt. */}
<div className="flex items-center justify-between gap-3">
<SectionLabel icon={Lightbulb}>
{(() => {
if (!data) return "Ideen-Queue"
@@ -363,6 +395,21 @@ function IdeenSection() {
return `Ideen-Queue${teile.length ? " — " + teile.join(" · ") : ""}`
})()}
</SectionLabel>
{(() => {
const fertig = items.filter((i) => i.status === "done").length
if (fertig === 0) return null
return (
<button
disabled={!!busy["arch:*"]}
onClick={() => archiveAlle("", fertig, `Alle ${fertig} fertigen Karten`)}
title="Räumt ALLE fertigen Karten weg — auch die in abgeschlossenen Projekt-Ketten. Laufende, hängende und wartende bleiben unberührt."
className="flex shrink-0 items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1 text-[11px] font-semibold text-muted-foreground transition-all hover:border-primary/40 hover:text-foreground disabled:opacity-50 cursor-pointer">
{busy["arch:*"] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
{fertig} fertige wegräumen
</button>
)
})()}
</div>
<div className="mc-card p-4 space-y-3">
<div className="flex gap-2">
<input
@@ -487,7 +534,8 @@ function IdeenSection() {
{items.length > 0 && (
<div className="space-y-3">
{(data?.projekte ?? []).map((p) => (
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)} zeile={zeile} />
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)}
zeile={zeile} onArchivAlle={archiveAlle} raeumt={!!busy["arch:" + p.key]} />
))}
{(() => {
// Einzelkarten (keine Kette). Bis 25.07. eine flache Erstellzeit-Liste — bei
@@ -542,10 +590,12 @@ function IdeenSection() {
// Ersetzt für Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
const AKTIV_RANG: Record<string, number> = { blocked: 0, running: 1, review: 2, ready: 3, triage: 4 }
function ProjektGruppe({ projekt, karten, zeile }: {
function ProjektGruppe({ projekt, karten, zeile, onArchivAlle, raeumt }: {
projekt: IdeenProjekt
karten: IdeenItem[]
zeile: (it: IdeenItem) => React.ReactNode
onArchivAlle?: (projekt: string, anzahl: number, was: string) => void
raeumt?: boolean
}) {
const [zeigWartende, setZeigWartende] = useState(false)
const [zeigFertige, setZeigFertige] = useState(false)
@@ -583,12 +633,26 @@ function ProjektGruppe({ projekt, karten, zeile }: {
)}
{fertig.length > 0 && (
<div>
<button
onClick={() => setZeigFertige((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{fertig.length} fertig
</button>
<div className="flex items-center gap-2">
<button
onClick={() => setZeigFertige((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{fertig.length} fertig
</button>
{/* Ganze abgeschlossene Kette in einem Zug wegräumen — bei fünf fertigen
Projekten waren das sonst ~18 Einzelklicks auf das X. */}
{onArchivAlle && (
<button
disabled={raeumt}
onClick={() => onArchivAlle(projekt.key, fertig.length, `${projekt.titel}"`)}
title="Räumt die fertigen Karten dieser Kette weg (Archiv, nichts wird gelöscht)."
className="flex items-center gap-1 text-[11px] text-muted-foreground/60 transition-colors hover:text-foreground disabled:opacity-50 cursor-pointer">
{raeumt ? <Loader2 className="h-3 w-3 animate-spin" /> : <X className="h-3 w-3" />}
wegräumen
</button>
)}
</div>
{zeigFertige && <ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>}
</div>
)}