mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
Padronização de peso tipográfico em títulos, rótulos de seção, nomes de entidades e valores monetários em toda a interface. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
266 lines
8.2 KiB
TypeScript
266 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
RiAttachmentLine,
|
|
RiFilePdf2Line,
|
|
RiImageLine,
|
|
} from "@remixicon/react";
|
|
import { useRouter } from "next/navigation";
|
|
import type React from "react";
|
|
import { useState, useTransition } from "react";
|
|
import { AttachmentGridItem } from "@/features/attachments/components/attachment-grid-item";
|
|
import { AttachmentPreview } from "@/features/attachments/components/attachment-preview";
|
|
import { useAttachmentUrl } from "@/features/attachments/hooks/use-attachment-url";
|
|
import type { AttachmentForPeriod } from "@/features/attachments/queries";
|
|
import { fetchTransactionByIdAction } from "@/features/transactions/actions/fetch-by-id";
|
|
import type { TransactionDialogOptions } from "@/features/transactions/actions/fetch-dialog-options";
|
|
import { fetchTransactionDialogOptionsAction } from "@/features/transactions/actions/fetch-dialog-options";
|
|
import { TransactionDetailsDialog } from "@/features/transactions/components/dialogs/transaction-details-dialog";
|
|
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
|
|
import type { TransactionItem } from "@/features/transactions/components/types";
|
|
import { EmptyState } from "@/shared/components/empty-state";
|
|
import { Card, CardContent } from "@/shared/components/ui/card";
|
|
import { cn } from "@/shared/utils/ui";
|
|
|
|
type FilterType = "all" | "images" | "pdfs";
|
|
|
|
function AttachmentGridItemWithUrl({
|
|
attachment,
|
|
onClick,
|
|
onDetails,
|
|
isLoadingDetails,
|
|
}: {
|
|
attachment: AttachmentForPeriod;
|
|
onClick: () => void;
|
|
onDetails: () => void;
|
|
isLoadingDetails: boolean;
|
|
}) {
|
|
const { url, containerRef } = useAttachmentUrl(attachment.attachmentId);
|
|
return (
|
|
<div ref={containerRef}>
|
|
<AttachmentGridItem
|
|
attachment={attachment}
|
|
url={url ?? undefined}
|
|
onClick={onClick}
|
|
onDetails={onDetails}
|
|
isLoadingDetails={isLoadingDetails}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const FILTERS: {
|
|
value: FilterType;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
}[] = [
|
|
{
|
|
value: "all",
|
|
label: "Todos",
|
|
icon: <RiAttachmentLine className="size-3.5" />,
|
|
},
|
|
{
|
|
value: "images",
|
|
label: "Imagens",
|
|
icon: <RiImageLine className="size-3.5 text-blue-500" />,
|
|
},
|
|
{
|
|
value: "pdfs",
|
|
label: "PDFs",
|
|
icon: <RiFilePdf2Line className="size-3.5 text-red-500" />,
|
|
},
|
|
];
|
|
|
|
interface AttachmentsPageProps {
|
|
attachments: AttachmentForPeriod[];
|
|
}
|
|
|
|
export function AttachmentsPage({ attachments }: AttachmentsPageProps) {
|
|
const router = useRouter();
|
|
const [filter, setFilter] = useState<FilterType>("all");
|
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
const [transactionDetails, setTransactionDetails] =
|
|
useState<TransactionItem | null>(null);
|
|
const [loadingTransactionId, setLoadingTransactionId] = useState<
|
|
string | null
|
|
>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
// Edit dialog state
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const [transactionToEdit, setTransactionToEdit] =
|
|
useState<TransactionItem | null>(null);
|
|
const [dialogOptions, setDialogOptions] =
|
|
useState<TransactionDialogOptions | null>(null);
|
|
|
|
const filteredAttachments = attachments.filter((a) => {
|
|
if (filter === "images") return a.mimeType.startsWith("image/");
|
|
if (filter === "pdfs") return a.mimeType === "application/pdf";
|
|
return true;
|
|
});
|
|
|
|
const imageCount = attachments.filter((a) =>
|
|
a.mimeType.startsWith("image/"),
|
|
).length;
|
|
const pdfCount = attachments.filter(
|
|
(a) => a.mimeType === "application/pdf",
|
|
).length;
|
|
|
|
const counts: Record<FilterType, number> = {
|
|
all: attachments.length,
|
|
images: imageCount,
|
|
pdfs: pdfCount,
|
|
};
|
|
|
|
function handleSelect(attachment: AttachmentForPeriod) {
|
|
const idx = filteredAttachments.findIndex(
|
|
(a) =>
|
|
a.attachmentId === attachment.attachmentId &&
|
|
a.transactionId === attachment.transactionId,
|
|
);
|
|
setSelectedIndex(idx);
|
|
}
|
|
|
|
function handleDetails(transactionId: string) {
|
|
setLoadingTransactionId(transactionId);
|
|
startTransition(async () => {
|
|
const transaction = await fetchTransactionByIdAction(transactionId);
|
|
setLoadingTransactionId(null);
|
|
if (transaction) setTransactionDetails(transaction);
|
|
});
|
|
}
|
|
|
|
function handleEdit(transaction: TransactionItem) {
|
|
setTransactionToEdit(transaction);
|
|
startTransition(async () => {
|
|
const options = await fetchTransactionDialogOptionsAction();
|
|
setDialogOptions(options);
|
|
setEditOpen(true);
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="w-full space-y-6">
|
|
<Card>
|
|
<CardContent>
|
|
{attachments.length === 0 ? (
|
|
<div className="flex w-full items-center justify-center py-12">
|
|
<EmptyState
|
|
media={<RiAttachmentLine className="size-6 text-primary" />}
|
|
title="Nenhum anexo neste mês"
|
|
description="Adicione comprovantes nos seus lançamentos para vê-los aqui."
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Header: filtros + contagem */}
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<p className="text-sm text-muted-foreground">
|
|
{filteredAttachments.length}{" "}
|
|
{filteredAttachments.length === 1 ? "anexo" : "anexos"}
|
|
{filter !== "all" &&
|
|
` · ${FILTERS.find((f) => f.value === filter)?.label.toLowerCase()}`}
|
|
</p>
|
|
<div className="flex items-center gap-1 rounded-lg border p-1">
|
|
{FILTERS.map(({ value, label, icon }) => (
|
|
<button
|
|
key={value}
|
|
type="button"
|
|
onClick={() => {
|
|
setFilter(value);
|
|
setSelectedIndex(-1);
|
|
}}
|
|
className={cn(
|
|
"flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors",
|
|
filter === value
|
|
? "bg-primary text-primary-foreground [&_svg]:opacity-100"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
<span className={cn(filter !== value && "opacity-60")}>
|
|
{icon}
|
|
</span>
|
|
{label}{" "}
|
|
<span
|
|
className={cn(
|
|
"tabular-nums",
|
|
filter === value ? "opacity-80" : "opacity-60",
|
|
)}
|
|
>
|
|
({counts[value]})
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{filteredAttachments.length === 0 ? (
|
|
<div className="flex w-full items-center justify-center py-12">
|
|
<EmptyState
|
|
media={<RiAttachmentLine className="size-6 text-primary" />}
|
|
title="Nenhum anexo encontrado"
|
|
description="Não há anexos do tipo selecionado neste mês."
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
{filteredAttachments.map((attachment) => (
|
|
<AttachmentGridItemWithUrl
|
|
key={`${attachment.attachmentId}-${attachment.transactionId}`}
|
|
attachment={attachment}
|
|
onClick={() => handleSelect(attachment)}
|
|
onDetails={() => handleDetails(attachment.transactionId)}
|
|
isLoadingDetails={
|
|
isPending &&
|
|
loadingTransactionId === attachment.transactionId
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AttachmentPreview
|
|
attachments={filteredAttachments}
|
|
selectedIndex={selectedIndex}
|
|
onClose={() => setSelectedIndex(-1)}
|
|
/>
|
|
|
|
<TransactionDetailsDialog
|
|
open={!!transactionDetails}
|
|
onOpenChange={(open) => {
|
|
if (!open) setTransactionDetails(null);
|
|
}}
|
|
transaction={transactionDetails}
|
|
onEdit={handleEdit}
|
|
/>
|
|
|
|
{dialogOptions && transactionToEdit && (
|
|
<TransactionDialog
|
|
mode="update"
|
|
open={editOpen}
|
|
onOpenChange={(open) => {
|
|
setEditOpen(open);
|
|
if (!open) {
|
|
setTransactionToEdit(null);
|
|
setDialogOptions(null);
|
|
router.refresh();
|
|
}
|
|
}}
|
|
transaction={transactionToEdit}
|
|
payerOptions={dialogOptions.payerOptions}
|
|
splitPayerOptions={dialogOptions.splitPayerOptions}
|
|
defaultPayerId={dialogOptions.defaultPayerId}
|
|
accountOptions={dialogOptions.accountOptions}
|
|
cardOptions={dialogOptions.cardOptions}
|
|
categoryOptions={dialogOptions.categoryOptions}
|
|
estabelecimentos={dialogOptions.estabelecimentos}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|