feat(dados-client): adotar react query em leituras do app

This commit is contained in:
Felipe Coutinho
2026-04-03 18:10:34 +00:00
parent e4c6a91350
commit acaf9d5c27
14 changed files with 409 additions and 150 deletions

View File

@@ -1,21 +1,16 @@
"use client";
import { RiFileAddLine } from "@remixicon/react";
import { useCallback, useEffect, useState } from "react";
import { fetchTransactionAttachmentsAction } from "@/features/transactions/actions/attachments";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import {
transactionAttachmentsQueryKey,
useTransactionAttachments,
} from "@/features/transactions/hooks/use-transaction-attachments";
import { Button } from "@/shared/components/ui/button";
import { AttachmentItem } from "./attachment-item";
import { AttachmentUpload } from "./attachment-upload";
type AttachmentRow = {
attachmentId: string;
fileName: string;
fileSize: number;
mimeType: string;
createdAt: Date;
url: string;
};
interface AttachmentSectionProps {
transactionId: string;
readonly?: boolean;
@@ -41,28 +36,35 @@ export function AttachmentSection({
onCancelPendingUpload,
maxSizeMb,
}: AttachmentSectionProps) {
const [items, setItems] = useState<AttachmentRow[]>([]);
const [isLoading, setIsLoading] = useState(true);
const load = useCallback(async () => {
setIsLoading(true);
try {
const data = await fetchTransactionAttachmentsAction(transactionId);
setItems(data);
onLoaded?.(data.length);
} finally {
setIsLoading(false);
}
}, [transactionId, onLoaded]);
const queryClient = useQueryClient();
const {
data: items = [],
isLoading,
isError,
} = useTransactionAttachments(transactionId);
useEffect(() => {
load();
}, [load]);
onLoaded?.(items.length);
}, [items.length, onLoaded]);
const invalidateAttachments = () => {
void queryClient.invalidateQueries({
queryKey: transactionAttachmentsQueryKey(transactionId),
});
};
if (isLoading) {
return <p className="text-xs text-muted-foreground">Carregando...</p>;
}
if (isError) {
return (
<p className="text-xs text-muted-foreground">
Não foi possível carregar os anexos.
</p>
);
}
const hasPendingUploads = (pendingUploadFiles?.length ?? 0) > 0;
return (
@@ -82,7 +84,7 @@ export function AttachmentSection({
fileSize={item.fileSize}
mimeType={item.mimeType}
url={item.url}
onDeleted={load}
onDeleted={invalidateAttachments}
readonly={readonly}
isPendingDelete={pendingDetachIds?.includes(item.attachmentId)}
onPendingDelete={onPendingDetach}
@@ -119,7 +121,7 @@ export function AttachmentSection({
{!readonly && (
<AttachmentUpload
transactionId={transactionId}
onUploaded={load}
onUploaded={invalidateAttachments}
onPendingUpload={onPendingUpload}
maxSizeMb={maxSizeMb}
/>

View File

@@ -1,9 +1,12 @@
"use client";
import { RiCalendarCheckLine, RiLoader4Line } from "@remixicon/react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { getInstallmentAnticipationsAction } from "@/features/transactions/anticipation-actions";
import { useQueryClient } from "@tanstack/react-query";
import {
installmentAnticipationsQueryKey,
useInstallmentAnticipations,
} from "@/features/transactions/hooks/use-installment-anticipations";
import { Button } from "@/shared/components/ui/button";
import {
Dialog,
DialogContent,
@@ -20,7 +23,6 @@ import {
EmptyTitle,
} from "@/shared/components/ui/empty";
import { useControlledState } from "@/shared/hooks/use-controlled-state";
import type { InstallmentAnticipationWithRelations } from "@/shared/lib/installments/anticipation-types";
import { AnticipationCard } from "../../shared/anticipation-card";
interface AnticipationHistoryDialogProps {
@@ -40,53 +42,23 @@ export function AnticipationHistoryDialog({
onOpenChange,
onViewLancamento,
}: AnticipationHistoryDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const [anticipations, setAnticipations] = useState<
InstallmentAnticipationWithRelations[]
>([]);
// Use controlled state hook for dialog open state
const queryClient = useQueryClient();
const [dialogOpen, setDialogOpen] = useControlledState(
open,
false,
onOpenChange,
);
// Define loadAnticipations before it's used in useEffect
const loadAnticipations = useCallback(async () => {
setIsLoading(true);
try {
const result = await getInstallmentAnticipationsAction(seriesId);
if (!result.success) {
toast.error(
result.error || "Erro ao carregar histórico de antecipações",
);
setAnticipations([]);
return;
}
setAnticipations(result.data ?? []);
} catch (error) {
console.error("Erro ao buscar antecipações:", error);
toast.error("Erro ao carregar histórico de antecipações");
setAnticipations([]);
} finally {
setIsLoading(false);
}
}, [seriesId]);
// Buscar antecipações ao abrir o dialog
useEffect(() => {
if (dialogOpen) {
loadAnticipations();
}
}, [dialogOpen, loadAnticipations]);
const {
data: anticipations = [],
isLoading,
isError,
refetch,
} = useInstallmentAnticipations(seriesId, dialogOpen);
const handleCanceled = () => {
// Recarregar lista após cancelamento
loadAnticipations();
void queryClient.invalidateQueries({
queryKey: installmentAnticipationsQueryKey(seriesId),
});
};
return (
@@ -106,6 +78,26 @@ export function AnticipationHistoryDialog({
Carregando histórico...
</span>
</div>
) : isError ? (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<RiCalendarCheckLine className="size-6 text-muted-foreground" />
</EmptyMedia>
<EmptyTitle>Não foi possível carregar</EmptyTitle>
<EmptyDescription>
O histórico de antecipações não pôde ser carregado agora.
</EmptyDescription>
</EmptyHeader>
<Button
type="button"
variant="outline"
className="mx-auto"
onClick={() => void refetch()}
>
Tentar novamente
</Button>
</Empty>
) : anticipations.length === 0 ? (
<Empty>
<EmptyHeader>

View File

@@ -6,6 +6,7 @@ import { ptBR } from "date-fns/locale";
import { useTransition } from "react";
import { toast } from "sonner";
import { cancelInstallmentAnticipationAction } from "@/features/transactions/anticipation-actions";
import type { InstallmentAnticipationListItem } from "@/features/transactions/hooks/use-installment-anticipations";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import MoneyValues from "@/shared/components/money-values";
import { Badge } from "@/shared/components/ui/badge";
@@ -18,11 +19,10 @@ import {
CardHeader,
CardTitle,
} from "@/shared/components/ui/card";
import type { InstallmentAnticipationWithRelations } from "@/shared/lib/installments/anticipation-types";
import { displayPeriod } from "@/shared/utils/period";
interface AnticipationCardProps {
anticipation: InstallmentAnticipationWithRelations;
anticipation: InstallmentAnticipationListItem;
onViewLancamento?: (transactionId: string) => void;
onCanceled?: () => void;
}
@@ -37,8 +37,8 @@ export function AnticipationCard({
const isSettled = anticipation.transaction?.isSettled === true;
const canCancel = !isSettled;
const formatDate = (date: Date) => {
return format(date, "dd 'de' MMMM 'de' yyyy", { locale: ptBR });
const formatDate = (date: string) => {
return format(new Date(date), "dd 'de' MMMM 'de' yyyy", { locale: ptBR });
};
const handleCancel = async () => {

View File

@@ -0,0 +1,56 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { fetchJson } from "@/shared/lib/fetch-json";
const anticipationItemSchema = z.object({
id: z.string().uuid(),
anticipationPeriod: z.string().regex(/^\d{4}-\d{2}$/),
anticipationDate: z.string().min(1),
installmentCount: z.number().int(),
totalAmount: z.string(),
discount: z.string(),
transactionId: z.string().uuid(),
note: z.string().nullable(),
transaction: z
.object({
isSettled: z.boolean().nullable(),
})
.nullable(),
payer: z
.object({
name: z.string().min(1),
})
.nullable(),
category: z
.object({
name: z.string().min(1),
})
.nullable(),
});
export type InstallmentAnticipationListItem = z.infer<
typeof anticipationItemSchema
>;
export const installmentAnticipationsQueryKey = (seriesId: string) =>
["transactions", "installment-anticipations", seriesId] as const;
export function useInstallmentAnticipations(
seriesId: string,
enabled: boolean,
) {
return useQuery({
queryKey: installmentAnticipationsQueryKey(seriesId),
queryFn: async () => {
const payload = await fetchJson<unknown>(
`/api/transactions/installments/${seriesId}/anticipations`,
);
return z.array(anticipationItemSchema).parse(payload);
},
enabled: enabled && Boolean(seriesId),
staleTime: 30_000,
});
}

View File

@@ -0,0 +1,20 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { TransactionAttachmentListItem } from "@/features/transactions/attachment-queries";
import { fetchJson } from "@/shared/lib/fetch-json";
export const transactionAttachmentsQueryKey = (transactionId: string) =>
["transactions", "attachments", transactionId] as const;
export function useTransactionAttachments(transactionId: string) {
return useQuery({
queryKey: transactionAttachmentsQueryKey(transactionId),
queryFn: () =>
fetchJson<TransactionAttachmentListItem[]>(
`/api/transactions/${transactionId}/attachments`,
),
enabled: Boolean(transactionId),
staleTime: 30_000,
});
}