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, "when": 1782051007412,
"tag": "0031_lame_cerise", "tag": "0031_lame_cerise",
"breakpoints": true "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, filters: searchFilters,
slugMaps, slugMaps,
accountId: account.id, accountId: account.id,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
}); });
const transactionsPage = await fetchAccountTransactionsPage( const transactionsPage = await fetchAccountTransactionsPage(

View File

@@ -82,6 +82,8 @@ export default async function Page({ params, searchParams }: PageProps) {
filters: searchFilters, filters: searchFilters,
slugMaps, slugMaps,
cardId: card.id, cardId: card.id,
hideAnticipatedInstallments:
userPreferences?.hideAnticipatedInstallments ?? false,
}); });
const transactionRows = await fetchCardTransactions(filters); 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 periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParam); const { period: selectedPeriod } = parsePeriodParam(periodoParam);
const [detail, filterSources, estabelecimentos, userPreferences] = const [filterSources, estabelecimentos, userPreferences] = await Promise.all([
await Promise.all([
fetchCategoryDetails(userId, categoryId, selectedPeriod),
fetchTransactionFilterSources(userId), fetchTransactionFilterSources(userId),
fetchRecentEstablishments(userId), fetchRecentEstablishments(userId),
fetchUserPreferences(userId), fetchUserPreferences(userId),
]); ]);
const detail = await fetchCategoryDetails(
userId,
categoryId,
selectedPeriod,
userPreferences?.hideAnticipatedInstallments ?? false,
);
if (!detail) { if (!detail) {
notFound(); notFound();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,6 +43,7 @@ interface PreferencesFormProps {
transactionsColumnOrder: string[] | null; transactionsColumnOrder: string[] | null;
attachmentMaxSizeMb: number; attachmentMaxSizeMb: number;
showTransactionSummary: boolean; showTransactionSummary: boolean;
hideAnticipatedInstallments: boolean;
} }
function SortableColumnItem({ id }: { id: string }) { function SortableColumnItem({ id }: { id: string }) {
@@ -87,6 +88,7 @@ export function PreferencesForm({
transactionsColumnOrder: initialColumnOrder, transactionsColumnOrder: initialColumnOrder,
attachmentMaxSizeMb: initialAttachmentMaxSizeMb, attachmentMaxSizeMb: initialAttachmentMaxSizeMb,
showTransactionSummary: initialShowTransactionSummary, showTransactionSummary: initialShowTransactionSummary,
hideAnticipatedInstallments: initialHideAnticipatedInstallments,
}: PreferencesFormProps) { }: PreferencesFormProps) {
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
@@ -109,6 +111,8 @@ export function PreferencesForm({
const [showTransactionSummary, setShowTransactionSummary] = useState( const [showTransactionSummary, setShowTransactionSummary] = useState(
initialShowTransactionSummary, initialShowTransactionSummary,
); );
const [hideAnticipatedInstallments, setHideAnticipatedInstallments] =
useState(initialHideAnticipatedInstallments);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
@@ -135,6 +139,7 @@ export function PreferencesForm({
transactionsColumnOrder: columnOrder, transactionsColumnOrder: columnOrder,
attachmentMaxSizeMb, attachmentMaxSizeMb,
showTransactionSummary, showTransactionSummary,
hideAnticipatedInstallments,
}); });
if (result.success) { if (result.success) {
@@ -198,6 +203,26 @@ export function PreferencesForm({
<Separator /> <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"> <section className="space-y-2 max-w-md">
<Label className="text-sm">Ordem das colunas</Label> <Label className="text-sm">Ordem das colunas</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View File

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

View File

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

View File

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