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,
|
||||
categorias,
|
||||
contas,
|
||||
faturas,
|
||||
lancamentos,
|
||||
pagadores,
|
||||
} from "@/db/schema";
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
||||
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
||||
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
|
||||
import { getNextPeriod } from "@/lib/utils/period";
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
export async function getRecentEstablishmentsAction(): Promise<string[]> {
|
||||
try {
|
||||
|
||||
@@ -72,7 +72,7 @@ export default async function DashboardLayout({
|
||||
<SiteHeader notificationsSnapshot={notificationsSnapshot} />
|
||||
<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="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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
@theme {
|
||||
--spacing-custom-height-1: 30rem;
|
||||
--spacing-8xl: 88rem; /* 1408px */
|
||||
--spacing-9xl: 96rem; /* 1536px */
|
||||
}
|
||||
|
||||
: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,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
checkFaturaStatusAction,
|
||||
createLancamentoAction,
|
||||
updateLancamentoAction,
|
||||
} from "@/app/(dashboard)/lancamentos/actions";
|
||||
@@ -30,6 +32,10 @@ import {
|
||||
applyFieldDependencies,
|
||||
buildLancamentoInitialState,
|
||||
} from "@/lib/lancamentos/form-helpers";
|
||||
import {
|
||||
type FaturaWarning,
|
||||
FaturaWarningDialog,
|
||||
} from "../fatura-warning-dialog";
|
||||
import { BasicFieldsSection } from "./basic-fields-section";
|
||||
import { BoletoFieldsSection } from "./boleto-fields-section";
|
||||
import { CategorySection } from "./category-section";
|
||||
@@ -90,6 +96,10 @@ export function LancamentoDialog({
|
||||
const [periodDirty, setPeriodDirty] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
|
||||
null,
|
||||
);
|
||||
const lastCheckedRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
@@ -111,6 +121,10 @@ export function LancamentoDialog({
|
||||
);
|
||||
setErrorMessage(null);
|
||||
setPeriodDirty(false);
|
||||
setFaturaWarning(null);
|
||||
lastCheckedRef.current = null;
|
||||
} else {
|
||||
setFaturaWarning(null);
|
||||
}
|
||||
}, [
|
||||
dialogOpen,
|
||||
@@ -126,6 +140,40 @@ export function LancamentoDialog({
|
||||
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 secondaryPagadorOptions = useMemo(
|
||||
@@ -392,6 +440,7 @@ export function LancamentoDialog({
|
||||
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
|
||||
@@ -490,5 +539,15 @@ export function LancamentoDialog({
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<FaturaWarningDialog
|
||||
warning={faturaWarning}
|
||||
onConfirm={(nextPeriod) => {
|
||||
handleFieldChange("period", nextPeriod);
|
||||
setFaturaWarning(null);
|
||||
}}
|
||||
onCancel={() => setFaturaWarning(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { checkFaturaStatusAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { PeriodPicker } from "@/components/period-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||
@@ -39,6 +40,10 @@ import {
|
||||
TransactionTypeSelectContent,
|
||||
} from "../select-items";
|
||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||
import {
|
||||
type FaturaWarning,
|
||||
FaturaWarningDialog,
|
||||
} from "./fatura-warning-dialog";
|
||||
|
||||
interface MassAddDialogProps {
|
||||
open: boolean;
|
||||
@@ -119,6 +124,39 @@ export function MassAddDialog({
|
||||
? contaCartaoId.replace("cartao:", "")
|
||||
: 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
|
||||
const [transactions, setTransactions] = useState<TransactionRow[]>([
|
||||
{
|
||||
@@ -238,13 +276,14 @@ export function MassAddDialog({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure os valores padrão e adicione várias transações de uma vez.
|
||||
Todos os lançamentos adicionados aqui são{" "}
|
||||
Configure os valores padrão e adicione várias transações de uma
|
||||
vez. Todos os lançamentos adicionados aqui são{" "}
|
||||
<span className="font-bold">sempre à vista</span>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -264,7 +303,9 @@ export function MassAddDialog({
|
||||
<SelectTrigger id="transaction-type" className="w-full">
|
||||
<SelectValue>
|
||||
{transactionType && (
|
||||
<TransactionTypeSelectContent label={transactionType} />
|
||||
<TransactionTypeSelectContent
|
||||
label={transactionType}
|
||||
/>
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
@@ -492,7 +533,11 @@ export function MassAddDialog({
|
||||
<Select
|
||||
value={transaction.pagadorId}
|
||||
onValueChange={(value) =>
|
||||
updateTransaction(transaction.id, "pagadorId", value)
|
||||
updateTransaction(
|
||||
transaction.id,
|
||||
"pagadorId",
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -503,7 +548,8 @@ export function MassAddDialog({
|
||||
{transaction.pagadorId &&
|
||||
(() => {
|
||||
const selectedOption = pagadorOptions.find(
|
||||
(opt) => opt.value === transaction.pagadorId,
|
||||
(opt) =>
|
||||
opt.value === transaction.pagadorId,
|
||||
);
|
||||
return selectedOption ? (
|
||||
<PagadorSelectContent
|
||||
@@ -516,7 +562,10 @@ export function MassAddDialog({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pagadorOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<PagadorSelectContent
|
||||
label={option.label}
|
||||
avatarUrl={option.avatarUrl}
|
||||
@@ -614,5 +663,15 @@ export function MassAddDialog({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<FaturaWarningDialog
|
||||
warning={faturaWarning}
|
||||
onConfirm={(nextPeriod) => {
|
||||
setPeriod(nextPeriod);
|
||||
setFaturaWarning(null);
|
||||
}}
|
||||
onCancel={() => setFaturaWarning(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user