import type { SelectOption } from "@/components/lancamentos/types"; import { cartoes, categorias, contas, lancamentos, pagadores, } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants"; import { db } from "@/lib/db"; import { LANCAMENTO_CONDITIONS, LANCAMENTO_PAYMENT_METHODS, LANCAMENTO_TRANSACTION_TYPES, } from "@/lib/lancamentos/constants"; import { PAGADOR_ROLE_ADMIN, PAGADOR_ROLE_TERCEIRO } from "@/lib/pagadores/constants"; import type { SQL } from "drizzle-orm"; import { and, eq, ilike, isNotNull, or } from "drizzle-orm"; type PagadorRow = typeof pagadores.$inferSelect; type ContaRow = typeof contas.$inferSelect; type CartaoRow = typeof cartoes.$inferSelect; type CategoriaRow = typeof categorias.$inferSelect; export type ResolvedSearchParams = | Record | undefined; export type LancamentoSearchFilters = { transactionFilter: string | null; conditionFilter: string | null; paymentFilter: string | null; pagadorFilter: string | null; categoriaFilter: string | null; contaCartaoFilter: string | null; searchFilter: string | null; }; type BaseSluggedOption = { id: string; label: string; slug: string; }; type PagadorSluggedOption = BaseSluggedOption & { role: string | null; avatarUrl: string | null; }; type CategoriaSluggedOption = BaseSluggedOption & { type: string | null; icon: string | null; }; type ContaSluggedOption = BaseSluggedOption & { kind: "conta"; logo: string | null; accountType: string | null; }; type CartaoSluggedOption = BaseSluggedOption & { kind: "cartao"; logo: string | null; }; export type SluggedFilters = { pagadorFiltersRaw: PagadorSluggedOption[]; categoriaFiltersRaw: CategoriaSluggedOption[]; contaFiltersRaw: ContaSluggedOption[]; cartaoFiltersRaw: CartaoSluggedOption[]; }; export type SlugMaps = { pagador: Map; categoria: Map; conta: Map; cartao: Map; }; export type FilterOption = { slug: string; label: string; }; export type ContaCartaoFilterOption = FilterOption & { kind: "conta" | "cartao"; }; export type LancamentoOptionSets = { pagadorOptions: SelectOption[]; splitPagadorOptions: SelectOption[]; defaultPagadorId: string | null; contaOptions: SelectOption[]; cartaoOptions: SelectOption[]; categoriaOptions: SelectOption[]; pagadorFilterOptions: FilterOption[]; categoriaFilterOptions: FilterOption[]; contaCartaoFilterOptions: ContaCartaoFilterOption[]; }; export const getSingleParam = ( params: ResolvedSearchParams, key: string ): string | null => { const value = params?.[key]; if (!value) { return null; } return Array.isArray(value) ? value[0] ?? null : value; }; export const extractLancamentoSearchFilters = ( params: ResolvedSearchParams ): LancamentoSearchFilters => ({ transactionFilter: getSingleParam(params, "transacao"), conditionFilter: getSingleParam(params, "condicao"), paymentFilter: getSingleParam(params, "pagamento"), pagadorFilter: getSingleParam(params, "pagador"), categoriaFilter: getSingleParam(params, "categoria"), contaCartaoFilter: getSingleParam(params, "contaCartao"), searchFilter: getSingleParam(params, "q"), }); const normalizeLabel = (value: string | null | undefined) => value?.trim().length ? value.trim() : "Sem descrição"; const slugify = (value: string) => { const base = value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return base || "item"; }; const createSlugGenerator = () => { const seen = new Map(); return (label: string) => { const base = slugify(label); const count = seen.get(base) ?? 0; seen.set(base, count + 1); if (count === 0) { return base; } return `${base}-${count + 1}`; }; }; export const toOption = ( value: string, label: string | null | undefined, role?: string | null, group?: string | null, slug?: string | null, avatarUrl?: string | null, logo?: string | null, icon?: string | null, accountType?: string | null ): SelectOption => ({ value, label: normalizeLabel(label), role: role ?? null, group: group ?? null, slug: slug ?? null, avatarUrl: avatarUrl ?? null, logo: logo ?? null, icon: icon ?? null, accountType: accountType ?? null, }); export const fetchLancamentoFilterSources = async (userId: string) => { const [pagadorRows, contaRows, cartaoRows, categoriaRows] = await Promise.all( [ db.query.pagadores.findMany({ where: eq(pagadores.userId, userId), }), db.query.contas.findMany({ where: (contas, { eq, and }) => and(eq(contas.userId, userId), eq(contas.status, "Ativa")), }), db.query.cartoes.findMany({ where: (cartoes, { eq, and }) => and(eq(cartoes.userId, userId), eq(cartoes.status, "Ativo")), }), db.query.categorias.findMany({ where: eq(categorias.userId, userId), }), ] ); return { pagadorRows, contaRows, cartaoRows, categoriaRows }; }; export const buildSluggedFilters = ({ pagadorRows, categoriaRows, contaRows, cartaoRows, }: { pagadorRows: PagadorRow[]; categoriaRows: CategoriaRow[]; contaRows: ContaRow[]; cartaoRows: CartaoRow[]; }): SluggedFilters => { const pagadorSlugger = createSlugGenerator(); const categoriaSlugger = createSlugGenerator(); const contaCartaoSlugger = createSlugGenerator(); const pagadorFiltersRaw = pagadorRows.map((pagador) => { const label = normalizeLabel(pagador.name); return { id: pagador.id, label, slug: pagadorSlugger(label), role: pagador.role ?? null, avatarUrl: pagador.avatarUrl ?? null, }; }); const categoriaFiltersRaw = categoriaRows.map((categoria) => { const label = normalizeLabel(categoria.name); return { id: categoria.id, label, slug: categoriaSlugger(label), type: categoria.type ?? null, icon: categoria.icon ?? null, }; }); const contaFiltersRaw = contaRows.map((conta) => { const label = normalizeLabel(conta.name); return { id: conta.id, label, slug: contaCartaoSlugger(label), kind: "conta" as const, logo: conta.logo ?? null, accountType: conta.accountType ?? null, }; }); const cartaoFiltersRaw = cartaoRows.map((cartao) => { const label = normalizeLabel(cartao.name); return { id: cartao.id, label, slug: contaCartaoSlugger(label), kind: "cartao" as const, logo: cartao.logo ?? null, }; }); return { pagadorFiltersRaw, categoriaFiltersRaw, contaFiltersRaw, cartaoFiltersRaw, }; }; export const buildSlugMaps = ({ pagadorFiltersRaw, categoriaFiltersRaw, contaFiltersRaw, cartaoFiltersRaw, }: SluggedFilters): SlugMaps => ({ pagador: new Map(pagadorFiltersRaw.map(({ slug, id }) => [slug, id])), categoria: new Map(categoriaFiltersRaw.map(({ slug, id }) => [slug, id])), conta: new Map(contaFiltersRaw.map(({ slug, id }) => [slug, id])), cartao: new Map(cartaoFiltersRaw.map(({ slug, id }) => [slug, id])), }); const isValidTransaction = ( value: string | null ): value is (typeof LANCAMENTO_TRANSACTION_TYPES)[number] => !!value && (LANCAMENTO_TRANSACTION_TYPES as readonly string[]).includes(value ?? ""); const isValidCondition = ( value: string | null ): value is (typeof LANCAMENTO_CONDITIONS)[number] => !!value && (LANCAMENTO_CONDITIONS as readonly string[]).includes(value ?? ""); const isValidPaymentMethod = ( value: string | null ): value is (typeof LANCAMENTO_PAYMENT_METHODS)[number] => !!value && (LANCAMENTO_PAYMENT_METHODS as readonly string[]).includes(value ?? ""); const buildSearchPattern = (value: string | null) => value ? `%${value.trim().replace(/\s+/g, "%")}%` : null; export const buildLancamentoWhere = ({ userId, period, filters, slugMaps, cardId, accountId, pagadorId, }: { userId: string; period: string; filters: LancamentoSearchFilters; slugMaps: SlugMaps; cardId?: string; accountId?: string; pagadorId?: string; }): SQL[] => { const where: SQL[] = [ eq(lancamentos.userId, userId), eq(lancamentos.period, period), ]; if (pagadorId) { where.push(eq(lancamentos.pagadorId, pagadorId)); } if (cardId) { where.push(eq(lancamentos.cartaoId, cardId)); } if (accountId) { where.push(eq(lancamentos.contaId, accountId)); } if (isValidTransaction(filters.transactionFilter)) { where.push(eq(lancamentos.transactionType, filters.transactionFilter)); } if (isValidCondition(filters.conditionFilter)) { where.push(eq(lancamentos.condition, filters.conditionFilter)); } if (isValidPaymentMethod(filters.paymentFilter)) { where.push(eq(lancamentos.paymentMethod, filters.paymentFilter)); } if (!pagadorId && filters.pagadorFilter) { const id = slugMaps.pagador.get(filters.pagadorFilter); if (id) { where.push(eq(lancamentos.pagadorId, id)); } } if (filters.categoriaFilter) { const id = slugMaps.categoria.get(filters.categoriaFilter); if (id) { where.push(eq(lancamentos.categoriaId, id)); } } if (filters.contaCartaoFilter) { const contaId = slugMaps.conta.get(filters.contaCartaoFilter); const relatedCartaoId = contaId ? null : slugMaps.cartao.get(filters.contaCartaoFilter); if (contaId) { where.push(eq(lancamentos.contaId, contaId)); } if (!contaId && relatedCartaoId) { where.push(eq(lancamentos.cartaoId, relatedCartaoId)); } } const searchPattern = buildSearchPattern(filters.searchFilter); if (searchPattern) { where.push( or( ilike(lancamentos.name, searchPattern), ilike(lancamentos.note, searchPattern), ilike(lancamentos.paymentMethod, searchPattern), ilike(lancamentos.condition, searchPattern), and(isNotNull(contas.name), ilike(contas.name, searchPattern)), and(isNotNull(cartoes.name), ilike(cartoes.name, searchPattern)) )! ); } return where; }; type LancamentoRowWithRelations = typeof lancamentos.$inferSelect & { pagador?: PagadorRow | null; conta?: ContaRow | null; cartao?: CartaoRow | null; categoria?: CategoriaRow | null; }; export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) => rows.map((item) => ({ id: item.id, userId: item.userId, name: item.name, purchaseDate: item.purchaseDate?.toISOString() ?? new Date().toISOString(), period: item.period ?? "", transactionType: item.transactionType, amount: Number(item.amount ?? 0), condition: item.condition, paymentMethod: item.paymentMethod, pagadorId: item.pagadorId ?? null, pagadorName: item.pagador?.name ?? null, pagadorAvatar: item.pagador?.avatarUrl ?? null, pagadorRole: item.pagador?.role ?? null, contaId: item.contaId ?? null, contaName: item.conta?.name ?? null, contaLogo: item.conta?.logo ?? null, cartaoId: item.cartaoId ?? null, cartaoName: item.cartao?.name ?? null, cartaoLogo: item.cartao?.logo ?? null, categoriaId: item.categoriaId ?? null, categoriaName: item.categoria?.name ?? null, categoriaType: item.categoria?.type ?? null, categoriaIcon: item.categoria?.icon ?? null, installmentCount: item.installmentCount ?? null, recurrenceCount: item.recurrenceCount ?? null, currentInstallment: item.currentInstallment ?? null, dueDate: item.dueDate ? item.dueDate.toISOString().slice(0, 10) : null, boletoPaymentDate: item.boletoPaymentDate ? item.boletoPaymentDate.toISOString().slice(0, 10) : null, note: item.note ?? null, isSettled: item.isSettled ?? null, isDivided: item.isDivided ?? false, isAnticipated: item.isAnticipated ?? false, anticipationId: item.anticipationId ?? null, seriesId: item.seriesId ?? null, readonly: Boolean(item.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) || item.categoria?.name === "Saldo inicial" || item.categoria?.name === "Pagamentos", })); const sortByLabel = (items: T[]) => items.sort((a, b) => a.label.localeCompare(b.label, "pt-BR", { sensitivity: "base" }) ); export const buildOptionSets = ({ pagadorFiltersRaw, categoriaFiltersRaw, contaFiltersRaw, cartaoFiltersRaw, pagadorRows, limitCartaoId, limitContaId, }: SluggedFilters & { pagadorRows: PagadorRow[]; limitCartaoId?: string; limitContaId?: string; }): LancamentoOptionSets => { const pagadorOptions = sortByLabel( pagadorFiltersRaw.map(({ id, label, role, slug, avatarUrl }) => toOption(id, label, role, undefined, slug, avatarUrl) ) ); const pagadorFilterOptions = sortByLabel( pagadorFiltersRaw.map(({ slug, label, avatarUrl }) => ({ slug, label, avatarUrl, })) ); const defaultPagadorId = pagadorRows.find((pagador) => pagador.role === PAGADOR_ROLE_ADMIN)?.id ?? null; const splitPagadorOptions = pagadorOptions.filter( (option) => option.role === PAGADOR_ROLE_TERCEIRO ); const contaOptionsSource = limitContaId ? contaFiltersRaw.filter((conta) => conta.id === limitContaId) : contaFiltersRaw; const contaOptions = sortByLabel( contaOptionsSource.map(({ id, label, slug, logo, accountType }) => toOption(id, label, undefined, undefined, slug, undefined, logo, undefined, accountType) ) ); const cartaoOptionsSource = limitCartaoId ? cartaoFiltersRaw.filter((cartao) => cartao.id === limitCartaoId) : cartaoFiltersRaw; const cartaoOptions = sortByLabel( cartaoOptionsSource.map(({ id, label, slug, logo }) => toOption(id, label, undefined, undefined, slug, undefined, logo) ) ); const categoriaOptions = sortByLabel( categoriaFiltersRaw.map(({ id, label, type, slug, icon }) => toOption(id, label, undefined, type, slug, undefined, undefined, icon) ) ); const categoriaFilterOptions = sortByLabel( categoriaFiltersRaw.map(({ slug, label, icon }) => ({ slug, label, icon })) ); const contaCartaoFilterOptions = sortByLabel( [...contaFiltersRaw, ...cartaoFiltersRaw] .filter( (option) => (limitCartaoId && option.kind === "cartao" ? option.id === limitCartaoId : true) && (limitContaId && option.kind === "conta" ? option.id === limitContaId : true) ) .map(({ slug, label, kind, logo }) => ({ slug, label, kind, logo })) ); return { pagadorOptions, splitPagadorOptions, defaultPagadorId, contaOptions, cartaoOptions, categoriaOptions, pagadorFilterOptions, categoriaFilterOptions, contaCartaoFilterOptions, }; };