mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
feat: adiciona ações em lote ao inbox
This commit is contained in:
@@ -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<typeof bulkDeleteSelectedInboxSchema>,
|
||||
): Promise<ActionResult> {
|
||||
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<typeof bulkDeleteInboxSchema>,
|
||||
): Promise<ActionResult> {
|
||||
|
||||
@@ -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<void>;
|
||||
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 (
|
||||
<Card className="flex flex-col gap-0 py-0 h-54">
|
||||
<Card
|
||||
className={`flex flex-col gap-0 py-0 h-54 transition-colors ${selected ? "ring-2 ring-primary" : ""}`}
|
||||
>
|
||||
{/* Header com app e valor */}
|
||||
<CardHeader className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -217,6 +224,13 @@ export function InboxCard({
|
||||
<RiDeleteBinLine className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onSelectToggle && (
|
||||
<Checkbox
|
||||
checked={!!selected}
|
||||
onCheckedChange={() => onSelectToggle(item.id)}
|
||||
aria-label="Selecionar item"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
) : (
|
||||
@@ -239,6 +253,13 @@ export function InboxCard({
|
||||
>
|
||||
<RiDeleteBinLine className="size-4" />
|
||||
</Button>
|
||||
{onSelectToggle && (
|
||||
<Checkbox
|
||||
checked={!!selected}
|
||||
onCheckedChange={() => onSelectToggle(item.id)}
|
||||
aria-label="Selecionar item"
|
||||
/>
|
||||
)}
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -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<string[]>([]);
|
||||
const [selectedProcessedIds, setSelectedProcessedIds] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [selectedDiscardedIds, setSelectedDiscardedIds] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
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({
|
||||
</Card>
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -344,11 +412,35 @@ export function InboxPage({
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pending" className="mt-4">
|
||||
{renderGrid(sortedPending)}
|
||||
{selectedPendingIds.length > 0 && (
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleSelectionBulkRequest("pending")}
|
||||
>
|
||||
<RiDeleteBinLine className="mr-1.5 size-4" />
|
||||
Descartar selecionados ({selectedPendingIds.length})
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{renderGrid(sortedPending, false, selectedPendingIds, (id) =>
|
||||
toggleSelection(selectedPendingIds, setSelectedPendingIds, id),
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="processed" className="mt-4">
|
||||
{sortedProcessed.length > 0 && (
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="mb-4 flex items-center justify-end gap-2">
|
||||
{selectedProcessedIds.length > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleSelectionBulkRequest("processed")}
|
||||
>
|
||||
<RiDeleteBinLine className="mr-1.5 size-4" />
|
||||
Excluir selecionados ({selectedProcessedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -359,11 +451,23 @@ export function InboxPage({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{renderGrid(sortedProcessed, true)}
|
||||
{renderGrid(sortedProcessed, true, selectedProcessedIds, (id) =>
|
||||
toggleSelection(selectedProcessedIds, setSelectedProcessedIds, id),
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="discarded" className="mt-4">
|
||||
{sortedDiscarded.length > 0 && (
|
||||
<div className="mb-4 flex justify-end">
|
||||
<div className="mb-4 flex items-center justify-end gap-2">
|
||||
{selectedDiscardedIds.length > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleSelectionBulkRequest("discarded")}
|
||||
>
|
||||
<RiDeleteBinLine className="mr-1.5 size-4" />
|
||||
Excluir selecionados ({selectedDiscardedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -374,7 +478,9 @@ export function InboxPage({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{renderGrid(sortedDiscarded, true)}
|
||||
{renderGrid(sortedDiscarded, true, selectedDiscardedIds, (id) =>
|
||||
toggleSelection(selectedDiscardedIds, setSelectedDiscardedIds, id),
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -446,6 +552,29 @@ export function InboxPage({
|
||||
pendingLabel="Excluindo..."
|
||||
onConfirm={handleBulkDeleteConfirm}
|
||||
/>
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={selectionBulkOpen}
|
||||
onOpenChange={setSelectionBulkOpen}
|
||||
title={
|
||||
selectionBulkStatus === "pending"
|
||||
? "Descartar selecionados?"
|
||||
: "Excluir selecionados?"
|
||||
}
|
||||
description={
|
||||
selectionBulkStatus === "pending"
|
||||
? `${selectedPendingIds.length} item(s) serão descartados.`
|
||||
: `${selectionBulkStatus === "processed" ? selectedProcessedIds.length : selectedDiscardedIds.length} item(s) serão excluídos permanentemente.`
|
||||
}
|
||||
confirmLabel={
|
||||
selectionBulkStatus === "pending" ? "Descartar" : "Excluir"
|
||||
}
|
||||
confirmVariant="destructive"
|
||||
pendingLabel={
|
||||
selectionBulkStatus === "pending" ? "Descartando..." : "Excluindo..."
|
||||
}
|
||||
onConfirm={handleSelectionBulkConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user