mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat: amplia ações e seleção em lote no inbox
This commit is contained in:
@@ -4,26 +4,30 @@ import { Skeleton } from "@/shared/components/ui/skeleton";
|
|||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col items-start gap-6">
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="w-full space-y-4">
|
||||||
<div className="flex justify-between">
|
{/* Tabs */}
|
||||||
<Skeleton className="h-10 w-48" />
|
<Skeleton className="h-9 w-72 rounded-md bg-foreground/10" />
|
||||||
<Skeleton className="h-10 w-32" />
|
|
||||||
</div>
|
{/* Grid de cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<Card key={i} className="p-4">
|
<Card key={i} className="flex h-54 flex-col gap-0 py-0">
|
||||||
<div className="space-y-3">
|
<div className="px-4 pt-4 pb-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Skeleton className="h-5 w-24" />
|
<Skeleton className="h-4 w-32 bg-foreground/10" />
|
||||||
<Skeleton className="h-5 w-16" />
|
<Skeleton className="h-4 w-16 bg-foreground/10" />
|
||||||
</div>
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-3/4" />
|
|
||||||
<div className="flex gap-2 pt-2">
|
|
||||||
<Skeleton className="h-8 w-20" />
|
|
||||||
<Skeleton className="h-8 w-20" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 space-y-2 px-4 py-2">
|
||||||
|
<Skeleton className="h-3 w-full bg-foreground/10" />
|
||||||
|
<Skeleton className="h-3 w-full bg-foreground/10" />
|
||||||
|
<Skeleton className="h-3 w-3/4 bg-foreground/10" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 px-4 pb-4 pt-3">
|
||||||
|
<Skeleton className="h-8 flex-1 bg-foreground/10" />
|
||||||
|
<Skeleton className="h-8 w-8 bg-foreground/10" />
|
||||||
|
<Skeleton className="h-8 w-8 bg-foreground/10" />
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
RiFileList2Line,
|
RiFileList2Line,
|
||||||
RiMoreLine,
|
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
@@ -15,22 +14,42 @@ import { Badge } from "@/shared/components/ui/badge";
|
|||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/shared/components/ui/card";
|
} from "@/shared/components/ui/card";
|
||||||
import { Checkbox } from "@/shared/components/ui/checkbox";
|
import { Checkbox } from "@/shared/components/ui/checkbox";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/shared/components/ui/dropdown-menu";
|
|
||||||
import { resolveLogoSrc } from "@/shared/lib/logo";
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
||||||
import type { InboxItem } from "./types";
|
import type { InboxItem } from "./types";
|
||||||
|
|
||||||
|
// O timestamp vem do app Android em horário local mas salvo como UTC.
|
||||||
|
// Adicionamos o offset de Brasília para corrigir o cálculo de "há X tempo".
|
||||||
|
const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function adjustToBrasilia(date: Date): Date {
|
||||||
|
return new Date(date.getTime() + BRASILIA_OFFSET_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMatchingLogo(
|
||||||
|
sourceAppName: string | null,
|
||||||
|
appLogoMap: Record<string, string>,
|
||||||
|
): string | null {
|
||||||
|
if (!sourceAppName) return null;
|
||||||
|
|
||||||
|
const appName = sourceAppName.toLowerCase();
|
||||||
|
|
||||||
|
if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]);
|
||||||
|
|
||||||
|
for (const [name, logo] of Object.entries(appLogoMap)) {
|
||||||
|
if (name.includes(appName) || appName.includes(name)) {
|
||||||
|
return resolveLogoSrc(logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
interface InboxCardProps {
|
interface InboxCardProps {
|
||||||
item: InboxItem;
|
item: InboxItem;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -44,27 +63,6 @@ interface InboxCardProps {
|
|||||||
onSelectToggle?: (id: string) => void;
|
onSelectToggle?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMatchingLogo(
|
|
||||||
sourceAppName: string | null,
|
|
||||||
appLogoMap: Record<string, string>,
|
|
||||||
): string | null {
|
|
||||||
if (!sourceAppName) return null;
|
|
||||||
|
|
||||||
const appName = sourceAppName.toLowerCase();
|
|
||||||
|
|
||||||
// Exact match first
|
|
||||||
if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]);
|
|
||||||
|
|
||||||
// Partial match: card/account name contains app name or vice versa
|
|
||||||
for (const [name, logo] of Object.entries(appLogoMap)) {
|
|
||||||
if (name.includes(appName) || appName.includes(name)) {
|
|
||||||
return resolveLogoSrc(logo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InboxCard({
|
export function InboxCard({
|
||||||
item,
|
item,
|
||||||
readonly,
|
readonly,
|
||||||
@@ -83,28 +81,14 @@ export function InboxCard({
|
|||||||
|
|
||||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||||
|
|
||||||
// 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);
|
const rawDate = new Date(item.notificationTimestamp);
|
||||||
|
const notificationDate = adjustToBrasilia(rawDate);
|
||||||
// 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, {
|
const timeAgo = formatDistanceToNow(notificationDate, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: ptBR,
|
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);
|
|
||||||
|
|
||||||
const statusDate =
|
const statusDate =
|
||||||
item.status === "processed"
|
item.status === "processed"
|
||||||
? item.processedAt
|
? item.processedAt
|
||||||
@@ -118,12 +102,11 @@ export function InboxCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`flex flex-col gap-0 py-0 h-54 transition-colors ${selected ? "ring-2 ring-primary" : ""}`}
|
className={`flex h-54 flex-col gap-0 py-0 transition-colors ${selected ? "ring-2 ring-primary" : ""}`}
|
||||||
>
|
>
|
||||||
{/* Header com app e valor */}
|
|
||||||
<CardHeader className="pt-4">
|
<CardHeader className="pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<CardTitle className="flex items-center gap-1.5 text-md">
|
<CardTitle className="flex min-w-0 items-center gap-1.5 text-sm">
|
||||||
{matchedLogo && (
|
{matchedLogo && (
|
||||||
<Image
|
<Image
|
||||||
src={matchedLogo}
|
src={matchedLogo}
|
||||||
@@ -133,64 +116,30 @@ export function InboxCard({
|
|||||||
className="shrink-0 rounded-full"
|
className="shrink-0 rounded-full"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.sourceAppName || item.sourceApp}
|
<span className="truncate">
|
||||||
{" "}
|
{item.sourceAppName || item.sourceApp}
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
</span>
|
||||||
|
<span className="shrink-0 text-xs font-normal text-muted-foreground">
|
||||||
{timeAgo}
|
{timeAgo}
|
||||||
</span>
|
</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{amount !== null && (
|
{amount !== null && (
|
||||||
<MoneyValues amount={amount} className="text-sm" />
|
<MoneyValues amount={amount} className="shrink-0 text-sm" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!readonly && (
|
|
||||||
<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)}>
|
|
||||||
<RiFileList2Line 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>
|
</CardHeader>
|
||||||
|
|
||||||
{/* Conteúdo da notificação */}
|
|
||||||
<CardContent className="flex-1 py-2">
|
<CardContent className="flex-1 py-2">
|
||||||
{item.originalTitle && (
|
{item.originalTitle && (
|
||||||
<p className="mb-1 text-sm font-bold">{item.originalTitle}</p>
|
<p className="mb-1 text-sm font-bold">{item.originalTitle}</p>
|
||||||
)}
|
)}
|
||||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground line-clamp-4">
|
<p className="line-clamp-4 whitespace-pre-wrap text-sm text-muted-foreground">
|
||||||
{item.originalText}
|
{item.originalText}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* Botões de ação ou badge de status */}
|
|
||||||
{readonly ? (
|
{readonly ? (
|
||||||
<CardFooter className="gap-2 pt-3 pb-4">
|
<CardFooter className="gap-2 pb-4 pt-3">
|
||||||
<Badge
|
<Badge
|
||||||
variant={item.status === "processed" ? "default" : "secondary"}
|
variant={item.status === "processed" ? "default" : "secondary"}
|
||||||
>
|
>
|
||||||
@@ -201,7 +150,7 @@ export function InboxCard({
|
|||||||
{formattedStatusDate}
|
{formattedStatusDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{item.status === "discarded" && onRestoreToPending && (
|
{item.status === "discarded" && onRestoreToPending && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -220,6 +169,7 @@ export function InboxCard({
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="text-muted-foreground hover:text-destructive"
|
className="text-muted-foreground hover:text-destructive"
|
||||||
onClick={() => onDelete(item)}
|
onClick={() => onDelete(item)}
|
||||||
|
aria-label="Excluir notificação"
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="size-4" />
|
<RiDeleteBinLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -234,7 +184,7 @@ export function InboxCard({
|
|||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
) : (
|
) : (
|
||||||
<CardFooter className="gap-2 pt-3 pb-4">
|
<CardFooter className="gap-2 pb-4 pt-3">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -243,6 +193,16 @@ export function InboxCard({
|
|||||||
<RiCheckLine className="mr-1.5 size-4" />
|
<RiCheckLine className="mr-1.5 size-4" />
|
||||||
Processar
|
Processar
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onViewDetails?.(item)}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
aria-label="Ver detalhes"
|
||||||
|
title="Ver detalhes"
|
||||||
|
>
|
||||||
|
<RiFileList2Line className="size-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -16,20 +16,29 @@ import {
|
|||||||
import { Separator } from "@/shared/components/ui/separator";
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
import type { InboxItem } from "./types";
|
import type { InboxItem } from "./types";
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
pending: "Pendente",
|
||||||
|
processed: "Processado",
|
||||||
|
discarded: "Descartado",
|
||||||
|
};
|
||||||
|
|
||||||
interface InboxDetailsDialogProps {
|
interface InboxDetailsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
item: InboxItem | null;
|
item: InboxItem | null;
|
||||||
|
onProcess?: (item: InboxItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InboxDetailsDialog({
|
export function InboxDetailsDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
item,
|
item,
|
||||||
|
onProcess,
|
||||||
}: InboxDetailsDialogProps) {
|
}: InboxDetailsDialogProps) {
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
|
||||||
|
const isPending = item.status === "pending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -39,32 +48,21 @@ export function InboxDetailsDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Dados da fonte */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="grid gap-2 text-sm">
|
<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">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">App</span>
|
<span className="text-muted-foreground">App</span>
|
||||||
<span>{item.sourceAppName || item.sourceApp}</span>
|
<span>{item.sourceAppName || item.sourceApp}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Texto original */}
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-1 text-sm font-medium text-muted-foreground">
|
<h4 className="mb-1 text-sm font-medium text-muted-foreground">
|
||||||
Notificação Original
|
Notificação Original
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{item.originalTitle && (
|
{item.originalTitle && (
|
||||||
<p className="mb-1 font-medium">{item.originalTitle}</p>
|
<p className="mb-1 font-medium">{item.originalTitle}</p>
|
||||||
)}
|
)}
|
||||||
@@ -73,14 +71,13 @@ export function InboxDetailsDialog({
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Dados parseados */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="grid gap-2 text-sm">
|
<div className="grid gap-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Estabelecimento</span>
|
<span className="text-muted-foreground">Estabelecimento</span>
|
||||||
<span>{item.parsedName || "Não extraído"}</span>
|
<span>{item.parsedName || "Não extraído"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Valor</span>
|
<span className="text-muted-foreground">Valor</span>
|
||||||
{amount !== null ? (
|
{amount !== null ? (
|
||||||
<MoneyValues amount={amount} className="text-sm" />
|
<MoneyValues amount={amount} className="text-sm" />
|
||||||
@@ -93,12 +90,13 @@ export function InboxDetailsDialog({
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Metadados */}
|
|
||||||
<div>
|
<div>
|
||||||
<div className="grid gap-2 text-sm">
|
<div className="grid gap-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Status</span>
|
<span className="text-muted-foreground">Status</span>
|
||||||
<Badge variant="outline">{item.status}</Badge>
|
<Badge variant="outline">
|
||||||
|
{STATUS_LABELS[item.status] ?? item.status}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Criado em</span>
|
<span className="text-muted-foreground">Criado em</span>
|
||||||
@@ -111,8 +109,21 @@ export function InboxDetailsDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
{isPending && onProcess && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onProcess(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Processar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button">Entendi</Button>
|
<Button type="button" variant="outline">
|
||||||
|
Fechar
|
||||||
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -231,6 +231,29 @@ export function InboxPage({
|
|||||||
setIds(ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]);
|
setIds(ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allPendingSelected =
|
||||||
|
sortedPending.length > 0 &&
|
||||||
|
selectedPendingIds.length === sortedPending.length;
|
||||||
|
const allProcessedSelected =
|
||||||
|
sortedProcessed.length > 0 &&
|
||||||
|
selectedProcessedIds.length === sortedProcessed.length;
|
||||||
|
const allDiscardedSelected =
|
||||||
|
sortedDiscarded.length > 0 &&
|
||||||
|
selectedDiscardedIds.length === sortedDiscarded.length;
|
||||||
|
|
||||||
|
const toggleSelectAllPending = () => {
|
||||||
|
if (allPendingSelected) setSelectedPendingIds([]);
|
||||||
|
else setSelectedPendingIds(sortedPending.map((item) => item.id));
|
||||||
|
};
|
||||||
|
const toggleSelectAllProcessed = () => {
|
||||||
|
if (allProcessedSelected) setSelectedProcessedIds([]);
|
||||||
|
else setSelectedProcessedIds(sortedProcessed.map((item) => item.id));
|
||||||
|
};
|
||||||
|
const toggleSelectAllDiscarded = () => {
|
||||||
|
if (allDiscardedSelected) setSelectedDiscardedIds([]);
|
||||||
|
else setSelectedDiscardedIds(sortedDiscarded.map((item) => item.id));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectionBulkRequest = (
|
const handleSelectionBulkRequest = (
|
||||||
status: "pending" | "processed" | "discarded",
|
status: "pending" | "processed" | "discarded",
|
||||||
) => {
|
) => {
|
||||||
@@ -412,16 +435,27 @@ export function InboxPage({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="pending" className="mt-4">
|
<TabsContent value="pending" className="mt-4">
|
||||||
{selectedPendingIds.length > 0 && (
|
{sortedPending.length > 0 && (
|
||||||
<div className="mb-4 flex justify-end">
|
<div className="mb-4 flex items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleSelectionBulkRequest("pending")}
|
onClick={toggleSelectAllPending}
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="mr-1.5 size-4" />
|
{allPendingSelected
|
||||||
Descartar selecionados ({selectedPendingIds.length})
|
? "Desselecionar todos"
|
||||||
|
: "Selecionar todos"}
|
||||||
</Button>
|
</Button>
|
||||||
|
{selectedPendingIds.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSelectionBulkRequest("pending")}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="mr-1.5 size-4" />
|
||||||
|
Descartar selecionados ({selectedPendingIds.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderGrid(sortedPending, false, selectedPendingIds, (id) =>
|
{renderGrid(sortedPending, false, selectedPendingIds, (id) =>
|
||||||
@@ -431,6 +465,15 @@ export function InboxPage({
|
|||||||
<TabsContent value="processed" className="mt-4">
|
<TabsContent value="processed" className="mt-4">
|
||||||
{sortedProcessed.length > 0 && (
|
{sortedProcessed.length > 0 && (
|
||||||
<div className="mb-4 flex items-center justify-end gap-2">
|
<div className="mb-4 flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleSelectAllProcessed}
|
||||||
|
>
|
||||||
|
{allProcessedSelected
|
||||||
|
? "Desselecionar todos"
|
||||||
|
: "Selecionar todos"}
|
||||||
|
</Button>
|
||||||
{selectedProcessedIds.length > 0 && (
|
{selectedProcessedIds.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -458,6 +501,15 @@ export function InboxPage({
|
|||||||
<TabsContent value="discarded" className="mt-4">
|
<TabsContent value="discarded" className="mt-4">
|
||||||
{sortedDiscarded.length > 0 && (
|
{sortedDiscarded.length > 0 && (
|
||||||
<div className="mb-4 flex items-center justify-end gap-2">
|
<div className="mb-4 flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleSelectAllDiscarded}
|
||||||
|
>
|
||||||
|
{allDiscardedSelected
|
||||||
|
? "Desselecionar todos"
|
||||||
|
: "Selecionar todos"}
|
||||||
|
</Button>
|
||||||
{selectedDiscardedIds.length > 0 && (
|
{selectedDiscardedIds.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -500,6 +552,7 @@ export function InboxPage({
|
|||||||
defaultAmount={defaultAmount}
|
defaultAmount={defaultAmount}
|
||||||
defaultCardId={matchedCartaoId}
|
defaultCardId={matchedCartaoId}
|
||||||
defaultPaymentMethod={matchedCartaoId ? "Cartão de crédito" : null}
|
defaultPaymentMethod={matchedCartaoId ? "Cartão de crédito" : null}
|
||||||
|
defaultTransactionType="Despesa"
|
||||||
forceShowTransactionType
|
forceShowTransactionType
|
||||||
onSuccess={handleLancamentoSuccess}
|
onSuccess={handleLancamentoSuccess}
|
||||||
/>
|
/>
|
||||||
@@ -508,6 +561,7 @@ export function InboxPage({
|
|||||||
open={detailsOpen}
|
open={detailsOpen}
|
||||||
onOpenChange={handleDetailsOpenChange}
|
onOpenChange={handleDetailsOpenChange}
|
||||||
item={itemDetails}
|
item={itemDetails}
|
||||||
|
onProcess={handleProcessRequest}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmActionDialog
|
<ConfirmActionDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user