diff --git a/frontend/src/components/BulkToolbar.jsx b/frontend/src/components/BulkToolbar.jsx
index ef2bcfe..97fa140 100644
--- a/frontend/src/components/BulkToolbar.jsx
+++ b/frontend/src/components/BulkToolbar.jsx
@@ -1,30 +1,93 @@
+import { useState } from 'react';
+import { MailOpen, Mail, Star, Archive, Trash2 } from 'lucide-react';
import { BulkApi } from '../api/client.js';
+import {
+ Button, useToast,
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,
+} from './ui';
-///
-/// Toolbar shown above an email list when one or more rows are selected.
-/// `onDone` is called with the action key after a successful bulk call so the
-/// caller can update local state (e.g. remove trashed/archived rows).
-///
+/**
+ * Toolbar shown above an email list when rows are selected.
+ *
+ * Safety (Phase 4 / UX-Critical): the destructive Trash action now requires an
+ * explicit confirmation dialog, every action surfaces success/partial-failure via
+ * a toast, and rows are only removed from the list when the server confirms the
+ * whole batch succeeded (no more optimistic removal that hides failures).
+ * `onDone(action, ids)` is called only on full success so the caller can prune state.
+ */
export default function BulkToolbar({ selectedIds, onDone, onClear }) {
+ const { toast } = useToast();
+ const [busy, setBusy] = useState(false);
+ const [confirmTrash, setConfirmTrash] = useState(false);
const count = selectedIds.length;
if (count === 0) return null;
- const run = (fn, action) => async () => {
- await fn(selectedIds);
- onDone(action, selectedIds);
- onClear();
+ const apply = async (fn, action, label) => {
+ setBusy(true);
+ try {
+ const res = await fn(selectedIds);
+ // CleanupResultDto: { succeededCount, failedCount, errors }
+ const ok = res?.succeededCount ?? count;
+ const failed = res?.failedCount ?? 0;
+ if (failed > 0) {
+ toast({
+ variant: 'warning',
+ title: `${label}: ${ok} done, ${failed} failed`,
+ description: 'Some items could not be updated — the list was left unchanged so you can retry.',
+ });
+ } else {
+ toast({ variant: 'success', title: `${label} ${ok} email${ok === 1 ? '' : 's'}` });
+ onDone(action, selectedIds);
+ onClear();
+ }
+ } catch {
+ toast({ variant: 'danger', title: `Couldn't ${label.toLowerCase()} ${count} email${count === 1 ? '' : 's'}`, description: 'Please try again.' });
+ } finally {
+ setBusy(false);
+ setConfirmTrash(false);
+ }
};
return (
-
-
{count} selected
-
-
-
-
-
-
-
+
+
{count} selected
+
+
+
+
+
+
+
+
+
);
}