forked from git.gladyson/openmonetis
275 lines
7.6 KiB
TypeScript
275 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import { RiInboxLine } from "@remixicon/react";
|
|
import { useCallback, useMemo, useState } from "react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
discardInboxItemAction,
|
|
markInboxAsProcessedAction,
|
|
} from "@/app/(dashboard)/pre-lancamentos/actions";
|
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
|
import { EmptyState } from "@/components/empty-state";
|
|
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { InboxCard } from "./inbox-card";
|
|
import { InboxDetailsDialog } from "./inbox-details-dialog";
|
|
import type { InboxItem, SelectOption } from "./types";
|
|
|
|
interface InboxPageProps {
|
|
pendingItems: InboxItem[];
|
|
processedItems: InboxItem[];
|
|
discardedItems: InboxItem[];
|
|
pagadorOptions: SelectOption[];
|
|
splitPagadorOptions: SelectOption[];
|
|
defaultPagadorId: string | null;
|
|
contaOptions: SelectOption[];
|
|
cartaoOptions: SelectOption[];
|
|
categoriaOptions: SelectOption[];
|
|
estabelecimentos: string[];
|
|
appLogoMap: Record<string, string>;
|
|
}
|
|
|
|
export function InboxPage({
|
|
pendingItems,
|
|
processedItems,
|
|
discardedItems,
|
|
pagadorOptions,
|
|
splitPagadorOptions,
|
|
defaultPagadorId,
|
|
contaOptions,
|
|
cartaoOptions,
|
|
categoriaOptions,
|
|
estabelecimentos,
|
|
appLogoMap,
|
|
}: InboxPageProps) {
|
|
const [processOpen, setProcessOpen] = useState(false);
|
|
const [itemToProcess, setItemToProcess] = useState<InboxItem | null>(null);
|
|
|
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
const [itemDetails, setItemDetails] = useState<InboxItem | null>(null);
|
|
|
|
const [discardOpen, setDiscardOpen] = useState(false);
|
|
const [itemToDiscard, setItemToDiscard] = useState<InboxItem | null>(null);
|
|
|
|
const sortByTimestamp = (list: InboxItem[]) =>
|
|
[...list].sort(
|
|
(a, b) =>
|
|
new Date(b.notificationTimestamp).getTime() -
|
|
new Date(a.notificationTimestamp).getTime(),
|
|
);
|
|
|
|
const sortedPending = useMemo(
|
|
() => sortByTimestamp(pendingItems),
|
|
[pendingItems],
|
|
);
|
|
const sortedProcessed = useMemo(
|
|
() => sortByTimestamp(processedItems),
|
|
[processedItems],
|
|
);
|
|
const sortedDiscarded = useMemo(
|
|
() => sortByTimestamp(discardedItems),
|
|
[discardedItems],
|
|
);
|
|
|
|
const handleProcessOpenChange = useCallback((open: boolean) => {
|
|
setProcessOpen(open);
|
|
if (!open) {
|
|
setItemToProcess(null);
|
|
}
|
|
}, []);
|
|
|
|
const handleDetailsOpenChange = useCallback((open: boolean) => {
|
|
setDetailsOpen(open);
|
|
if (!open) {
|
|
setItemDetails(null);
|
|
}
|
|
}, []);
|
|
|
|
const handleDiscardOpenChange = useCallback((open: boolean) => {
|
|
setDiscardOpen(open);
|
|
if (!open) {
|
|
setItemToDiscard(null);
|
|
}
|
|
}, []);
|
|
|
|
const handleProcessRequest = useCallback((item: InboxItem) => {
|
|
setItemToProcess(item);
|
|
setProcessOpen(true);
|
|
}, []);
|
|
|
|
const handleDetailsRequest = useCallback((item: InboxItem) => {
|
|
setItemDetails(item);
|
|
setDetailsOpen(true);
|
|
}, []);
|
|
|
|
const handleDiscardRequest = useCallback((item: InboxItem) => {
|
|
setItemToDiscard(item);
|
|
setDiscardOpen(true);
|
|
}, []);
|
|
|
|
const handleDiscardConfirm = useCallback(async () => {
|
|
if (!itemToDiscard) return;
|
|
|
|
const result = await discardInboxItemAction({
|
|
inboxItemId: itemToDiscard.id,
|
|
});
|
|
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
return;
|
|
}
|
|
|
|
toast.error(result.error);
|
|
throw new Error(result.error);
|
|
}, [itemToDiscard]);
|
|
|
|
const handleLancamentoSuccess = useCallback(async () => {
|
|
if (!itemToProcess) return;
|
|
|
|
const result = await markInboxAsProcessedAction({
|
|
inboxItemId: itemToProcess.id,
|
|
});
|
|
|
|
if (result.success) {
|
|
toast.success("Notificação processada!");
|
|
} else {
|
|
toast.error(result.error);
|
|
}
|
|
}, [itemToProcess]);
|
|
|
|
// Prepare default values from inbox item
|
|
const getDateString = (
|
|
date: Date | string | null | undefined,
|
|
): string | null => {
|
|
if (!date) return null;
|
|
if (typeof date === "string") return date.slice(0, 10);
|
|
return date.toISOString().slice(0, 10);
|
|
};
|
|
|
|
const defaultPurchaseDate =
|
|
getDateString(itemToProcess?.notificationTimestamp) ?? null;
|
|
|
|
const defaultName = itemToProcess?.parsedName
|
|
? itemToProcess.parsedName
|
|
.toLowerCase()
|
|
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
: null;
|
|
|
|
const defaultAmount = itemToProcess?.parsedAmount
|
|
? String(Math.abs(Number(itemToProcess.parsedAmount)))
|
|
: null;
|
|
|
|
// Match sourceAppName with a cartão to pre-fill card select
|
|
const matchedCartaoId = useMemo(() => {
|
|
const appName = itemToProcess?.sourceAppName?.toLowerCase();
|
|
if (!appName) return null;
|
|
|
|
for (const option of cartaoOptions) {
|
|
const label = option.label.toLowerCase();
|
|
if (label.includes(appName) || appName.includes(label)) {
|
|
return option.value;
|
|
}
|
|
}
|
|
return null;
|
|
}, [itemToProcess?.sourceAppName, cartaoOptions]);
|
|
|
|
const renderEmptyState = (message: string) => (
|
|
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
|
<EmptyState
|
|
media={<RiInboxLine className="size-6 text-primary" />}
|
|
title={message}
|
|
description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui. Saiba mais em Ajustes > Companion."
|
|
/>
|
|
</Card>
|
|
);
|
|
|
|
const renderGrid = (list: InboxItem[], readonly?: boolean) =>
|
|
list.length === 0 ? (
|
|
renderEmptyState(
|
|
readonly
|
|
? "Nenhuma notificação nesta aba"
|
|
: "Nenhum pré-lançamento pendente",
|
|
)
|
|
) : (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{list.map((item) => (
|
|
<InboxCard
|
|
key={item.id}
|
|
item={item}
|
|
readonly={readonly}
|
|
appLogoMap={appLogoMap}
|
|
onProcess={readonly ? undefined : handleProcessRequest}
|
|
onDiscard={readonly ? undefined : handleDiscardRequest}
|
|
onViewDetails={readonly ? undefined : handleDetailsRequest}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Tabs defaultValue="pending" className="w-full">
|
|
<TabsList>
|
|
<TabsTrigger value="pending">
|
|
Pendentes ({pendingItems.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="processed">
|
|
Processados ({processedItems.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="discarded">
|
|
Descartados ({discardedItems.length})
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="pending" className="mt-4">
|
|
{renderGrid(sortedPending)}
|
|
</TabsContent>
|
|
<TabsContent value="processed" className="mt-4">
|
|
{renderGrid(sortedProcessed, true)}
|
|
</TabsContent>
|
|
<TabsContent value="discarded" className="mt-4">
|
|
{renderGrid(sortedDiscarded, true)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<LancamentoDialog
|
|
mode="create"
|
|
open={processOpen}
|
|
onOpenChange={handleProcessOpenChange}
|
|
pagadorOptions={pagadorOptions}
|
|
splitPagadorOptions={splitPagadorOptions}
|
|
defaultPagadorId={defaultPagadorId}
|
|
contaOptions={contaOptions}
|
|
cartaoOptions={cartaoOptions}
|
|
categoriaOptions={categoriaOptions}
|
|
estabelecimentos={estabelecimentos}
|
|
defaultPurchaseDate={defaultPurchaseDate}
|
|
defaultName={defaultName}
|
|
defaultAmount={defaultAmount}
|
|
defaultCartaoId={matchedCartaoId}
|
|
defaultPaymentMethod={matchedCartaoId ? "Cartão de crédito" : null}
|
|
forceShowTransactionType
|
|
onSuccess={handleLancamentoSuccess}
|
|
/>
|
|
|
|
<InboxDetailsDialog
|
|
open={detailsOpen}
|
|
onOpenChange={handleDetailsOpenChange}
|
|
item={itemDetails}
|
|
/>
|
|
|
|
<ConfirmActionDialog
|
|
open={discardOpen}
|
|
onOpenChange={handleDiscardOpenChange}
|
|
title="Descartar notificação?"
|
|
description="A notificação será marcada como descartada e não aparecerá mais na lista de pendentes."
|
|
confirmLabel="Descartar"
|
|
confirmVariant="destructive"
|
|
pendingLabel="Descartando..."
|
|
onConfirm={handleDiscardConfirm}
|
|
/>
|
|
</>
|
|
);
|
|
}
|