feat: melhora a inbox de pre-lancamentos
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { preLancamentos } from "@/db/schema";
|
import { preLancamentos } from "@/db/schema";
|
||||||
import { handleActionError } from "@/lib/actions/helpers";
|
import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers";
|
||||||
import type { ActionResult } from "@/lib/actions/types";
|
import type { ActionResult } from "@/lib/actions/types";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@@ -17,6 +16,10 @@ const discardInboxSchema = z.object({
|
|||||||
inboxItemId: z.string().uuid("ID do item inválido"),
|
inboxItemId: z.string().uuid("ID do item inválido"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const restoreDiscardedInboxSchema = z.object({
|
||||||
|
inboxItemId: z.string().uuid("ID do item inválido"),
|
||||||
|
});
|
||||||
|
|
||||||
const bulkDiscardSchema = z.object({
|
const bulkDiscardSchema = z.object({
|
||||||
inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"),
|
inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"),
|
||||||
});
|
});
|
||||||
@@ -30,9 +33,7 @@ const bulkDeleteInboxSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function revalidateInbox() {
|
function revalidateInbox() {
|
||||||
revalidatePath("/pre-lancamentos");
|
revalidateForEntity("inbox");
|
||||||
revalidatePath("/lancamentos");
|
|
||||||
revalidatePath("/dashboard");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,6 +167,54 @@ export async function bulkDiscardInboxItemsAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restoreDiscardedInboxItemAction(
|
||||||
|
input: z.infer<typeof restoreDiscardedInboxSchema>,
|
||||||
|
): Promise<ActionResult> {
|
||||||
|
try {
|
||||||
|
const user = await getUser();
|
||||||
|
const data = restoreDiscardedInboxSchema.parse(input);
|
||||||
|
|
||||||
|
const [item] = await db
|
||||||
|
.select({ id: preLancamentos.id })
|
||||||
|
.from(preLancamentos)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(preLancamentos.id, data.inboxItemId),
|
||||||
|
eq(preLancamentos.userId, user.id),
|
||||||
|
eq(preLancamentos.status, "discarded"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Item não encontrado ou não está descartado.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(preLancamentos)
|
||||||
|
.set({
|
||||||
|
status: "pending",
|
||||||
|
discardedAt: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(preLancamentos.id, data.inboxItemId),
|
||||||
|
eq(preLancamentos.userId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateInbox();
|
||||||
|
|
||||||
|
return { success: true, message: "Item voltou para pendentes." };
|
||||||
|
} catch (error) {
|
||||||
|
return handleActionError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteInboxItemAction(
|
export async function deleteInboxItemAction(
|
||||||
input: z.infer<typeof deleteInboxSchema>,
|
input: z.infer<typeof deleteInboxSchema>,
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
RiArrowGoBackLine,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiMoreLine,
|
RiMoreLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
@@ -37,6 +38,7 @@ interface InboxCardProps {
|
|||||||
onDiscard?: (item: InboxItem) => void;
|
onDiscard?: (item: InboxItem) => void;
|
||||||
onViewDetails?: (item: InboxItem) => void;
|
onViewDetails?: (item: InboxItem) => void;
|
||||||
onDelete?: (item: InboxItem) => void;
|
onDelete?: (item: InboxItem) => void;
|
||||||
|
onRestoreToPending?: (item: InboxItem) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLogoPath(logo: string): string {
|
function resolveLogoPath(logo: string): string {
|
||||||
@@ -79,6 +81,7 @@ export function InboxCard({
|
|||||||
onDiscard,
|
onDiscard,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onRestoreToPending,
|
||||||
}: InboxCardProps) {
|
}: InboxCardProps) {
|
||||||
const matchedLogo = useMemo(
|
const matchedLogo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -161,7 +164,7 @@ export function InboxCard({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => onViewDetails?.(item)}>
|
<DropdownMenuItem onClick={() => onViewDetails?.(item)}>
|
||||||
<RiEyeLine className="mr-2 size-4" />
|
<RiFileList2Line className="mr-2 size-4" />
|
||||||
Ver detalhes
|
Ver detalhes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onProcess?.(item)}>
|
<DropdownMenuItem onClick={() => onProcess?.(item)}>
|
||||||
@@ -204,16 +207,30 @@ export function InboxCard({
|
|||||||
{formattedStatusDate}
|
{formattedStatusDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{onDelete && (
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<Button
|
{item.status === "discarded" && onRestoreToPending && (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon-sm"
|
variant="ghost"
|
||||||
className="ml-auto text-muted-foreground hover:text-destructive"
|
size="icon-sm"
|
||||||
onClick={() => onDelete(item)}
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
onClick={() => onRestoreToPending(item)}
|
||||||
<RiDeleteBinLine className="size-4" />
|
aria-label="Voltar para pendente"
|
||||||
</Button>
|
title="Voltar para pendente"
|
||||||
)}
|
>
|
||||||
|
<RiArrowGoBackLine className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={() => onDelete(item)}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
) : (
|
) : (
|
||||||
<CardFooter className="gap-2 pt-3 pb-4">
|
<CardFooter className="gap-2 pt-3 pb-4">
|
||||||
@@ -226,10 +243,12 @@ export function InboxCard({
|
|||||||
Processar
|
Processar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
onClick={() => onDiscard?.(item)}
|
onClick={() => onDiscard?.(item)}
|
||||||
className="text-muted-foreground hover:text-destructive hover:border-destructive"
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
aria-label="Descartar notificação"
|
||||||
|
title="Descartar notificação"
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="size-4" />
|
<RiDeleteBinLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
deleteInboxItemAction,
|
deleteInboxItemAction,
|
||||||
discardInboxItemAction,
|
discardInboxItemAction,
|
||||||
markInboxAsProcessedAction,
|
markInboxAsProcessedAction,
|
||||||
|
restoreDiscardedInboxItemAction,
|
||||||
} from "@/app/(dashboard)/pre-lancamentos/actions";
|
} from "@/app/(dashboard)/pre-lancamentos/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
|
||||||
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
||||||
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
@@ -58,6 +59,9 @@ export function InboxPage({
|
|||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState<InboxItem | null>(null);
|
const [itemToDelete, setItemToDelete] = useState<InboxItem | null>(null);
|
||||||
|
|
||||||
|
const [restoreOpen, setRestoreOpen] = useState(false);
|
||||||
|
const [itemToRestore, setItemToRestore] = useState<InboxItem | null>(null);
|
||||||
|
|
||||||
const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
|
const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
|
||||||
const [bulkDeleteStatus, setBulkDeleteStatus] = useState<
|
const [bulkDeleteStatus, setBulkDeleteStatus] = useState<
|
||||||
"processed" | "discarded"
|
"processed" | "discarded"
|
||||||
@@ -166,6 +170,34 @@ export function InboxPage({
|
|||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
}, [itemToDelete]);
|
}, [itemToDelete]);
|
||||||
|
|
||||||
|
const handleRestoreOpenChange = useCallback((open: boolean) => {
|
||||||
|
setRestoreOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setItemToRestore(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRestoreRequest = useCallback((item: InboxItem) => {
|
||||||
|
setItemToRestore(item);
|
||||||
|
setRestoreOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRestoreToPendingConfirm = useCallback(async () => {
|
||||||
|
if (!itemToRestore) return;
|
||||||
|
|
||||||
|
const result = await restoreDiscardedInboxItemAction({
|
||||||
|
inboxItemId: itemToRestore.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(result.error);
|
||||||
|
throw new Error(result.error);
|
||||||
|
}, [itemToRestore]);
|
||||||
|
|
||||||
const handleBulkDeleteOpenChange = useCallback((open: boolean) => {
|
const handleBulkDeleteOpenChange = useCallback((open: boolean) => {
|
||||||
setBulkDeleteOpen(open);
|
setBulkDeleteOpen(open);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -271,6 +303,7 @@ export function InboxPage({
|
|||||||
onDiscard={readonly ? undefined : handleDiscardRequest}
|
onDiscard={readonly ? undefined : handleDiscardRequest}
|
||||||
onViewDetails={readonly ? undefined : handleDetailsRequest}
|
onViewDetails={readonly ? undefined : handleDetailsRequest}
|
||||||
onDelete={readonly ? handleDeleteRequest : undefined}
|
onDelete={readonly ? handleDeleteRequest : undefined}
|
||||||
|
onRestoreToPending={readonly ? handleRestoreRequest : undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -279,15 +312,27 @@ export function InboxPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs defaultValue="pending" className="w-full">
|
<Tabs defaultValue="pending" className="w-full">
|
||||||
<TabsList>
|
<TabsList className="grid h-auto w-full grid-cols-3 sm:inline-flex sm:h-9 sm:grid-cols-none">
|
||||||
<TabsTrigger value="pending">
|
<TabsTrigger
|
||||||
Pendentes ({pendingItems.length})
|
value="pending"
|
||||||
|
className="h-11 min-w-0 flex-col gap-0 px-1 text-sm leading-tight sm:h-9 sm:flex-row sm:gap-1 sm:px-4"
|
||||||
|
>
|
||||||
|
<span>Pendentes</span>
|
||||||
|
<span>({pendingItems.length})</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="processed">
|
<TabsTrigger
|
||||||
Processados ({processedItems.length})
|
value="processed"
|
||||||
|
className="h-11 min-w-0 flex-col gap-0 px-1 text-sm leading-tight sm:h-9 sm:flex-row sm:gap-1 sm:px-4"
|
||||||
|
>
|
||||||
|
<span>Processados</span>
|
||||||
|
<span>({processedItems.length})</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="discarded">
|
<TabsTrigger
|
||||||
Descartados ({discardedItems.length})
|
value="discarded"
|
||||||
|
className="h-11 min-w-0 flex-col gap-0 px-1 text-sm leading-tight sm:h-9 sm:flex-row sm:gap-1 sm:px-4"
|
||||||
|
>
|
||||||
|
<span>Descartados</span>
|
||||||
|
<span>({discardedItems.length})</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -374,6 +419,16 @@ export function InboxPage({
|
|||||||
onConfirm={handleDeleteConfirm}
|
onConfirm={handleDeleteConfirm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmActionDialog
|
||||||
|
open={restoreOpen}
|
||||||
|
onOpenChange={handleRestoreOpenChange}
|
||||||
|
title="Retornar para pendentes?"
|
||||||
|
description="A notificação voltará para a lista de pendentes e poderá ser processada depois."
|
||||||
|
confirmLabel="Retornar"
|
||||||
|
pendingLabel="Retornando..."
|
||||||
|
onConfirm={handleRestoreToPendingConfirm}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmActionDialog
|
<ConfirmActionDialog
|
||||||
open={bulkDeleteOpen}
|
open={bulkDeleteOpen}
|
||||||
onOpenChange={handleBulkDeleteOpenChange}
|
onOpenChange={handleBulkDeleteOpenChange}
|
||||||
|
|||||||
Reference in New Issue
Block a user