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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user