feat(lancamentos): aprimora parcelamentos e protecoes

This commit is contained in:
Felipe Coutinho
2026-05-21 13:47:14 +00:00
parent b6659ef66e
commit 4e8f9cc5fa
16 changed files with 275 additions and 66 deletions

View File

@@ -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}

View File

@@ -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:

View File

@@ -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 */}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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.",
};
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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}

View File

@@ -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?"

View File

@@ -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" />

View File

@@ -174,7 +174,7 @@ export function TransactionsTable({
: getPaginationRowModel(),
manualPagination: isServerPaginated,
pageCount: serverPagination?.totalPages,
enableRowSelection: true,
enableRowSelection: (row) => !row.original.readonly,
});
const rowModel = table.getRowModel();

View File

@@ -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") {

View File

@@ -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[]) =>