feat(lancamentos): oculta parcelas antecipadas por preferencia

This commit is contained in:
Felipe Coutinho
2026-06-27 14:22:51 -03:00
parent d06bac5624
commit 32b190ab4e
16 changed files with 3088 additions and 10 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE "preferencias_usuario" ADD COLUMN "ocultar_parcelas_antecipadas" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -218,6 +218,13 @@
"when": 1782051007412,
"tag": "0031_lame_cerise",
"breakpoints": true
},
{
"idx": 32,
"version": "7",
"when": 1782569103402,
"tag": "0032_bumpy_spencer_smythe",
"breakpoints": true
}
]
}

View File

@@ -109,6 +109,8 @@ export default async function Page({ params, searchParams }: PageProps) {
filters: searchFilters,
slugMaps,
accountId: account.id,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
});
const transactionsPage = await fetchAccountTransactionsPage(

View File

@@ -82,6 +82,8 @@ export default async function Page({ params, searchParams }: PageProps) {
filters: searchFilters,
slugMaps,
cardId: card.id,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
});
const transactionRows = await fetchCardTransactions(filters);

View File

@@ -41,13 +41,17 @@ export default async function Page({ params, searchParams }: PageProps) {
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
const [detail, filterSources, estabelecimentos, userPreferences] =
await Promise.all([
fetchCategoryDetails(userId, categoryId, selectedPeriod),
const [filterSources, estabelecimentos, userPreferences] = await Promise.all([
fetchTransactionFilterSources(userId),
fetchRecentEstablishments(userId),
fetchUserPreferences(userId),
]);
const detail = await fetchCategoryDetails(
userId,
categoryId,
selectedPeriod,
userPreferences?.hideAnticipatedInstallments ?? false,
);
if (!detail) {
notFound();

View File

@@ -131,6 +131,7 @@ export default async function Page({ params, searchParams }: PageProps) {
...EMPTY_FILTERS,
searchFilter: allSearchFilters.searchFilter, // Permitir busca mesmo em modo read-only
};
const userPreferences = await fetchUserPreferences(userId);
let filterSources: Awaited<
ReturnType<typeof fetchTransactionFilterSources>
@@ -163,6 +164,8 @@ export default async function Page({ params, searchParams }: PageProps) {
filters: searchFilters,
slugMaps,
payerId: pagador.id,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
});
const sharesPromise = canEdit
@@ -184,7 +187,6 @@ export default async function Page({ params, searchParams }: PageProps) {
shareRows,
currentUserShare,
estabelecimentos,
userPreferences,
] = await Promise.all([
fetchPayerTransactions(filters),
fetchPayerMonthlyBreakdown({
@@ -220,7 +222,6 @@ export default async function Page({ params, searchParams }: PageProps) {
sharesPromise,
currentUserSharePromise,
fetchRecentEstablishments(userId),
fetchUserPreferences(userId),
]);
const mappedTransactions = mapTransactionsData(transactionRows);

View File

@@ -85,6 +85,9 @@ export default async function Page() {
showTransactionSummary={
userPreferences?.showTransactionSummary ?? true
}
hideAnticipatedInstallments={
userPreferences?.hideAnticipatedInstallments ?? false
}
/>
</div>
</Card>

View File

@@ -53,6 +53,8 @@ export default async function Page({ searchParams }: PageProps) {
period: selectedPeriod,
filters: searchFilters,
slugMaps,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
});
const [transactionsPage, estabelecimentos] = await Promise.all([

View File

@@ -157,6 +157,9 @@ export const userPreferences = pgTable("preferencias_usuario", {
showTransactionSummary: boolean("mostrar_resumo_lancamento")
.notNull()
.default(true),
hideAnticipatedInstallments: boolean("ocultar_parcelas_antecipadas")
.notNull()
.default(false),
dashboardWidgets: jsonb("dashboard_widgets").$type<{
order: string[];
hidden: string[];

View File

@@ -36,6 +36,7 @@ export async function fetchCategoryDetails(
userId: string,
categoryId: string,
period: string,
hideAnticipatedInstallments = false,
): Promise<CategoryDetailData | null> {
const category = await db.query.categories.findFirst({
where: and(eq(categories.userId, userId), eq(categories.id, categoryId)),
@@ -63,6 +64,14 @@ export async function fetchCategoryDetails(
eq(transactions.transactionType, transactionType),
eq(transactions.period, period),
eq(transactions.payerId, adminPayerId),
...(hideAnticipatedInstallments
? [
or(
isNull(transactions.isAnticipated),
eq(transactions.isAnticipated, false),
),
]
: []),
...(isInvoiceCategory ? [] : [sanitizedNote]),
),
with: {

View File

@@ -69,6 +69,7 @@ const updatePreferencesSchema = z.object({
transactionsColumnOrder: z.array(z.string()).nullable(),
attachmentMaxSizeMb: z.number().int().min(1).max(100),
showTransactionSummary: z.boolean(),
hideAnticipatedInstallments: z.boolean(),
});
type ResettableUser = {
@@ -584,6 +585,7 @@ export async function updatePreferencesAction(
transactionsColumnOrder: validated.transactionsColumnOrder,
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
showTransactionSummary: validated.showTransactionSummary,
hideAnticipatedInstallments: validated.hideAnticipatedInstallments,
updatedAt: new Date(),
})
.where(eq(schema.userPreferences.userId, session.user.id));
@@ -595,6 +597,7 @@ export async function updatePreferencesAction(
transactionsColumnOrder: validated.transactionsColumnOrder,
attachmentMaxSizeMb: validated.attachmentMaxSizeMb,
showTransactionSummary: validated.showTransactionSummary,
hideAnticipatedInstallments: validated.hideAnticipatedInstallments,
});
}

View File

@@ -43,6 +43,7 @@ interface PreferencesFormProps {
transactionsColumnOrder: string[] | null;
attachmentMaxSizeMb: number;
showTransactionSummary: boolean;
hideAnticipatedInstallments: boolean;
}
function SortableColumnItem({ id }: { id: string }) {
@@ -87,6 +88,7 @@ export function PreferencesForm({
transactionsColumnOrder: initialColumnOrder,
attachmentMaxSizeMb: initialAttachmentMaxSizeMb,
showTransactionSummary: initialShowTransactionSummary,
hideAnticipatedInstallments: initialHideAnticipatedInstallments,
}: PreferencesFormProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
@@ -109,6 +111,8 @@ export function PreferencesForm({
const [showTransactionSummary, setShowTransactionSummary] = useState(
initialShowTransactionSummary,
);
const [hideAnticipatedInstallments, setHideAnticipatedInstallments] =
useState(initialHideAnticipatedInstallments);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
@@ -135,6 +139,7 @@ export function PreferencesForm({
transactionsColumnOrder: columnOrder,
attachmentMaxSizeMb,
showTransactionSummary,
hideAnticipatedInstallments,
});
if (result.success) {
@@ -198,6 +203,26 @@ export function PreferencesForm({
<Separator />
<section className="flex items-center justify-between max-w-md gap-4">
<div className="space-y-2">
<Label htmlFor="hide-anticipated-installments" className="text-sm">
Ocultar parcelas antecipadas
</Label>
<p className="text-sm text-muted-foreground">
Quando ativo, parcelas antecipadas não aparecem na tabela de
lançamentos.
</p>
</div>
<Switch
id="hide-anticipated-installments"
checked={hideAnticipatedInstallments}
onCheckedChange={setHideAnticipatedInstallments}
disabled={isPending}
/>
</section>
<Separator />
<section className="space-y-2 max-w-md">
<Label className="text-sm">Ordem das colunas</Label>
<p className="text-sm text-muted-foreground">

View File

@@ -7,6 +7,7 @@ interface UserPreferences {
transactionsColumnOrder: string[] | null;
attachmentMaxSizeMb: number;
showTransactionSummary: boolean;
hideAnticipatedInstallments: boolean;
}
interface ApiToken {
@@ -36,6 +37,8 @@ export async function fetchUserPreferences(
transactionsColumnOrder: schema.userPreferences.transactionsColumnOrder,
attachmentMaxSizeMb: schema.userPreferences.attachmentMaxSizeMb,
showTransactionSummary: schema.userPreferences.showTransactionSummary,
hideAnticipatedInstallments:
schema.userPreferences.hideAnticipatedInstallments,
})
.from(schema.userPreferences)
.where(eq(schema.userPreferences.userId, userId))

View File

@@ -2,6 +2,7 @@
import { z } from "zod";
import { fetchAccountTransactions } from "@/features/accounts/statement-queries";
import { fetchUserPreferences } from "@/features/settings/queries";
import type { TransactionsExportContext } from "@/features/transactions/lib/export-types";
import {
buildSluggedFilters,
@@ -60,7 +61,10 @@ export async function exportTransactionsDataAction(
try {
const userId = await getUserId();
const validated = exportTransactionsSchema.parse(input);
const filterSources = await fetchTransactionFilterSources(userId);
const [filterSources, userPreferences] = await Promise.all([
fetchTransactionFilterSources(userId),
fetchUserPreferences(userId),
]);
const sluggedFilters = buildSluggedFilters(filterSources);
const slugMaps = buildSlugMaps(sluggedFilters);
@@ -72,6 +76,8 @@ export async function exportTransactionsDataAction(
accountId: validated.accountId ?? undefined,
cardId: validated.cardId ?? undefined,
payerId: validated.payerId ?? undefined,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
});
const rows =

View File

@@ -6,6 +6,7 @@ import {
ilike,
inArray,
isNotNull,
isNull,
lte,
or,
sql,
@@ -384,6 +385,7 @@ export const buildTransactionWhere = ({
cardId,
accountId,
payerId,
hideAnticipatedInstallments = false,
}: {
userId: string;
period: string;
@@ -392,6 +394,7 @@ export const buildTransactionWhere = ({
cardId?: string;
accountId?: string;
payerId?: string;
hideAnticipatedInstallments?: boolean;
}): SQL[] => {
const where: SQL[] = [eq(transactions.userId, userId)];
@@ -421,6 +424,15 @@ export const buildTransactionWhere = ({
where.push(eq(transactions.payerId, payerId));
}
if (hideAnticipatedInstallments) {
where.push(
or(
isNull(transactions.isAnticipated),
eq(transactions.isAnticipated, false),
) as SQL,
);
}
if (cardId) {
where.push(eq(transactions.cardId, cardId));
}