UI: Bestätigungsdialog vor allen Löschungen (Projekt + Lead)

Bisher loeschten 8 Muelltonnen-Icons sofort und unwiderruflich, ganz ohne
Rueckfrage. Jetzt fragt jeder Loeschpfad ueber useConfirm() nach.

- projekt.tsx: Aenderung, Stakeholder, Risiko, Meilenstein
- lead.tsx: Kontakt, Touchpoint, offene Frage, Anhang

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 11:28:48 +02:00
1 parent 33a740ca7e
commit 90caa282ee
2 files changed
+90 -8

No files matched your search

+45 -4
View File
@@ -10,6 +10,7 @@ import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { useConfirm } from '../components/ui/ConfirmProvider';
import {
LEAD_STATUS_META,
LEAD_STATUS_ORDER,
@@ -591,6 +592,7 @@ function ContactRow({ leadId, contact }: { leadId: string; contact: Contact }) {
await utils.lead.byId.invalidate({ id: leadId });
},
});
const confirm = useConfirm();
const details = [
contact.email,
contact.telefon,
@@ -628,7 +630,16 @@ function ContactRow({ leadId, contact }: { leadId: string; contact: Contact }) {
</div>
<button
type="button"
onClick={() => remove.mutate({ contactId: contact.id })}
onClick={async () => {
if (
await confirm({
title: 'Kontakt entfernen?',
destructive: true,
confirmLabel: 'Entfernen',
})
)
remove.mutate({ contactId: contact.id });
}}
aria-label="Kontakt entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -804,6 +815,7 @@ function TouchpointRow({ leadId, touchpoint }: { leadId: string; touchpoint: Tou
await utils.lead.byId.invalidate({ id: leadId });
},
});
const confirm = useConfirm();
return (
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
<div className="flex items-start justify-between gap-2">
@@ -821,7 +833,16 @@ function TouchpointRow({ leadId, touchpoint }: { leadId: string; touchpoint: Tou
</div>
<button
type="button"
onClick={() => remove.mutate({ touchpointId: touchpoint.id })}
onClick={async () => {
if (
await confirm({
title: 'Touchpoint löschen?',
destructive: true,
confirmLabel: 'Löschen',
})
)
remove.mutate({ touchpointId: touchpoint.id });
}}
aria-label="Eintrag entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -934,6 +955,7 @@ function OffeneFragenSection({
await utils.lead.byId.invalidate({ id: leadId });
},
});
const confirm = useConfirm();
function persist(next: Array<{ text: string; done: boolean }>) {
update.mutate({ id: leadId, offeneFragen: next });
}
@@ -963,7 +985,16 @@ function OffeneFragenSection({
</span>
<button
type="button"
onClick={() => persist(fragen.filter((_, j) => j !== i))}
onClick={async () => {
if (
await confirm({
title: 'Frage löschen?',
destructive: true,
confirmLabel: 'Löschen',
})
)
persist(fragen.filter((_, j) => j !== i));
}}
aria-label="Frage entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -1013,6 +1044,7 @@ function AnhaengeSection({
await utils.lead.byId.invalidate({ id: leadId });
},
});
const confirm = useConfirm();
function persist(next: Array<{ label: string; url: string }>) {
update.mutate({ id: leadId, anhaenge: next });
}
@@ -1031,7 +1063,16 @@ function AnhaengeSection({
<ExternalLink url={a.url} label={a.label} />
<button
type="button"
onClick={() => persist(anhaenge.filter((_, j) => j !== i))}
onClick={async () => {
if (
await confirm({
title: 'Anhang entfernen?',
destructive: true,
confirmLabel: 'Entfernen',
})
)
persist(anhaenge.filter((_, j) => j !== i));
}}
aria-label="Anhang entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
+45 -4
View File
@@ -7,6 +7,7 @@ import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
import { useConfirm } from '../components/ui/ConfirmProvider';
import { useCurrentUser } from '../hooks/useCurrentUser';
import { voice } from '../lib/voice';
import { toast } from 'sonner';
@@ -1049,6 +1050,7 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
const invalidate = () => utils.change.forProject.invalidate({ projectId });
const update = trpc.change.update.useMutation({ onSuccess: invalidate });
const remove = trpc.change.remove.useMutation({ onSuccess: invalidate });
const confirm = useConfirm();
const meta = CHANGE_ENTSCHEIDUNG_META[c.entscheidung as ChangeEntscheidung];
return (
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
@@ -1087,7 +1089,16 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
</select>
<button
type="button"
onClick={() => remove.mutate({ id: c.id })}
onClick={async () => {
if (
await confirm({
title: 'Änderung löschen?',
destructive: true,
confirmLabel: 'Löschen',
})
)
remove.mutate({ id: c.id });
}}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -1223,6 +1234,7 @@ function StakeholderRow({
},
});
const remove = trpc.stakeholder.remove.useMutation({ onSuccess: invalidate });
const confirm = useConfirm();
const [name, setName] = useState(s.name);
const [rolle, setRolle] = useState(s.rolle ?? '');
@@ -1341,7 +1353,16 @@ function StakeholderRow({
</button>
<button
type="button"
onClick={() => remove.mutate({ id: s.id })}
onClick={async () => {
if (
await confirm({
title: 'Stakeholder entfernen?',
destructive: true,
confirmLabel: 'Entfernen',
})
)
remove.mutate({ id: s.id });
}}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -1673,6 +1694,7 @@ function RiskRow({
},
});
const remove = trpc.risk.remove.useMutation({ onSuccess: invalidate });
const confirm = useConfirm();
const [title, setTitle] = useState(r.title);
const [wahr, setWahr] = useState<Level>(r.wahrscheinlichkeit as Level);
@@ -1828,7 +1850,16 @@ function RiskRow({
</button>
<button
type="button"
onClick={() => remove.mutate({ id: r.id })}
onClick={async () => {
if (
await confirm({
title: 'Risiko löschen?',
destructive: true,
confirmLabel: 'Löschen',
})
)
remove.mutate({ id: r.id });
}}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
@@ -2061,6 +2092,7 @@ function MilestoneRow({ projectId, milestone: m }: { projectId: string; mileston
},
});
const remove = trpc.project.removeMilestone.useMutation({ onSuccess: invalidate });
const confirm = useConfirm();
const [title, setTitle] = useState(m.title);
const [due, setDue] = useState(m.due ?? '');
@@ -2174,7 +2206,16 @@ function MilestoneRow({ projectId, milestone: m }: { projectId: string; mileston
</button>
<button
type="button"
onClick={() => remove.mutate({ milestoneId: m.id })}
onClick={async () => {
if (
await confirm({
title: 'Meilenstein löschen?',
destructive: true,
confirmLabel: 'Löschen',
})
)
remove.mutate({ milestoneId: m.id });
}}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>