From 173fc86920e02490d4d80ac69968697392cb53fb Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sun, 15 Mar 2026 23:24:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20adiciona=20a=C3=A7=C3=B5es=20em=20lote?= =?UTF-8?q?=20ao=20inbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/inbox/actions.ts | 36 ++++- src/features/inbox/components/inbox-card.tsx | 23 ++- src/features/inbox/components/inbox-page.tsx | 141 ++++++++++++++++++- 3 files changed, 192 insertions(+), 8 deletions(-) diff --git a/src/features/inbox/actions.ts b/src/features/inbox/actions.ts index 22c50fe..de321b1 100644 --- a/src/features/inbox/actions.ts +++ b/src/features/inbox/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, ne } from "drizzle-orm"; import { z } from "zod"; import { inboxItems } from "@/db/schema"; import { @@ -35,6 +35,10 @@ const bulkDeleteInboxSchema = z.object({ status: z.enum(["processed", "discarded"]), }); +const bulkDeleteSelectedInboxSchema = z.object({ + inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"), +}); + function revalidateInbox() { revalidateForEntity("inbox"); } @@ -264,6 +268,36 @@ export async function deleteInboxItemAction( } } +export async function bulkDeleteSelectedInboxItemsAction( + input: z.infer, +): Promise { + try { + const user = await getUser(); + const data = bulkDeleteSelectedInboxSchema.parse(input); + + const result = await db + .delete(inboxItems) + .where( + and( + inArray(inboxItems.id, data.inboxItemIds), + eq(inboxItems.userId, user.id), + ne(inboxItems.status, "pending"), + ), + ) + .returning({ id: inboxItems.id }); + + revalidateInbox(); + + const count = result.length; + return { + success: true, + message: `${count} item(s) excluído(s).`, + }; + } catch (error) { + return handleActionError(error); + } +} + export async function bulkDeleteInboxItemsAction( input: z.infer, ): Promise { diff --git a/src/features/inbox/components/inbox-card.tsx b/src/features/inbox/components/inbox-card.tsx index bdd8a3e..1539877 100644 --- a/src/features/inbox/components/inbox-card.tsx +++ b/src/features/inbox/components/inbox-card.tsx @@ -21,6 +21,7 @@ import { CardHeader, CardTitle, } from "@/shared/components/ui/card"; +import { Checkbox } from "@/shared/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, @@ -39,6 +40,8 @@ interface InboxCardProps { onViewDetails?: (item: InboxItem) => void; onDelete?: (item: InboxItem) => void; onRestoreToPending?: (item: InboxItem) => void | Promise; + selected?: boolean; + onSelectToggle?: (id: string) => void; } function findMatchingLogo( @@ -71,6 +74,8 @@ export function InboxCard({ onViewDetails, onDelete, onRestoreToPending, + selected, + onSelectToggle, }: InboxCardProps) { const matchedLogo = appLogoMap ? findMatchingLogo(item.sourceAppName, appLogoMap) @@ -112,7 +117,9 @@ export function InboxCard({ : null; return ( - + {/* Header com app e valor */}
@@ -217,6 +224,13 @@ export function InboxCard({ )} + {onSelectToggle && ( + onSelectToggle(item.id)} + aria-label="Selecionar item" + /> + )}
) : ( @@ -239,6 +253,13 @@ export function InboxCard({ > + {onSelectToggle && ( + onSelectToggle(item.id)} + aria-label="Selecionar item" + /> + )} )}
diff --git a/src/features/inbox/components/inbox-page.tsx b/src/features/inbox/components/inbox-page.tsx index df1597a..c7795c6 100644 --- a/src/features/inbox/components/inbox-page.tsx +++ b/src/features/inbox/components/inbox-page.tsx @@ -5,6 +5,8 @@ import { useMemo, useState } from "react"; import { toast } from "sonner"; import { bulkDeleteInboxItemsAction, + bulkDeleteSelectedInboxItemsAction, + bulkDiscardInboxItemsAction, deleteInboxItemAction, discardInboxItemAction, markInboxAsProcessedAction, @@ -72,6 +74,19 @@ export function InboxPage({ "processed" | "discarded" >("processed"); + const [selectedPendingIds, setSelectedPendingIds] = useState([]); + const [selectedProcessedIds, setSelectedProcessedIds] = useState( + [], + ); + const [selectedDiscardedIds, setSelectedDiscardedIds] = useState( + [], + ); + + const [selectionBulkOpen, setSelectionBulkOpen] = useState(false); + const [selectionBulkStatus, setSelectionBulkStatus] = useState< + "pending" | "processed" | "discarded" + >("pending"); + const sortedPending = useMemo( () => [...pendingItems].sort( @@ -208,6 +223,52 @@ export function InboxPage({ throw new Error(result.error); }; + const toggleSelection = ( + ids: string[], + setIds: (v: string[]) => void, + id: string, + ) => { + setIds(ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]); + }; + + const handleSelectionBulkRequest = ( + status: "pending" | "processed" | "discarded", + ) => { + setSelectionBulkStatus(status); + setSelectionBulkOpen(true); + }; + + const handleSelectionBulkConfirm = async () => { + if (selectionBulkStatus === "pending") { + const result = await bulkDiscardInboxItemsAction({ + inboxItemIds: selectedPendingIds, + }); + if (result.success) { + toast.success(result.message); + setSelectedPendingIds([]); + return; + } + toast.error(result.error); + throw new Error(result.error); + } else { + const ids = + selectionBulkStatus === "processed" + ? selectedProcessedIds + : selectedDiscardedIds; + const result = await bulkDeleteSelectedInboxItemsAction({ + inboxItemIds: ids, + }); + if (result.success) { + toast.success(result.message); + if (selectionBulkStatus === "processed") setSelectedProcessedIds([]); + else setSelectedDiscardedIds([]); + return; + } + toast.error(result.error); + throw new Error(result.error); + } + }; + const handleBulkDeleteOpenChange = (open: boolean) => { setBulkDeleteOpen(open); }; @@ -291,7 +352,12 @@ export function InboxPage({
); - const renderGrid = (list: InboxItem[], readonly?: boolean) => + const renderGrid = ( + list: InboxItem[], + readonly?: boolean, + selectedIds?: string[], + onToggle?: (id: string) => void, + ) => list.length === 0 ? ( renderEmptyState( readonly @@ -311,6 +377,8 @@ export function InboxPage({ onViewDetails={readonly ? undefined : handleDetailsRequest} onDelete={readonly ? handleDeleteRequest : undefined} onRestoreToPending={readonly ? handleRestoreRequest : undefined} + selected={selectedIds?.includes(item.id)} + onSelectToggle={onToggle} /> ))} @@ -344,11 +412,35 @@ export function InboxPage({ - {renderGrid(sortedPending)} + {selectedPendingIds.length > 0 && ( +
+ +
+ )} + {renderGrid(sortedPending, false, selectedPendingIds, (id) => + toggleSelection(selectedPendingIds, setSelectedPendingIds, id), + )}
{sortedProcessed.length > 0 && ( -
+
+ {selectedProcessedIds.length > 0 && ( + + )}
)} - {renderGrid(sortedProcessed, true)} + {renderGrid(sortedProcessed, true, selectedProcessedIds, (id) => + toggleSelection(selectedProcessedIds, setSelectedProcessedIds, id), + )} {sortedDiscarded.length > 0 && ( -
+
+ {selectedDiscardedIds.length > 0 && ( + + )}
)} - {renderGrid(sortedDiscarded, true)} + {renderGrid(sortedDiscarded, true, selectedDiscardedIds, (id) => + toggleSelection(selectedDiscardedIds, setSelectedDiscardedIds, id), + )} @@ -446,6 +552,29 @@ export function InboxPage({ pendingLabel="Excluindo..." onConfirm={handleBulkDeleteConfirm} /> + + ); }