mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(lancamentos): aprimora parcelamentos e protecoes
This commit is contained in:
@@ -43,6 +43,15 @@ type PageProps = {
|
||||
const capitalize = (value: string) =>
|
||||
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
||||
|
||||
const resolveDefaultPaymentMethod = (
|
||||
accountType: string | null | undefined,
|
||||
) => {
|
||||
if (accountType === "Dinheiro") return "Dinheiro";
|
||||
if (accountType === "Pré-Pago | VR/VA") return "Pré-Pago | VR/VA";
|
||||
|
||||
return "Pix";
|
||||
};
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
await connection();
|
||||
const { accountId } = await params;
|
||||
@@ -197,7 +206,11 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
accountId: account.id,
|
||||
settledOnly: true,
|
||||
}}
|
||||
allowCreate={false}
|
||||
allowCreate
|
||||
defaultAccountId={account.id}
|
||||
defaultPaymentMethod={resolveDefaultPaymentMethod(
|
||||
account.accountType,
|
||||
)}
|
||||
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
|
||||
@@ -130,7 +130,7 @@ export function InstallmentAnalysisPage({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Card de resumo principal */}
|
||||
<Card className="border-none bg-primary/10 dark:bg-primary/10">
|
||||
<Card className="border-none bg-primary/10 shadow-none">
|
||||
<CardContent className="flex flex-col items-start justify-center gap-2 py-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Se você pagar tudo que está selecionado:
|
||||
|
||||
@@ -64,8 +64,8 @@ export function InstallmentGroupCard({
|
||||
const hasSelection = selectedInstallments.size > 0;
|
||||
|
||||
const progress =
|
||||
group.totalInstallments > 0
|
||||
? (group.paidInstallments / group.totalInstallments) * 100
|
||||
group.trackedInstallments > 0
|
||||
? (group.paidInstallments / group.trackedInstallments) * 100
|
||||
: 0;
|
||||
|
||||
const selectedAmount = group.pendingInstallments
|
||||
@@ -83,6 +83,10 @@ export function InstallmentGroupCard({
|
||||
);
|
||||
const cardLogoSrc = resolveLogoSrc(group.cartaoLogo);
|
||||
const cardName = group.cartaoName ?? "Compra parcelada";
|
||||
const untrackedLabel =
|
||||
group.untrackedInstallments === 1
|
||||
? "1 parcela anterior fora do acompanhamento"
|
||||
: `${group.untrackedInstallments} parcelas anteriores fora do acompanhamento`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -153,7 +157,7 @@ export function InstallmentGroupCard({
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-primary/5 mb-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground font-medium">
|
||||
Valor total
|
||||
Valor acompanhado
|
||||
</p>
|
||||
<MoneyValues
|
||||
amount={totalAmount}
|
||||
@@ -180,8 +184,8 @@ export function InstallmentGroupCard({
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<RiCheckboxCircleFill className="size-3.5 text-success" />
|
||||
<span>
|
||||
{group.paidInstallments} de {group.totalInstallments} parcelas
|
||||
pagas
|
||||
{group.paidInstallments} de {group.trackedInstallments}{" "}
|
||||
parcelas acompanhadas pagas
|
||||
</span>
|
||||
</div>
|
||||
{unpaidCount > 0 && (
|
||||
@@ -198,6 +202,9 @@ export function InstallmentGroupCard({
|
||||
className="h-2.5 bg-muted"
|
||||
indicatorClassName="bg-success"
|
||||
/>
|
||||
{group.untrackedInstallments > 0 && (
|
||||
<p className="text-xs text-muted-foreground">{untrackedLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Valor selecionado */}
|
||||
|
||||
@@ -51,6 +51,9 @@ export type InstallmentGroup = {
|
||||
cartaoDueDay: string | null;
|
||||
cartaoLogo: string | null;
|
||||
totalInstallments: number;
|
||||
trackedStartInstallment: number;
|
||||
trackedInstallments: number;
|
||||
untrackedInstallments: number;
|
||||
paidInstallments: number;
|
||||
pendingInstallments: InstallmentDetail[];
|
||||
totalPendingAmount: number;
|
||||
@@ -153,6 +156,12 @@ export async function fetchInstallmentAnalysis(
|
||||
cartaoDueDay: row.cartaoDueDay,
|
||||
cartaoLogo: row.cartaoLogo,
|
||||
totalInstallments: row.installmentCount ?? 0,
|
||||
trackedStartInstallment: installmentDetail.currentInstallment,
|
||||
trackedInstallments: 1,
|
||||
untrackedInstallments: Math.max(
|
||||
0,
|
||||
installmentDetail.currentInstallment - 1,
|
||||
),
|
||||
paidInstallments: 0,
|
||||
pendingInstallments: [installmentDetail],
|
||||
totalPendingAmount: amount,
|
||||
@@ -168,7 +177,13 @@ export async function fetchInstallmentAnalysis(
|
||||
const paidCount = group.pendingInstallments.filter(
|
||||
(i) => i.isSettled,
|
||||
).length;
|
||||
const trackedStartInstallment = Math.min(
|
||||
...group.pendingInstallments.map((i) => i.currentInstallment),
|
||||
);
|
||||
group.paidInstallments = paidCount;
|
||||
group.trackedStartInstallment = trackedStartInstallment;
|
||||
group.trackedInstallments = group.pendingInstallments.length;
|
||||
group.untrackedInstallments = Math.max(0, trackedStartInstallment - 1);
|
||||
return group;
|
||||
})
|
||||
// Filtrar apenas séries que têm pelo menos uma parcela em aberto (não paga)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TRANSACTION_CONDITIONS,
|
||||
TRANSACTION_TYPES,
|
||||
} from "@/features/transactions/lib/constants";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { handleActionError } from "@/shared/lib/actions/helpers";
|
||||
import { getUser } from "@/shared/lib/auth/server";
|
||||
import { db } from "@/shared/lib/db";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
fetchOwnedPayerIds,
|
||||
formatPaidInvoicePeriods,
|
||||
getPaidInvoicePeriods,
|
||||
isInitialBalanceTransaction,
|
||||
type MassAddInput,
|
||||
massAddSchema,
|
||||
resolvePeriod,
|
||||
@@ -47,6 +49,19 @@ const getPeriodOffset = (basePeriod: string, targetPeriod: string) => {
|
||||
return (target.year - base.year) * 12 + (target.month - base.month);
|
||||
};
|
||||
|
||||
type ProtectedTransactionCandidate = {
|
||||
note: string | null;
|
||||
transactionType: string | null;
|
||||
condition: string | null;
|
||||
paymentMethod: string | null;
|
||||
};
|
||||
|
||||
const isProtectedTransaction = (
|
||||
record: ProtectedTransactionCandidate,
|
||||
): boolean =>
|
||||
Boolean(record.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) ||
|
||||
isInitialBalanceTransaction(record);
|
||||
|
||||
export async function deleteTransactionBulkAction(
|
||||
input: DeleteBulkInput,
|
||||
): Promise<ActionResult> {
|
||||
@@ -61,6 +76,9 @@ export async function deleteTransactionBulkAction(
|
||||
seriesId: true,
|
||||
period: true,
|
||||
condition: true,
|
||||
transactionType: true,
|
||||
paymentMethod: true,
|
||||
note: true,
|
||||
},
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
@@ -79,6 +97,13 @@ export async function deleteTransactionBulkAction(
|
||||
};
|
||||
}
|
||||
|
||||
if (isProtectedTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser removidos em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
let scopeFilter: ReturnType<typeof and>;
|
||||
let successMessage: string;
|
||||
|
||||
@@ -171,6 +196,7 @@ export async function updateTransactionBulkAction(
|
||||
purchaseDate: true,
|
||||
payerId: true,
|
||||
cardId: true,
|
||||
note: true,
|
||||
},
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
@@ -189,6 +215,13 @@ export async function updateTransactionBulkAction(
|
||||
};
|
||||
}
|
||||
|
||||
if (isProtectedTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser atualizados em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
const baseUpdatePayload: Record<string, unknown> = {
|
||||
name: data.name,
|
||||
categoryId: data.categoryId ?? null,
|
||||
@@ -753,6 +786,13 @@ export async function deleteMultipleTransactionsAction(
|
||||
return { success: false, error: "Nenhum lançamento encontrado." };
|
||||
}
|
||||
|
||||
if (existing.some(isProtectedTransaction)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos protegidos não podem ser removidos em massa.",
|
||||
};
|
||||
}
|
||||
|
||||
const linkedAttachments = await db
|
||||
.select({ id: attachments.id, fileKey: attachments.fileKey })
|
||||
.from(transactionAttachments)
|
||||
|
||||
@@ -335,6 +335,12 @@ const baseFields = z.object({
|
||||
.min(1, "Selecione uma quantidade válida.")
|
||||
.max(60, "Selecione uma quantidade válida.")
|
||||
.optional(),
|
||||
startInstallment: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Selecione uma parcela válida.")
|
||||
.max(60, "Selecione uma parcela válida.")
|
||||
.optional(),
|
||||
recurrenceCount: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
@@ -415,6 +421,15 @@ const refineLancamento = (
|
||||
path: ["installmentCount"],
|
||||
message: "Selecione pelo menos duas parcelas.",
|
||||
});
|
||||
} else if (
|
||||
data.startInstallment &&
|
||||
data.startInstallment > data.installmentCount
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["startInstallment"],
|
||||
message: "A parcela inicial não pode ser maior que o total.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,24 +666,27 @@ export const buildTransactionRecords = ({
|
||||
|
||||
if (data.condition === "Parcelado") {
|
||||
const installmentTotal = data.installmentCount ?? 0;
|
||||
const startInstallment = data.startInstallment ?? 1;
|
||||
const amountsByShare = shares.map((share) =>
|
||||
splitAmount(share.amountCents, installmentTotal),
|
||||
);
|
||||
|
||||
for (
|
||||
let installment = 0;
|
||||
installment < installmentTotal;
|
||||
installment += 1
|
||||
let index = 0;
|
||||
index <= installmentTotal - startInstallment;
|
||||
index += 1
|
||||
) {
|
||||
const installmentPeriod = addMonthsToPeriod(period, installment);
|
||||
const currentInstallment = startInstallment + index;
|
||||
const installmentPeriod = addMonthsToPeriod(period, index);
|
||||
const installmentDueDate = dueDate
|
||||
? addMonthsToDate(dueDate, installment)
|
||||
? addMonthsToDate(dueDate, index)
|
||||
: null;
|
||||
const splitGroupId = cycleSplitGroupId();
|
||||
|
||||
shares.forEach((share, shareIndex) => {
|
||||
const amountCents = amountsByShare[shareIndex]?.[installment] ?? 0;
|
||||
const settled = resolveSettledValue(installment);
|
||||
const amountCents =
|
||||
amountsByShare[shareIndex]?.[currentInstallment - 1] ?? 0;
|
||||
const settled = resolveSettledValue(index);
|
||||
records.push({
|
||||
...basePayload,
|
||||
amount: centsToDecimalString(amountCents * amountSign),
|
||||
@@ -677,7 +695,7 @@ export const buildTransactionRecords = ({
|
||||
period: installmentPeriod,
|
||||
isSettled: settled,
|
||||
installmentCount: installmentTotal,
|
||||
currentInstallment: installment + 1,
|
||||
currentInstallment,
|
||||
recurrenceCount: null,
|
||||
dueDate: installmentDueDate,
|
||||
splitGroupId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
transactionAttachments,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { handleActionError } from "@/shared/lib/actions/helpers";
|
||||
import { getUser } from "@/shared/lib/auth/server";
|
||||
import { db } from "@/shared/lib/db";
|
||||
@@ -230,13 +231,6 @@ export async function updateTransactionAction(
|
||||
eq(transactions.id, data.id),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
with: {
|
||||
category: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as
|
||||
| {
|
||||
id: string;
|
||||
@@ -248,7 +242,6 @@ export async function updateTransactionAction(
|
||||
accountId: string | null;
|
||||
cardId: string | null;
|
||||
categoryId: string | null;
|
||||
category: { name: string } | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -256,14 +249,17 @@ export async function updateTransactionAction(
|
||||
return { success: false, error: "Lançamento não encontrado." };
|
||||
}
|
||||
|
||||
const categoriasProtegidasEdicao = ["Saldo inicial", "Pagamentos"];
|
||||
if (
|
||||
existing.category?.name &&
|
||||
categoriasProtegidasEdicao.includes(existing.category.name)
|
||||
) {
|
||||
if (existing.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Lançamentos com a categoria '${existing.category.name}' não podem ser editados.`,
|
||||
error: "Pagamentos automáticos de fatura não podem ser editados.",
|
||||
};
|
||||
}
|
||||
|
||||
if (isInitialBalanceTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos de saldo inicial não podem ser editados.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,13 +387,6 @@ export async function deleteTransactionAction(
|
||||
eq(transactions.id, data.id),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
with: {
|
||||
category: {
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as
|
||||
| {
|
||||
id: string;
|
||||
@@ -411,7 +400,6 @@ export async function deleteTransactionAction(
|
||||
period: string;
|
||||
note: string | null;
|
||||
categoryId: string | null;
|
||||
category: { name: string } | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -419,14 +407,17 @@ export async function deleteTransactionAction(
|
||||
return { success: false, error: "Lançamento não encontrado." };
|
||||
}
|
||||
|
||||
const categoriasProtegidasRemocao = ["Saldo inicial", "Pagamentos"];
|
||||
if (
|
||||
existing.category?.name &&
|
||||
categoriasProtegidasRemocao.includes(existing.category.name)
|
||||
) {
|
||||
if (existing.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Lançamentos com a categoria '${existing.category.name}' não podem ser removidos.`,
|
||||
error: "Pagamentos automáticos de fatura não podem ser removidos.",
|
||||
};
|
||||
}
|
||||
|
||||
if (isInitialBalanceTransaction(existing)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Lançamentos de saldo inicial não podem ser removidos.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TRANSACTION_CONDITIONS } from "@/features/transactions/lib/constants";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/shared/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -14,6 +20,61 @@ import { cn } from "@/shared/utils/ui";
|
||||
import { ConditionSelectContent } from "../../select-items";
|
||||
import type { ConditionSectionProps } from "./transaction-dialog-types";
|
||||
|
||||
function InlineStartInstallmentPicker({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
options: number[];
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selected = Number(value || "1");
|
||||
const selectedLabel =
|
||||
!Number.isNaN(selected) && selected > 0
|
||||
? `${selected}ª parcela`
|
||||
: "1ª parcela";
|
||||
const disabled = options.length === 0;
|
||||
|
||||
return (
|
||||
<div className="ml-1">
|
||||
<span className="text-xs text-muted-foreground">Começar em </span>
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-xs text-primary underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:text-muted-foreground disabled:hover:no-underline"
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedLabel}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 p-1" align="start">
|
||||
<div className="max-h-56 overflow-y-auto">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground",
|
||||
option === selected && "font-medium text-primary",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(String(option));
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{option}ª parcela
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConditionSection({
|
||||
formState,
|
||||
onFieldChange,
|
||||
@@ -37,11 +98,17 @@ export function ConditionSection({
|
||||
const installmentSummary =
|
||||
showInstallments &&
|
||||
formState.installmentCount &&
|
||||
amount &&
|
||||
!Number.isNaN(installmentCount) &&
|
||||
installmentCount > 0
|
||||
? getInstallmentLabel(installmentCount)
|
||||
: null;
|
||||
const startInstallmentOptions =
|
||||
showInstallments &&
|
||||
formState.installmentCount &&
|
||||
!Number.isNaN(installmentCount) &&
|
||||
installmentCount > 0
|
||||
? Array.from({ length: installmentCount }, (_, index) => index + 1)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
||||
@@ -96,6 +163,11 @@ export function ConditionSection({
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InlineStartInstallmentPicker
|
||||
value={formState.startInstallment}
|
||||
options={startInstallmentOptions}
|
||||
onChange={(value) => onFieldChange("startInstallment", value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface TransactionDialogProps {
|
||||
estabelecimentos: string[];
|
||||
transaction?: TransactionItem;
|
||||
defaultPeriod?: string;
|
||||
defaultAccountId?: string | null;
|
||||
defaultCardId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
|
||||
@@ -65,6 +65,7 @@ export function TransactionDialog({
|
||||
estabelecimentos,
|
||||
transaction,
|
||||
defaultPeriod,
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -88,6 +89,7 @@ export function TransactionDialog({
|
||||
|
||||
const [formState, setFormState] = useState<FormState>(() =>
|
||||
buildTransactionInitialState(transaction, defaultPayerId, defaultPeriod, {
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -112,6 +114,7 @@ export function TransactionDialog({
|
||||
defaultPayerId,
|
||||
defaultPeriod,
|
||||
{
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -151,6 +154,7 @@ export function TransactionDialog({
|
||||
transaction,
|
||||
defaultPayerId,
|
||||
defaultPeriod,
|
||||
defaultAccountId,
|
||||
defaultCardId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -327,6 +331,12 @@ export function TransactionDialog({
|
||||
formState.condition === "Parcelado" && formState.installmentCount
|
||||
? Number(formState.installmentCount)
|
||||
: undefined,
|
||||
startInstallment:
|
||||
mode === "create" &&
|
||||
formState.condition === "Parcelado" &&
|
||||
formState.startInstallment
|
||||
? Number(formState.startInstallment)
|
||||
: undefined,
|
||||
recurrenceCount:
|
||||
formState.condition === "Recorrente" && formState.recurrenceCount
|
||||
? Number(formState.recurrenceCount)
|
||||
|
||||
@@ -63,6 +63,7 @@ interface TransactionsPageProps {
|
||||
categoryFilterOptions: TransactionFilterOption[];
|
||||
accountCardFilterOptions: AccountCardFilterOption[];
|
||||
selectedPeriod: string;
|
||||
defaultAccountId?: string | null;
|
||||
estabelecimentos: string[];
|
||||
allowCreate?: boolean;
|
||||
noteAsColumn?: boolean;
|
||||
@@ -96,6 +97,7 @@ export function TransactionsPage({
|
||||
categoryFilterOptions,
|
||||
accountCardFilterOptions,
|
||||
selectedPeriod,
|
||||
defaultAccountId,
|
||||
estabelecimentos,
|
||||
allowCreate = true,
|
||||
noteAsColumn = false,
|
||||
@@ -562,6 +564,7 @@ export function TransactionsPage({
|
||||
categoryOptions={categoryOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
defaultCardId={defaultCardId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCardSelection={lockCardSelection}
|
||||
@@ -585,6 +588,7 @@ export function TransactionsPage({
|
||||
categoryOptions={categoryOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
defaultCardId={defaultCardId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCardSelection={lockCardSelection}
|
||||
@@ -648,6 +652,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={transactionToCopy ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
/>
|
||||
|
||||
@@ -669,6 +674,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={transactionToImport ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
isImporting={true}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
/>
|
||||
@@ -697,6 +703,7 @@ export function TransactionsPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
transaction={selectedTransaction ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultAccountId={defaultAccountId}
|
||||
onBulkEditRequest={handleBulkEditRequest}
|
||||
onSplitEditRequest={handleSplitEditRequest}
|
||||
maxSizeMb={attachmentMaxSizeMb}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
|
||||
import { RiCalendarCheckLine } from "@remixicon/react";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { useTransition } from "react";
|
||||
@@ -164,16 +164,14 @@ export function AnticipationCard({
|
||||
onClick={handleViewLancamento}
|
||||
disabled={isPending}
|
||||
>
|
||||
<RiEyeLine className="mr-2 size-4" />
|
||||
Ver Lançamento
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
{canCancel && (
|
||||
<ConfirmActionDialog
|
||||
trigger={
|
||||
<Button variant="destructive" size="sm" disabled={isPending}>
|
||||
<RiCloseLine className="mr-2 size-4" />
|
||||
Cancelar Antecipação
|
||||
Desfazer Antecipação
|
||||
</Button>
|
||||
}
|
||||
title="Cancelar antecipação?"
|
||||
|
||||
@@ -426,7 +426,7 @@ function buildColumns({
|
||||
const initial = displayName.charAt(0).toUpperCase() || "?";
|
||||
const content = (
|
||||
<>
|
||||
<Avatar className="size-7">
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={avatarSrc} alt={`Avatar de ${label}`} />
|
||||
<AvatarFallback className="text-xs font-medium uppercase">
|
||||
{initial}
|
||||
@@ -477,15 +477,21 @@ function buildColumns({
|
||||
const content = (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{logoSrc && (
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={`Logo de ${label}`}
|
||||
width={30}
|
||||
height={30}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={logoSrc} alt={`Logo de ${label}`} />
|
||||
<AvatarFallback className="text-xs font-medium uppercase">
|
||||
{label}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<span className="truncate">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate underline-offset-2",
|
||||
isOwnData && href && "group-hover:underline",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -503,7 +509,7 @@ function buildColumns({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={href} className="hover:underline">
|
||||
<Link href={href} className="group">
|
||||
{content}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
@@ -654,14 +660,14 @@ function buildColumns({
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.categoriaName !== "Pagamentos" &&
|
||||
{!row.original.readonly &&
|
||||
row.original.userId === currentUserId && (
|
||||
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Copiar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.categoriaName !== "Pagamentos" &&
|
||||
{!row.original.readonly &&
|
||||
row.original.userId !== currentUserId && (
|
||||
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
|
||||
@@ -174,7 +174,7 @@ export function TransactionsTable({
|
||||
: getPaginationRowModel(),
|
||||
manualPagination: isServerPaginated,
|
||||
pageCount: serverPagination?.totalPages,
|
||||
enableRowSelection: true,
|
||||
enableRowSelection: (row) => !row.original.readonly,
|
||||
});
|
||||
|
||||
const rowModel = table.getRowModel();
|
||||
|
||||
@@ -80,6 +80,7 @@ export type TransactionFormState = {
|
||||
cardId: string | undefined;
|
||||
categoryId: string | undefined;
|
||||
installmentCount: string;
|
||||
startInstallment: string;
|
||||
recurrenceCount: string;
|
||||
dueDate: string;
|
||||
boletoPaymentDate: string;
|
||||
@@ -92,6 +93,7 @@ export type TransactionFormState = {
|
||||
*/
|
||||
type TransactionFormOverrides = {
|
||||
defaultCardId?: string | null;
|
||||
defaultAccountId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
defaultName?: string | null;
|
||||
@@ -178,7 +180,9 @@ export function buildTransactionInitialState(
|
||||
? undefined
|
||||
: isImporting
|
||||
? undefined
|
||||
: (transaction?.accountId ?? undefined),
|
||||
: (transaction?.accountId ??
|
||||
overrides?.defaultAccountId ??
|
||||
undefined),
|
||||
cardId:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? isImporting
|
||||
@@ -191,6 +195,12 @@ export function buildTransactionInitialState(
|
||||
installmentCount: transaction?.installmentCount
|
||||
? String(transaction.installmentCount)
|
||||
: "",
|
||||
startInstallment:
|
||||
isImporting &&
|
||||
transaction?.condition === "Parcelado" &&
|
||||
transaction.currentInstallment
|
||||
? String(transaction.currentInstallment)
|
||||
: "1",
|
||||
recurrenceCount: transaction?.recurrenceCount
|
||||
? String(transaction.recurrenceCount)
|
||||
: "",
|
||||
@@ -252,12 +262,25 @@ export function applyFieldDependencies(
|
||||
if (key === "condition" && typeof value === "string") {
|
||||
if (value !== "Parcelado") {
|
||||
updates.installmentCount = "";
|
||||
updates.startInstallment = "1";
|
||||
}
|
||||
if (value !== "Recorrente") {
|
||||
updates.recurrenceCount = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "installmentCount" && typeof value === "string" && value) {
|
||||
const nextCount = Number.parseInt(value, 10);
|
||||
const currentStart = Number.parseInt(currentState.startInstallment, 10);
|
||||
if (
|
||||
!Number.isNaN(nextCount) &&
|
||||
!Number.isNaN(currentStart) &&
|
||||
currentStart > nextCount
|
||||
) {
|
||||
updates.startInstallment = String(nextCount);
|
||||
}
|
||||
}
|
||||
|
||||
// When payment method changes, adjust related fields
|
||||
if (key === "paymentMethod" && typeof value === "string") {
|
||||
if (value === "Cartão de crédito") {
|
||||
|
||||
@@ -27,7 +27,13 @@ import {
|
||||
TRANSACTION_CONDITIONS,
|
||||
TRANSACTION_TYPES,
|
||||
} from "@/features/transactions/lib/constants";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import {
|
||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||
INITIAL_BALANCE_CONDITION,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
INITIAL_BALANCE_PAYMENT_METHOD,
|
||||
INITIAL_BALANCE_TRANSACTION_TYPE,
|
||||
} from "@/shared/lib/accounts/constants";
|
||||
import {
|
||||
PAYER_ROLE_ADMIN,
|
||||
PAYER_ROLE_THIRD_PARTY,
|
||||
@@ -551,8 +557,10 @@ export const mapTransactionsData = (rows: TransactionRowWithRelations[]) =>
|
||||
hasAttachments: item.hasAttachments ?? false,
|
||||
readonly:
|
||||
Boolean(item.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) ||
|
||||
item.category?.name === "Saldo inicial" ||
|
||||
item.category?.name === "Pagamentos",
|
||||
(item.note === INITIAL_BALANCE_NOTE &&
|
||||
item.transactionType === INITIAL_BALANCE_TRANSACTION_TYPE &&
|
||||
item.condition === INITIAL_BALANCE_CONDITION &&
|
||||
item.paymentMethod === INITIAL_BALANCE_PAYMENT_METHOD),
|
||||
}));
|
||||
|
||||
const sortByLabel = <T extends { label: string }>(items: T[]) =>
|
||||
|
||||
Reference in New Issue
Block a user