forked from git.gladyson/openmonetis
chore: snapshot sidebar layout antes de experimentar topbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
cartoes,
|
cartoes,
|
||||||
categorias,
|
categorias,
|
||||||
contas,
|
contas,
|
||||||
|
faturas,
|
||||||
lancamentos,
|
lancamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
||||||
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
||||||
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
|
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
|
||||||
|
import { getNextPeriod } from "@/lib/utils/period";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Authorization Validation Functions
|
// Authorization Validation Functions
|
||||||
@@ -1639,6 +1641,59 @@ export async function deleteMultipleLancamentosAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check fatura payment status and card closing day for the given period
|
||||||
|
export async function checkFaturaStatusAction(
|
||||||
|
cartaoId: string,
|
||||||
|
period: string,
|
||||||
|
): Promise<{
|
||||||
|
shouldSuggestNext: boolean;
|
||||||
|
isPaid: boolean;
|
||||||
|
isAfterClosing: boolean;
|
||||||
|
closingDay: string | null;
|
||||||
|
cardName: string;
|
||||||
|
nextPeriod: string;
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
const cartao = await db.query.cartoes.findFirst({
|
||||||
|
where: and(eq(cartoes.id, cartaoId), eq(cartoes.userId, user.id)),
|
||||||
|
columns: { id: true, name: true, closingDay: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cartao) return null;
|
||||||
|
|
||||||
|
const fatura = await db.query.faturas.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(faturas.cartaoId, cartaoId),
|
||||||
|
eq(faturas.userId, user.id),
|
||||||
|
eq(faturas.period, period),
|
||||||
|
),
|
||||||
|
columns: { paymentStatus: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPaid = fatura?.paymentStatus === "pago";
|
||||||
|
const today = new Date();
|
||||||
|
const currentPeriod = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
|
||||||
|
const closingDayNum = Number.parseInt(cartao.closingDay ?? "", 10);
|
||||||
|
const isAfterClosing =
|
||||||
|
period === currentPeriod &&
|
||||||
|
!Number.isNaN(closingDayNum) &&
|
||||||
|
today.getDate() > closingDayNum;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldSuggestNext: isPaid || isAfterClosing,
|
||||||
|
isPaid,
|
||||||
|
isAfterClosing,
|
||||||
|
closingDay: cartao.closingDay,
|
||||||
|
cardName: cartao.name,
|
||||||
|
nextPeriod: getNextPeriod(period),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get unique establishment names from the last 3 months
|
// Get unique establishment names from the last 3 months
|
||||||
export async function getRecentEstablishmentsAction(): Promise<string[]> {
|
export async function getRecentEstablishmentsAction(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default async function DashboardLayout({
|
|||||||
<SiteHeader notificationsSnapshot={notificationsSnapshot} />
|
<SiteHeader notificationsSnapshot={notificationsSnapshot} />
|
||||||
<div className="flex flex-1 flex-col pt-12 md:pt-0">
|
<div className="flex flex-1 flex-col pt-12 md:pt-0">
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6">
|
<div className="flex flex-col gap-4 py-4 md:gap-6 w-full max-w-8xl mx-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--spacing-custom-height-1: 30rem;
|
--spacing-custom-height-1: 30rem;
|
||||||
|
--spacing-8xl: 88rem; /* 1408px */
|
||||||
|
--spacing-9xl: 96rem; /* 1536px */
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
84
components/lancamentos/dialogs/fatura-warning-dialog.tsx
Normal file
84
components/lancamentos/dialogs/fatura-warning-dialog.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { MONTH_NAMES } from "@/lib/utils/period";
|
||||||
|
|
||||||
|
export type FaturaWarning = {
|
||||||
|
nextPeriod: string;
|
||||||
|
cardName: string;
|
||||||
|
isPaid: boolean;
|
||||||
|
isAfterClosing: boolean;
|
||||||
|
closingDay: string | null;
|
||||||
|
currentPeriod: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatPeriodDisplay(period: string): string {
|
||||||
|
const [yearStr, monthStr] = period.split("-");
|
||||||
|
const monthIndex = Number.parseInt(monthStr ?? "1", 10) - 1;
|
||||||
|
const monthName = MONTH_NAMES[monthIndex] ?? monthStr;
|
||||||
|
return `${monthName}/${yearStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWarningMessage(warning: FaturaWarning): string {
|
||||||
|
const currentDisplay = formatPeriodDisplay(warning.currentPeriod);
|
||||||
|
if (warning.isPaid && warning.isAfterClosing) {
|
||||||
|
return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga e fechou no dia ${warning.closingDay}.`;
|
||||||
|
}
|
||||||
|
if (warning.isPaid) {
|
||||||
|
return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga.`;
|
||||||
|
}
|
||||||
|
return `A fatura do ${warning.cardName} fechou no dia ${warning.closingDay}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FaturaWarningDialogProps {
|
||||||
|
warning: FaturaWarning | null;
|
||||||
|
onConfirm: (nextPeriod: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FaturaWarningDialog({
|
||||||
|
warning,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: FaturaWarningDialogProps) {
|
||||||
|
if (!warning) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertDialogContent className="sm:max-w-md">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Fatura indisponível</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{buildWarningMessage(warning)} Deseja registrá-lo em{" "}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{formatPeriodDisplay(warning.nextPeriod)}
|
||||||
|
</span>
|
||||||
|
?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter className="flex-col gap-2 sm:flex-col">
|
||||||
|
<AlertDialogAction onClick={() => onConfirm(warning.nextPeriod)}>
|
||||||
|
Mover para {formatPeriodDisplay(warning.nextPeriod)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
<AlertDialogCancel onClick={onCancel}>
|
||||||
|
Manter em {formatPeriodDisplay(warning.currentPeriod)}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
useTransition,
|
useTransition,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
checkFaturaStatusAction,
|
||||||
createLancamentoAction,
|
createLancamentoAction,
|
||||||
updateLancamentoAction,
|
updateLancamentoAction,
|
||||||
} from "@/app/(dashboard)/lancamentos/actions";
|
} from "@/app/(dashboard)/lancamentos/actions";
|
||||||
@@ -30,6 +32,10 @@ import {
|
|||||||
applyFieldDependencies,
|
applyFieldDependencies,
|
||||||
buildLancamentoInitialState,
|
buildLancamentoInitialState,
|
||||||
} from "@/lib/lancamentos/form-helpers";
|
} from "@/lib/lancamentos/form-helpers";
|
||||||
|
import {
|
||||||
|
type FaturaWarning,
|
||||||
|
FaturaWarningDialog,
|
||||||
|
} from "../fatura-warning-dialog";
|
||||||
import { BasicFieldsSection } from "./basic-fields-section";
|
import { BasicFieldsSection } from "./basic-fields-section";
|
||||||
import { BoletoFieldsSection } from "./boleto-fields-section";
|
import { BoletoFieldsSection } from "./boleto-fields-section";
|
||||||
import { CategorySection } from "./category-section";
|
import { CategorySection } from "./category-section";
|
||||||
@@ -90,6 +96,10 @@ export function LancamentoDialog({
|
|||||||
const [periodDirty, setPeriodDirty] = useState(false);
|
const [periodDirty, setPeriodDirty] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const lastCheckedRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
@@ -111,6 +121,10 @@ export function LancamentoDialog({
|
|||||||
);
|
);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setPeriodDirty(false);
|
setPeriodDirty(false);
|
||||||
|
setFaturaWarning(null);
|
||||||
|
lastCheckedRef.current = null;
|
||||||
|
} else {
|
||||||
|
setFaturaWarning(null);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
dialogOpen,
|
dialogOpen,
|
||||||
@@ -126,6 +140,40 @@ export function LancamentoDialog({
|
|||||||
isImporting,
|
isImporting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode !== "create") return;
|
||||||
|
if (!dialogOpen) return;
|
||||||
|
if (formState.paymentMethod !== "Cartão de crédito") return;
|
||||||
|
if (!formState.cartaoId) return;
|
||||||
|
|
||||||
|
const checkKey = `${formState.cartaoId}:${formState.period}`;
|
||||||
|
if (checkKey === lastCheckedRef.current) return;
|
||||||
|
lastCheckedRef.current = checkKey;
|
||||||
|
|
||||||
|
checkFaturaStatusAction(formState.cartaoId, formState.period).then(
|
||||||
|
(result) => {
|
||||||
|
if (result?.shouldSuggestNext) {
|
||||||
|
setFaturaWarning({
|
||||||
|
nextPeriod: result.nextPeriod,
|
||||||
|
cardName: result.cardName,
|
||||||
|
isPaid: result.isPaid,
|
||||||
|
isAfterClosing: result.isAfterClosing,
|
||||||
|
closingDay: result.closingDay,
|
||||||
|
currentPeriod: formState.period,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFaturaWarning(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
mode,
|
||||||
|
dialogOpen,
|
||||||
|
formState.paymentMethod,
|
||||||
|
formState.cartaoId,
|
||||||
|
formState.period,
|
||||||
|
]);
|
||||||
|
|
||||||
const primaryPagador = formState.pagadorId;
|
const primaryPagador = formState.pagadorId;
|
||||||
|
|
||||||
const secondaryPagadorOptions = useMemo(
|
const secondaryPagadorOptions = useMemo(
|
||||||
@@ -392,103 +440,114 @@ export function LancamentoDialog({
|
|||||||
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
|
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<>
|
||||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
|
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||||
<DialogHeader>
|
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription>{description}</DialogDescription>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
</DialogHeader>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="space-y-2 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
|
className="space-y-2 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
noValidate
|
noValidate
|
||||||
>
|
>
|
||||||
<BasicFieldsSection
|
<BasicFieldsSection
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
estabelecimentos={estabelecimentos}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CategorySection
|
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
categoriaOptions={categoriaOptions}
|
|
||||||
categoriaGroups={categoriaGroups}
|
|
||||||
isUpdateMode={isUpdateMode}
|
|
||||||
hideTransactionType={
|
|
||||||
Boolean(isNewWithType) && !forceShowTransactionType
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isUpdateMode ? (
|
|
||||||
<SplitAndSettlementSection
|
|
||||||
formState={formState}
|
formState={formState}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
showSettledToggle={showSettledToggle}
|
estabelecimentos={estabelecimentos}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
<PagadorSection
|
<CategorySection
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
pagadorOptions={pagadorOptions}
|
|
||||||
secondaryPagadorOptions={secondaryPagadorOptions}
|
|
||||||
totalAmount={totalAmount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PaymentMethodSection
|
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
contaOptions={contaOptions}
|
|
||||||
cartaoOptions={cartaoOptions}
|
|
||||||
isUpdateMode={isUpdateMode}
|
|
||||||
disablePaymentMethod={disablePaymentMethod}
|
|
||||||
disableCartaoSelect={disableCartaoSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showDueDate ? (
|
|
||||||
<BoletoFieldsSection
|
|
||||||
formState={formState}
|
formState={formState}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
showPaymentDate={showPaymentDate}
|
categoriaOptions={categoriaOptions}
|
||||||
|
categoriaGroups={categoriaGroups}
|
||||||
|
isUpdateMode={isUpdateMode}
|
||||||
|
hideTransactionType={
|
||||||
|
Boolean(isNewWithType) && !forceShowTransactionType
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isUpdateMode ? (
|
{!isUpdateMode ? (
|
||||||
<ConditionSection
|
<SplitAndSettlementSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
showSettledToggle={showSettledToggle}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<PagadorSection
|
||||||
formState={formState}
|
formState={formState}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
showInstallments={showInstallments}
|
pagadorOptions={pagadorOptions}
|
||||||
showRecurrence={showRecurrence}
|
secondaryPagadorOptions={secondaryPagadorOptions}
|
||||||
|
totalAmount={totalAmount}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
<NoteSection
|
<PaymentMethodSection
|
||||||
formState={formState}
|
formState={formState}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
/>
|
contaOptions={contaOptions}
|
||||||
|
cartaoOptions={cartaoOptions}
|
||||||
|
isUpdateMode={isUpdateMode}
|
||||||
|
disablePaymentMethod={disablePaymentMethod}
|
||||||
|
disableCartaoSelect={disableCartaoSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
{errorMessage ? (
|
{showDueDate ? (
|
||||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
<BoletoFieldsSection
|
||||||
) : null}
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
showPaymentDate={showPaymentDate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DialogFooter className="gap-3">
|
{!isUpdateMode ? (
|
||||||
<Button
|
<ConditionSection
|
||||||
type="button"
|
formState={formState}
|
||||||
variant="outline"
|
onFieldChange={handleFieldChange}
|
||||||
onClick={() => setDialogOpen(false)}
|
showInstallments={showInstallments}
|
||||||
disabled={isPending}
|
showRecurrence={showRecurrence}
|
||||||
>
|
/>
|
||||||
Cancelar
|
) : null}
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending}>
|
<NoteSection
|
||||||
{isPending ? "Salvando..." : submitLabel}
|
formState={formState}
|
||||||
</Button>
|
onFieldChange={handleFieldChange}
|
||||||
</DialogFooter>
|
/>
|
||||||
</form>
|
|
||||||
</DialogContent>
|
{errorMessage ? (
|
||||||
</Dialog>
|
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<DialogFooter className="gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setDialogOpen(false)}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isPending}>
|
||||||
|
{isPending ? "Salvando..." : submitLabel}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<FaturaWarningDialog
|
||||||
|
warning={faturaWarning}
|
||||||
|
onConfirm={(nextPeriod) => {
|
||||||
|
handleFieldChange("period", nextPeriod);
|
||||||
|
setFaturaWarning(null);
|
||||||
|
}}
|
||||||
|
onCancel={() => setFaturaWarning(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { checkFaturaStatusAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||||
import { PeriodPicker } from "@/components/period-picker";
|
import { PeriodPicker } from "@/components/period-picker";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||||
@@ -39,6 +40,10 @@ import {
|
|||||||
TransactionTypeSelectContent,
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||||
|
import {
|
||||||
|
type FaturaWarning,
|
||||||
|
FaturaWarningDialog,
|
||||||
|
} from "./fatura-warning-dialog";
|
||||||
|
|
||||||
interface MassAddDialogProps {
|
interface MassAddDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -119,6 +124,39 @@ export function MassAddDialog({
|
|||||||
? contaCartaoId.replace("cartao:", "")
|
? contaCartaoId.replace("cartao:", "")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const lastCheckedRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setFaturaWarning(null);
|
||||||
|
lastCheckedRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isCartaoSelected || !cartaoId) return;
|
||||||
|
|
||||||
|
const checkKey = `${cartaoId}:${period}`;
|
||||||
|
if (checkKey === lastCheckedRef.current) return;
|
||||||
|
lastCheckedRef.current = checkKey;
|
||||||
|
|
||||||
|
checkFaturaStatusAction(cartaoId, period).then((result) => {
|
||||||
|
if (result?.shouldSuggestNext) {
|
||||||
|
setFaturaWarning({
|
||||||
|
nextPeriod: result.nextPeriod,
|
||||||
|
cardName: result.cardName,
|
||||||
|
isPaid: result.isPaid,
|
||||||
|
isAfterClosing: result.isAfterClosing,
|
||||||
|
closingDay: result.closingDay,
|
||||||
|
currentPeriod: period,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFaturaWarning(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [open, isCartaoSelected, cartaoId, period]);
|
||||||
|
|
||||||
// Transaction rows
|
// Transaction rows
|
||||||
const [transactions, setTransactions] = useState<TransactionRow[]>([
|
const [transactions, setTransactions] = useState<TransactionRow[]>([
|
||||||
{
|
{
|
||||||
@@ -238,381 +276,402 @@ export function MassAddDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<>
|
||||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogHeader>
|
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
||||||
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription>
|
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
||||||
Configure os valores padrão e adicione várias transações de uma vez.
|
<DialogDescription>
|
||||||
Todos os lançamentos adicionados aqui são{" "}
|
Configure os valores padrão e adicione várias transações de uma
|
||||||
<span className="font-bold">sempre à vista</span>.
|
vez. Todos os lançamentos adicionados aqui são{" "}
|
||||||
</DialogDescription>
|
<span className="font-bold">sempre à vista</span>.
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Fixed Fields Section */}
|
{/* Fixed Fields Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{/* Transaction Type */}
|
{/* Transaction Type */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
||||||
<Select
|
<Select
|
||||||
value={transactionType}
|
value={transactionType}
|
||||||
onValueChange={setTransactionType}
|
onValueChange={setTransactionType}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="transaction-type" className="w-full">
|
<SelectTrigger id="transaction-type" className="w-full">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{transactionType && (
|
{transactionType && (
|
||||||
<TransactionTypeSelectContent label={transactionType} />
|
<TransactionTypeSelectContent
|
||||||
)}
|
label={transactionType}
|
||||||
</SelectValue>
|
/>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="Despesa">
|
|
||||||
<TransactionTypeSelectContent label="Despesa" />
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Receita">
|
|
||||||
<TransactionTypeSelectContent label="Receita" />
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment Method */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
|
||||||
<Select
|
|
||||||
value={paymentMethod}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setPaymentMethod(value);
|
|
||||||
// Reset conta/cartao when changing payment method
|
|
||||||
if (value === "Cartão de crédito") {
|
|
||||||
setContaId(undefined);
|
|
||||||
} else {
|
|
||||||
setCartaoId(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="payment-method" className="w-full">
|
|
||||||
<SelectValue>
|
|
||||||
{paymentMethod && (
|
|
||||||
<PaymentMethodSelectContent label={paymentMethod} />
|
|
||||||
)}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
|
|
||||||
<SelectItem key={method} value={method}>
|
|
||||||
<PaymentMethodSelectContent label={method} />
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Period */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="period">Período</Label>
|
|
||||||
<PeriodPicker
|
|
||||||
value={period}
|
|
||||||
onChange={setPeriod}
|
|
||||||
className="w-full truncate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Conta/Cartao */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="conta-cartao">
|
|
||||||
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={contaCartaoId}
|
|
||||||
onValueChange={setContaCartaoId}
|
|
||||||
disabled={isLockedToCartao}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="conta-cartao" className="w-full">
|
|
||||||
<SelectValue placeholder="Selecione">
|
|
||||||
{contaCartaoId &&
|
|
||||||
(() => {
|
|
||||||
if (isCartaoSelected) {
|
|
||||||
const selectedOption = cartaoOptions.find(
|
|
||||||
(opt) => opt.value === cartaoId,
|
|
||||||
);
|
|
||||||
return selectedOption ? (
|
|
||||||
<ContaCartaoSelectContent
|
|
||||||
label={selectedOption.label}
|
|
||||||
logo={selectedOption.logo}
|
|
||||||
isCartao={true}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
} else {
|
|
||||||
const selectedOption = contaOptions.find(
|
|
||||||
(opt) => opt.value === contaId,
|
|
||||||
);
|
|
||||||
return selectedOption ? (
|
|
||||||
<ContaCartaoSelectContent
|
|
||||||
label={selectedOption.label}
|
|
||||||
logo={selectedOption.logo}
|
|
||||||
isCartao={false}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{cartaoOptions.length > 0 && (
|
|
||||||
<SelectGroup>
|
|
||||||
{!isLockedToCartao && (
|
|
||||||
<SelectLabel>Cartões</SelectLabel>
|
|
||||||
)}
|
)}
|
||||||
{cartaoOptions
|
</SelectValue>
|
||||||
.filter(
|
</SelectTrigger>
|
||||||
(option) =>
|
<SelectContent>
|
||||||
!isLockedToCartao ||
|
<SelectItem value="Despesa">
|
||||||
option.value === defaultCartaoId,
|
<TransactionTypeSelectContent label="Despesa" />
|
||||||
)
|
</SelectItem>
|
||||||
.map((option) => (
|
<SelectItem value="Receita">
|
||||||
|
<TransactionTypeSelectContent label="Receita" />
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Method */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
||||||
|
<Select
|
||||||
|
value={paymentMethod}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setPaymentMethod(value);
|
||||||
|
// Reset conta/cartao when changing payment method
|
||||||
|
if (value === "Cartão de crédito") {
|
||||||
|
setContaId(undefined);
|
||||||
|
} else {
|
||||||
|
setCartaoId(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="payment-method" className="w-full">
|
||||||
|
<SelectValue>
|
||||||
|
{paymentMethod && (
|
||||||
|
<PaymentMethodSelectContent label={paymentMethod} />
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
|
||||||
|
<SelectItem key={method} value={method}>
|
||||||
|
<PaymentMethodSelectContent label={method} />
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Period */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="period">Período</Label>
|
||||||
|
<PeriodPicker
|
||||||
|
value={period}
|
||||||
|
onChange={setPeriod}
|
||||||
|
className="w-full truncate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conta/Cartao */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="conta-cartao">
|
||||||
|
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={contaCartaoId}
|
||||||
|
onValueChange={setContaCartaoId}
|
||||||
|
disabled={isLockedToCartao}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="conta-cartao" className="w-full">
|
||||||
|
<SelectValue placeholder="Selecione">
|
||||||
|
{contaCartaoId &&
|
||||||
|
(() => {
|
||||||
|
if (isCartaoSelected) {
|
||||||
|
const selectedOption = cartaoOptions.find(
|
||||||
|
(opt) => opt.value === cartaoId,
|
||||||
|
);
|
||||||
|
return selectedOption ? (
|
||||||
|
<ContaCartaoSelectContent
|
||||||
|
label={selectedOption.label}
|
||||||
|
logo={selectedOption.logo}
|
||||||
|
isCartao={true}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
} else {
|
||||||
|
const selectedOption = contaOptions.find(
|
||||||
|
(opt) => opt.value === contaId,
|
||||||
|
);
|
||||||
|
return selectedOption ? (
|
||||||
|
<ContaCartaoSelectContent
|
||||||
|
label={selectedOption.label}
|
||||||
|
logo={selectedOption.logo}
|
||||||
|
isCartao={false}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{cartaoOptions.length > 0 && (
|
||||||
|
<SelectGroup>
|
||||||
|
{!isLockedToCartao && (
|
||||||
|
<SelectLabel>Cartões</SelectLabel>
|
||||||
|
)}
|
||||||
|
{cartaoOptions
|
||||||
|
.filter(
|
||||||
|
(option) =>
|
||||||
|
!isLockedToCartao ||
|
||||||
|
option.value === defaultCartaoId,
|
||||||
|
)
|
||||||
|
.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={`cartao:${option.value}`}
|
||||||
|
>
|
||||||
|
<ContaCartaoSelectContent
|
||||||
|
label={option.label}
|
||||||
|
logo={option.logo}
|
||||||
|
isCartao={true}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
)}
|
||||||
|
{!isLockedToCartao && contaOptions.length > 0 && (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Contas</SelectLabel>
|
||||||
|
{contaOptions.map((option) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={`cartao:${option.value}`}
|
value={`conta:${option.value}`}
|
||||||
>
|
>
|
||||||
<ContaCartaoSelectContent
|
<ContaCartaoSelectContent
|
||||||
label={option.label}
|
label={option.label}
|
||||||
logo={option.logo}
|
logo={option.logo}
|
||||||
isCartao={true}
|
isCartao={false}
|
||||||
/>
|
/>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
)}
|
)}
|
||||||
{!isLockedToCartao && contaOptions.length > 0 && (
|
</SelectContent>
|
||||||
<SelectGroup>
|
</Select>
|
||||||
<SelectLabel>Contas</SelectLabel>
|
</div>
|
||||||
{contaOptions.map((option) => (
|
</div>
|
||||||
<SelectItem
|
</div>
|
||||||
key={option.value}
|
|
||||||
value={`conta:${option.value}`}
|
<Separator />
|
||||||
|
|
||||||
|
{/* Transactions Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-semibold">Lançamentos</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{transactions.map((transaction, index) => (
|
||||||
|
<div
|
||||||
|
key={transaction.id}
|
||||||
|
className="grid gap-2 border-b pb-3 border-dashed last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 w-full">
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`date-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Data {index + 1}
|
||||||
|
</Label>
|
||||||
|
<DatePicker
|
||||||
|
id={`date-${transaction.id}`}
|
||||||
|
value={transaction.purchaseDate}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateTransaction(
|
||||||
|
transaction.id,
|
||||||
|
"purchaseDate",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Data"
|
||||||
|
className="w-32 truncate"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`name-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Estabelecimento {index + 1}
|
||||||
|
</Label>
|
||||||
|
<EstabelecimentoInput
|
||||||
|
id={`name-${transaction.id}`}
|
||||||
|
placeholder="Local"
|
||||||
|
value={transaction.name}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "name", value)
|
||||||
|
}
|
||||||
|
estabelecimentos={estabelecimentos}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`amount-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Valor {index + 1}
|
||||||
|
</Label>
|
||||||
|
<CurrencyInput
|
||||||
|
id={`amount-${transaction.id}`}
|
||||||
|
placeholder="R$ 0,00"
|
||||||
|
value={transaction.amount}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "amount", value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`pagador-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Pagador {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={transaction.pagadorId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(
|
||||||
|
transaction.id,
|
||||||
|
"pagadorId",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`pagador-${transaction.id}`}
|
||||||
|
className="w-32 truncate"
|
||||||
>
|
>
|
||||||
<ContaCartaoSelectContent
|
<SelectValue placeholder="Pagador">
|
||||||
label={option.label}
|
{transaction.pagadorId &&
|
||||||
logo={option.logo}
|
(() => {
|
||||||
isCartao={false}
|
const selectedOption = pagadorOptions.find(
|
||||||
/>
|
(opt) =>
|
||||||
</SelectItem>
|
opt.value === transaction.pagadorId,
|
||||||
))}
|
);
|
||||||
</SelectGroup>
|
return selectedOption ? (
|
||||||
)}
|
<PagadorSelectContent
|
||||||
</SelectContent>
|
label={selectedOption.label}
|
||||||
</Select>
|
avatarUrl={selectedOption.avatarUrl}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pagadorOptions.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
<PagadorSelectContent
|
||||||
|
label={option.label}
|
||||||
|
avatarUrl={option.avatarUrl}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`categoria-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Categoria {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={transaction.categoriaId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(
|
||||||
|
transaction.id,
|
||||||
|
"categoriaId",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`categoria-${transaction.id}`}
|
||||||
|
className="w-32 truncate"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Categoria" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{groupedCategorias.map((group) => (
|
||||||
|
<SelectGroup key={group.label}>
|
||||||
|
<SelectLabel>{group.label}</SelectLabel>
|
||||||
|
{group.options.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
<CategoriaSelectContent
|
||||||
|
label={option.label}
|
||||||
|
icon={option.icon}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 shrink-0"
|
||||||
|
onClick={addTransaction}
|
||||||
|
>
|
||||||
|
<RiAddLine className="size-3.5" />
|
||||||
|
<span className="sr-only">Adicionar transação</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 shrink-0"
|
||||||
|
onClick={() => removeTransaction(transaction.id)}
|
||||||
|
disabled={transactions.length === 1}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="size-3.5" />
|
||||||
|
<span className="sr-only">Remover transação</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={loading}>
|
||||||
|
{loading && <Spinner className="size-4" />}
|
||||||
|
Criar {transactions.length}{" "}
|
||||||
|
{transactions.length === 1 ? "lançamento" : "lançamentos"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Transactions Section */}
|
<FaturaWarningDialog
|
||||||
<div className="space-y-4">
|
warning={faturaWarning}
|
||||||
<h3 className="text-sm font-semibold">Lançamentos</h3>
|
onConfirm={(nextPeriod) => {
|
||||||
|
setPeriod(nextPeriod);
|
||||||
<div className="space-y-3">
|
setFaturaWarning(null);
|
||||||
{transactions.map((transaction, index) => (
|
}}
|
||||||
<div
|
onCancel={() => setFaturaWarning(null)}
|
||||||
key={transaction.id}
|
/>
|
||||||
className="grid gap-2 border-b pb-3 border-dashed last:border-0"
|
</>
|
||||||
>
|
|
||||||
<div className="flex gap-2 w-full">
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`date-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Data {index + 1}
|
|
||||||
</Label>
|
|
||||||
<DatePicker
|
|
||||||
id={`date-${transaction.id}`}
|
|
||||||
value={transaction.purchaseDate}
|
|
||||||
onChange={(value) =>
|
|
||||||
updateTransaction(
|
|
||||||
transaction.id,
|
|
||||||
"purchaseDate",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="Data"
|
|
||||||
className="w-32 truncate"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`name-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Estabelecimento {index + 1}
|
|
||||||
</Label>
|
|
||||||
<EstabelecimentoInput
|
|
||||||
id={`name-${transaction.id}`}
|
|
||||||
placeholder="Local"
|
|
||||||
value={transaction.name}
|
|
||||||
onChange={(value) =>
|
|
||||||
updateTransaction(transaction.id, "name", value)
|
|
||||||
}
|
|
||||||
estabelecimentos={estabelecimentos}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`amount-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Valor {index + 1}
|
|
||||||
</Label>
|
|
||||||
<CurrencyInput
|
|
||||||
id={`amount-${transaction.id}`}
|
|
||||||
placeholder="R$ 0,00"
|
|
||||||
value={transaction.amount}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(transaction.id, "amount", value)
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`pagador-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Pagador {index + 1}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={transaction.pagadorId}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(transaction.id, "pagadorId", value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id={`pagador-${transaction.id}`}
|
|
||||||
className="w-32 truncate"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Pagador">
|
|
||||||
{transaction.pagadorId &&
|
|
||||||
(() => {
|
|
||||||
const selectedOption = pagadorOptions.find(
|
|
||||||
(opt) => opt.value === transaction.pagadorId,
|
|
||||||
);
|
|
||||||
return selectedOption ? (
|
|
||||||
<PagadorSelectContent
|
|
||||||
label={selectedOption.label}
|
|
||||||
avatarUrl={selectedOption.avatarUrl}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
})()}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{pagadorOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
<PagadorSelectContent
|
|
||||||
label={option.label}
|
|
||||||
avatarUrl={option.avatarUrl}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`categoria-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Categoria {index + 1}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={transaction.categoriaId}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(
|
|
||||||
transaction.id,
|
|
||||||
"categoriaId",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id={`categoria-${transaction.id}`}
|
|
||||||
className="w-32 truncate"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Categoria" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{groupedCategorias.map((group) => (
|
|
||||||
<SelectGroup key={group.label}>
|
|
||||||
<SelectLabel>{group.label}</SelectLabel>
|
|
||||||
{group.options.map((option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
>
|
|
||||||
<CategoriaSelectContent
|
|
||||||
label={option.label}
|
|
||||||
icon={option.icon}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
onClick={addTransaction}
|
|
||||||
>
|
|
||||||
<RiAddLine className="size-3.5" />
|
|
||||||
<span className="sr-only">Adicionar transação</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
onClick={() => removeTransaction(transaction.id)}
|
|
||||||
disabled={transactions.length === 1}
|
|
||||||
>
|
|
||||||
<RiDeleteBinLine className="size-3.5" />
|
|
||||||
<span className="sr-only">Remover transação</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={loading}>
|
|
||||||
{loading && <Spinner className="size-4" />}
|
|
||||||
Criar {transactions.length}{" "}
|
|
||||||
{transactions.length === 1 ? "lançamento" : "lançamentos"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user