mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat(dados-client): adotar react query em leituras do app
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user