refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,153 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import MoneyValues from "@/components/money-values";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiDeleteBinLine,
|
||||
RiEyeLine,
|
||||
RiMoreLine,
|
||||
RiCheckLine,
|
||||
RiDeleteBinLine,
|
||||
RiEyeLine,
|
||||
RiMoreLine,
|
||||
} from "@remixicon/react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import MoneyValues from "@/components/money-values";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import type { InboxItem } from "./types";
|
||||
|
||||
interface InboxCardProps {
|
||||
item: InboxItem;
|
||||
onProcess: (item: InboxItem) => void;
|
||||
onDiscard: (item: InboxItem) => void;
|
||||
onViewDetails: (item: InboxItem) => void;
|
||||
item: InboxItem;
|
||||
onProcess: (item: InboxItem) => void;
|
||||
onDiscard: (item: InboxItem) => void;
|
||||
onViewDetails: (item: InboxItem) => void;
|
||||
}
|
||||
|
||||
export function InboxCard({
|
||||
item,
|
||||
onProcess,
|
||||
onDiscard,
|
||||
onViewDetails,
|
||||
item,
|
||||
onProcess,
|
||||
onDiscard,
|
||||
onViewDetails,
|
||||
}: InboxCardProps) {
|
||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||
const isReceita = item.parsedTransactionType === "Receita";
|
||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||
const isReceita = item.parsedTransactionType === "Receita";
|
||||
|
||||
// O timestamp vem do app Android em horário local mas salvo como UTC
|
||||
// Precisamos interpretar o valor UTC como se fosse horário de Brasília
|
||||
const rawDate = new Date(item.notificationTimestamp);
|
||||
// O timestamp vem do app Android em horário local mas salvo como UTC
|
||||
// Precisamos interpretar o valor UTC como se fosse horário de Brasília
|
||||
const rawDate = new Date(item.notificationTimestamp);
|
||||
|
||||
// Ajusta adicionando o offset de Brasília (3 horas) para corrigir o cálculo do "há X tempo"
|
||||
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
|
||||
const notificationDate = new Date(rawDate.getTime() + BRASILIA_OFFSET_MS);
|
||||
// Ajusta adicionando o offset de Brasília (3 horas) para corrigir o cálculo do "há X tempo"
|
||||
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
|
||||
const notificationDate = new Date(rawDate.getTime() + BRASILIA_OFFSET_MS);
|
||||
|
||||
const timeAgo = formatDistanceToNow(notificationDate, {
|
||||
addSuffix: true,
|
||||
locale: ptBR,
|
||||
});
|
||||
const timeAgo = formatDistanceToNow(notificationDate, {
|
||||
addSuffix: true,
|
||||
locale: ptBR,
|
||||
});
|
||||
|
||||
// Para exibição, usa UTC pois o valor já representa horário de Brasília
|
||||
const formattedTime = new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZone: "UTC",
|
||||
}).format(rawDate);
|
||||
// Para exibição, usa UTC pois o valor já representa horário de Brasília
|
||||
const _formattedTime = new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZone: "UTC",
|
||||
}).format(rawDate);
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-0 py-0 h-54">
|
||||
{/* Header com app e valor */}
|
||||
<CardHeader className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-md">
|
||||
{item.sourceAppName || item.sourceApp}
|
||||
{" "}
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{timeAgo}
|
||||
</span>
|
||||
</CardTitle>
|
||||
{amount !== null && (
|
||||
<MoneyValues
|
||||
amount={isReceita ? amount : -amount}
|
||||
showPositiveSign={isReceita}
|
||||
className={cn(
|
||||
"text-sm",
|
||||
isReceita
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-foreground"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<Card className="flex flex-col gap-0 py-0 h-54">
|
||||
{/* Header com app e valor */}
|
||||
<CardHeader className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-md">
|
||||
{item.sourceAppName || item.sourceApp}
|
||||
{" "}
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{timeAgo}
|
||||
</span>
|
||||
</CardTitle>
|
||||
{amount !== null && (
|
||||
<MoneyValues
|
||||
amount={isReceita ? amount : -amount}
|
||||
showPositiveSign={isReceita}
|
||||
className={cn(
|
||||
"text-sm",
|
||||
isReceita
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-foreground",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 -mr-2 -mt-1"
|
||||
>
|
||||
<RiMoreLine className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onViewDetails(item)}>
|
||||
<RiEyeLine className="mr-2 size-4" />
|
||||
Ver detalhes
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onProcess(item)}>
|
||||
<RiCheckLine className="mr-2 size-4" />
|
||||
Processar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDiscard(item)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="mr-2 size-4" />
|
||||
Descartar
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 -mr-2 -mt-1"
|
||||
>
|
||||
<RiMoreLine className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onViewDetails(item)}>
|
||||
<RiEyeLine className="mr-2 size-4" />
|
||||
Ver detalhes
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onProcess(item)}>
|
||||
<RiCheckLine className="mr-2 size-4" />
|
||||
Processar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDiscard(item)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="mr-2 size-4" />
|
||||
Descartar
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
|
||||
{/* Conteúdo da notificação */}
|
||||
<CardContent className="flex-1 py-2">
|
||||
{item.originalTitle && (
|
||||
<p className="mb-1 text-sm font-bold">{item.originalTitle}</p>
|
||||
)}
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground line-clamp-4">
|
||||
{item.originalText}
|
||||
</p>
|
||||
</CardContent>
|
||||
{/* Conteúdo da notificação */}
|
||||
<CardContent className="flex-1 py-2">
|
||||
{item.originalTitle && (
|
||||
<p className="mb-1 text-sm font-bold">{item.originalTitle}</p>
|
||||
)}
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground line-clamp-4">
|
||||
{item.originalText}
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
{/* Botões de ação */}
|
||||
<CardFooter className="gap-2 pt-3 pb-4">
|
||||
<Button size="sm" className="flex-1" onClick={() => onProcess(item)}>
|
||||
<RiCheckLine className="mr-1.5 size-4" />
|
||||
Processar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onDiscard(item)}
|
||||
className="text-muted-foreground hover:text-destructive hover:border-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="size-4" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
{/* Botões de ação */}
|
||||
<CardFooter className="gap-2 pt-3 pb-4">
|
||||
<Button size="sm" className="flex-1" onClick={() => onProcess(item)}>
|
||||
<RiCheckLine className="mr-1.5 size-4" />
|
||||
Processar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onDiscard(item)}
|
||||
className="text-muted-foreground hover:text-destructive hover:border-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="size-4" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,145 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import MoneyValues from "@/components/money-values";
|
||||
import { TypeBadge } from "@/components/type-badge";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import type { InboxItem } from "./types";
|
||||
|
||||
interface InboxDetailsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
item: InboxItem | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
item: InboxItem | null;
|
||||
}
|
||||
|
||||
export function InboxDetailsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
item,
|
||||
open,
|
||||
onOpenChange,
|
||||
item,
|
||||
}: InboxDetailsDialogProps) {
|
||||
if (!item) return null;
|
||||
if (!item) return null;
|
||||
|
||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||
const isReceita = item.parsedTransactionType === "Receita";
|
||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||
const isReceita = item.parsedTransactionType === "Receita";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detalhes da Notificação</DialogTitle>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detalhes da Notificação</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Dados da fonte */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">ID</span>
|
||||
<span className="font-mono text-xs">{item.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">App</span>
|
||||
<span>{item.sourceAppName || item.sourceApp}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Package</span>
|
||||
<span className="font-mono text-xs">{item.sourceApp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* Dados da fonte */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">ID</span>
|
||||
<span className="font-mono text-xs">{item.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">App</span>
|
||||
<span>{item.sourceAppName || item.sourceApp}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Package</span>
|
||||
<span className="font-mono text-xs">{item.sourceApp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
{/* Texto original */}
|
||||
<div>
|
||||
<h4 className="mb-1 text-sm font-medium text-muted-foreground">
|
||||
Notificação Original
|
||||
</h4>
|
||||
{/* Texto original */}
|
||||
<div>
|
||||
<h4 className="mb-1 text-sm font-medium text-muted-foreground">
|
||||
Notificação Original
|
||||
</h4>
|
||||
|
||||
{item.originalTitle && (
|
||||
<p className="mb-1 font-medium">{item.originalTitle}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.originalText}</p>
|
||||
</div>
|
||||
{item.originalTitle && (
|
||||
<p className="mb-1 font-medium">{item.originalTitle}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.originalText}</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
{/* Dados parseados */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Estabelecimento</span>
|
||||
<span>{item.parsedName || "Não extraído"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Valor</span>
|
||||
{amount !== null ? (
|
||||
<MoneyValues
|
||||
amount={isReceita ? amount : -amount}
|
||||
showPositiveSign={isReceita}
|
||||
className={cn(
|
||||
"text-sm",
|
||||
isReceita
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-foreground",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Não extraído</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Tipo</span>
|
||||
{item.parsedTransactionType ? (
|
||||
<TypeBadge type={item.parsedTransactionType} />
|
||||
) : (
|
||||
<span className="text-muted-foreground">
|
||||
Não identificado
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Dados parseados */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Estabelecimento</span>
|
||||
<span>{item.parsedName || "Não extraído"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Valor</span>
|
||||
{amount !== null ? (
|
||||
<MoneyValues
|
||||
amount={isReceita ? amount : -amount}
|
||||
showPositiveSign={isReceita}
|
||||
className={cn(
|
||||
"text-sm",
|
||||
isReceita
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-foreground",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Não extraído</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Tipo</span>
|
||||
{item.parsedTransactionType ? (
|
||||
<TypeBadge type={item.parsedTransactionType} />
|
||||
) : (
|
||||
<span className="text-muted-foreground">
|
||||
Não identificado
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
{/* Metadados */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<Badge variant="outline">{item.status}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Criado em</span>
|
||||
<span>
|
||||
{format(new Date(item.createdAt), "PPpp", { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metadados */}
|
||||
<div>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<Badge variant="outline">{item.status}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Criado em</span>
|
||||
<span>
|
||||
{format(new Date(item.createdAt), "PPpp", { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button className="w-full mt-2" type="button">
|
||||
Entendi
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button className="w-full mt-2" type="button">
|
||||
Entendi
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,208 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import { RiInboxLine } from "@remixicon/react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
discardInboxItemAction,
|
||||
markInboxAsProcessedAction,
|
||||
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 { RiInboxLine } from "@remixicon/react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { InboxCard } from "./inbox-card";
|
||||
import { InboxDetailsDialog } from "./inbox-details-dialog";
|
||||
import type { InboxItem, SelectOption } from "./types";
|
||||
|
||||
interface InboxPageProps {
|
||||
items: InboxItem[];
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
items: InboxItem[];
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
}
|
||||
|
||||
export function InboxPage({
|
||||
items,
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
estabelecimentos,
|
||||
items,
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
estabelecimentos,
|
||||
}: InboxPageProps) {
|
||||
const [processOpen, setProcessOpen] = useState(false);
|
||||
const [itemToProcess, setItemToProcess] = useState<InboxItem | null>(null);
|
||||
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 [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [itemDetails, setItemDetails] = useState<InboxItem | null>(null);
|
||||
|
||||
const [discardOpen, setDiscardOpen] = useState(false);
|
||||
const [itemToDiscard, setItemToDiscard] = useState<InboxItem | null>(null);
|
||||
const [discardOpen, setDiscardOpen] = useState(false);
|
||||
const [itemToDiscard, setItemToDiscard] = useState<InboxItem | null>(null);
|
||||
|
||||
const sortedItems = useMemo(
|
||||
() =>
|
||||
[...items].sort(
|
||||
(a, b) =>
|
||||
new Date(b.notificationTimestamp).getTime() -
|
||||
new Date(a.notificationTimestamp).getTime(),
|
||||
),
|
||||
[items],
|
||||
);
|
||||
const sortedItems = useMemo(
|
||||
() =>
|
||||
[...items].sort(
|
||||
(a, b) =>
|
||||
new Date(b.notificationTimestamp).getTime() -
|
||||
new Date(a.notificationTimestamp).getTime(),
|
||||
),
|
||||
[items],
|
||||
);
|
||||
|
||||
const handleProcessOpenChange = useCallback((open: boolean) => {
|
||||
setProcessOpen(open);
|
||||
if (!open) {
|
||||
setItemToProcess(null);
|
||||
}
|
||||
}, []);
|
||||
const handleProcessOpenChange = useCallback((open: boolean) => {
|
||||
setProcessOpen(open);
|
||||
if (!open) {
|
||||
setItemToProcess(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDetailsOpenChange = useCallback((open: boolean) => {
|
||||
setDetailsOpen(open);
|
||||
if (!open) {
|
||||
setItemDetails(null);
|
||||
}
|
||||
}, []);
|
||||
const handleDetailsOpenChange = useCallback((open: boolean) => {
|
||||
setDetailsOpen(open);
|
||||
if (!open) {
|
||||
setItemDetails(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDiscardOpenChange = useCallback((open: boolean) => {
|
||||
setDiscardOpen(open);
|
||||
if (!open) {
|
||||
setItemToDiscard(null);
|
||||
}
|
||||
}, []);
|
||||
const handleDiscardOpenChange = useCallback((open: boolean) => {
|
||||
setDiscardOpen(open);
|
||||
if (!open) {
|
||||
setItemToDiscard(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleProcessRequest = useCallback((item: InboxItem) => {
|
||||
setItemToProcess(item);
|
||||
setProcessOpen(true);
|
||||
}, []);
|
||||
const handleProcessRequest = useCallback((item: InboxItem) => {
|
||||
setItemToProcess(item);
|
||||
setProcessOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDetailsRequest = useCallback((item: InboxItem) => {
|
||||
setItemDetails(item);
|
||||
setDetailsOpen(true);
|
||||
}, []);
|
||||
const handleDetailsRequest = useCallback((item: InboxItem) => {
|
||||
setItemDetails(item);
|
||||
setDetailsOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDiscardRequest = useCallback((item: InboxItem) => {
|
||||
setItemToDiscard(item);
|
||||
setDiscardOpen(true);
|
||||
}, []);
|
||||
const handleDiscardRequest = useCallback((item: InboxItem) => {
|
||||
setItemToDiscard(item);
|
||||
setDiscardOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDiscardConfirm = useCallback(async () => {
|
||||
if (!itemToDiscard) return;
|
||||
const handleDiscardConfirm = useCallback(async () => {
|
||||
if (!itemToDiscard) return;
|
||||
|
||||
const result = await discardInboxItemAction({
|
||||
inboxItemId: itemToDiscard.id,
|
||||
});
|
||||
const result = await discardInboxItemAction({
|
||||
inboxItemId: itemToDiscard.id,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
return;
|
||||
}
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}, [itemToDiscard]);
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}, [itemToDiscard]);
|
||||
|
||||
const handleLancamentoSuccess = useCallback(async () => {
|
||||
if (!itemToProcess) return;
|
||||
const handleLancamentoSuccess = useCallback(async () => {
|
||||
if (!itemToProcess) return;
|
||||
|
||||
const result = await markInboxAsProcessedAction({
|
||||
inboxItemId: itemToProcess.id,
|
||||
});
|
||||
const result = await markInboxAsProcessedAction({
|
||||
inboxItemId: itemToProcess.id,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Notificação processada!");
|
||||
} else {
|
||||
toast.error(result.error);
|
||||
}
|
||||
}, [itemToProcess]);
|
||||
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);
|
||||
};
|
||||
// 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 defaultPurchaseDate =
|
||||
getDateString(itemToProcess?.notificationTimestamp) ?? null;
|
||||
|
||||
const defaultName = itemToProcess?.parsedName ?? null;
|
||||
const defaultName = itemToProcess?.parsedName ?? null;
|
||||
|
||||
const defaultAmount = itemToProcess?.parsedAmount
|
||||
? String(Math.abs(Number(itemToProcess.parsedAmount)))
|
||||
: null;
|
||||
const defaultAmount = itemToProcess?.parsedAmount
|
||||
? String(Math.abs(Number(itemToProcess.parsedAmount)))
|
||||
: null;
|
||||
|
||||
const defaultTransactionType =
|
||||
itemToProcess?.parsedTransactionType === "Receita" ? "Receita" : "Despesa";
|
||||
const defaultTransactionType =
|
||||
itemToProcess?.parsedTransactionType === "Receita" ? "Receita" : "Despesa";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{sortedItems.length === 0 ? (
|
||||
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
media={<RiInboxLine className="size-6 text-primary" />}
|
||||
title="Nenhum pré-lançamento"
|
||||
description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui para você processar."
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{sortedItems.map((item) => (
|
||||
<InboxCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onProcess={handleProcessRequest}
|
||||
onDiscard={handleDiscardRequest}
|
||||
onViewDetails={handleDetailsRequest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{sortedItems.length === 0 ? (
|
||||
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
media={<RiInboxLine className="size-6 text-primary" />}
|
||||
title="Nenhum pré-lançamento"
|
||||
description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui para você processar."
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{sortedItems.map((item) => (
|
||||
<InboxCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onProcess={handleProcessRequest}
|
||||
onDiscard={handleDiscardRequest}
|
||||
onViewDetails={handleDetailsRequest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
defaultTransactionType={defaultTransactionType}
|
||||
forceShowTransactionType
|
||||
onSuccess={handleLancamentoSuccess}
|
||||
/>
|
||||
<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}
|
||||
defaultTransactionType={defaultTransactionType}
|
||||
forceShowTransactionType
|
||||
onSuccess={handleLancamentoSuccess}
|
||||
/>
|
||||
|
||||
<InboxDetailsDialog
|
||||
open={detailsOpen}
|
||||
onOpenChange={handleDetailsOpenChange}
|
||||
item={itemDetails}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,39 +5,39 @@
|
||||
import type { SelectOption as LancamentoSelectOption } from "@/components/lancamentos/types";
|
||||
|
||||
export interface InboxItem {
|
||||
id: string;
|
||||
sourceApp: string;
|
||||
sourceAppName: string | null;
|
||||
originalTitle: string | null;
|
||||
originalText: string;
|
||||
notificationTimestamp: Date;
|
||||
parsedName: string | null;
|
||||
parsedAmount: string | null;
|
||||
parsedTransactionType: string | null;
|
||||
status: string;
|
||||
lancamentoId: string | null;
|
||||
processedAt: Date | null;
|
||||
discardedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
id: string;
|
||||
sourceApp: string;
|
||||
sourceAppName: string | null;
|
||||
originalTitle: string | null;
|
||||
originalText: string;
|
||||
notificationTimestamp: Date;
|
||||
parsedName: string | null;
|
||||
parsedAmount: string | null;
|
||||
parsedTransactionType: string | null;
|
||||
status: string;
|
||||
lancamentoId: string | null;
|
||||
processedAt: Date | null;
|
||||
discardedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ProcessInboxInput {
|
||||
inboxItemId: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
purchaseDate: string;
|
||||
transactionType: "Despesa" | "Receita";
|
||||
condition: string;
|
||||
paymentMethod: string;
|
||||
categoriaId: string;
|
||||
contaId?: string;
|
||||
cartaoId?: string;
|
||||
note?: string;
|
||||
inboxItemId: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
purchaseDate: string;
|
||||
transactionType: "Despesa" | "Receita";
|
||||
condition: string;
|
||||
paymentMethod: string;
|
||||
categoriaId: string;
|
||||
contaId?: string;
|
||||
cartaoId?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface DiscardInboxInput {
|
||||
inboxItemId: string;
|
||||
inboxItemId: string;
|
||||
}
|
||||
|
||||
// Re-export the lancamentos SelectOption for use in inbox components
|
||||
|
||||
Reference in New Issue
Block a user