feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter - Implementados ícones para modos claro e escuro do ChatGPT - Criado script de inicialização para PostgreSQL com extensão pgcrypto - Adicionado script de configuração de ambiente que faz backup do .env - Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
83
lib/lancamentos/categoria-helpers.ts
Normal file
83
lib/lancamentos/categoria-helpers.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Categoria grouping and sorting helpers for lancamentos
|
||||
*/
|
||||
|
||||
import type { SelectOption } from "@/components/lancamentos/types";
|
||||
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
*/
|
||||
function capitalize(value: string): string {
|
||||
return value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group label for categorias
|
||||
*/
|
||||
type CategoriaGroup = {
|
||||
label: string;
|
||||
options: SelectOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes category group labels (Despesa -> Despesas, Receita -> Receitas)
|
||||
*/
|
||||
function normalizeCategoryGroupLabel(value: string): string {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === "despesa") {
|
||||
return "Despesas";
|
||||
}
|
||||
if (lower === "receita") {
|
||||
return "Receitas";
|
||||
}
|
||||
return capitalize(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups and sorts categoria options by their group property
|
||||
* @param categoriaOptions - Array of categoria select options
|
||||
* @returns Array of grouped and sorted categoria options
|
||||
*/
|
||||
export function groupAndSortCategorias(
|
||||
categoriaOptions: SelectOption[]
|
||||
): CategoriaGroup[] {
|
||||
// Group categorias by their group property
|
||||
const groups = categoriaOptions.reduce<Record<string, SelectOption[]>>(
|
||||
(acc, option) => {
|
||||
const key = option.group ?? "Outros";
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(option);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// Define preferred order (Despesa first, then Receita, then others)
|
||||
const preferredOrder = ["Despesa", "Receita"];
|
||||
const orderedKeys = [
|
||||
...preferredOrder.filter((key) => groups[key]?.length),
|
||||
...Object.keys(groups).filter((key) => !preferredOrder.includes(key)),
|
||||
];
|
||||
|
||||
// Map to final structure with normalized labels and sorted options
|
||||
return orderedKeys.map((key) => ({
|
||||
label: normalizeCategoryGroupLabel(key),
|
||||
options: groups[key]
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
a.label.localeCompare(b.label, "pt-BR", { sensitivity: "base" })
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters secondary pagador options to exclude the primary pagador
|
||||
*/
|
||||
export function filterSecondaryPagadorOptions(
|
||||
allOptions: SelectOption[],
|
||||
primaryPagadorId?: string
|
||||
): SelectOption[] {
|
||||
return allOptions.filter((option) => option.value !== primaryPagadorId);
|
||||
}
|
||||
15
lib/lancamentos/constants.ts
Normal file
15
lib/lancamentos/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const LANCAMENTO_TRANSACTION_TYPES = ["Despesa", "Receita", "Transferência"] as const;
|
||||
|
||||
export const LANCAMENTO_CONDITIONS = [
|
||||
"À vista",
|
||||
"Parcelado",
|
||||
"Recorrente",
|
||||
] as const;
|
||||
|
||||
export const LANCAMENTO_PAYMENT_METHODS = [
|
||||
"Cartão de crédito",
|
||||
"Cartão de débito",
|
||||
"Pix",
|
||||
"Dinheiro",
|
||||
"Boleto",
|
||||
] as const;
|
||||
192
lib/lancamentos/form-helpers.ts
Normal file
192
lib/lancamentos/form-helpers.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Form state management helpers for lancamentos
|
||||
*/
|
||||
|
||||
import type { LancamentoItem } from "@/components/lancamentos/types";
|
||||
import { LANCAMENTO_CONDITIONS, LANCAMENTO_PAYMENT_METHODS, LANCAMENTO_TRANSACTION_TYPES } from "./constants";
|
||||
import { derivePeriodFromDate } from "@/lib/utils/period";
|
||||
import { getTodayDateString } from "@/lib/utils/date";
|
||||
|
||||
/**
|
||||
* Form state type for lancamento dialog
|
||||
*/
|
||||
export type LancamentoFormState = {
|
||||
purchaseDate: string;
|
||||
period: string;
|
||||
name: string;
|
||||
transactionType: string;
|
||||
amount: string;
|
||||
condition: string;
|
||||
paymentMethod: string;
|
||||
pagadorId: string | undefined;
|
||||
secondaryPagadorId: string | undefined;
|
||||
isSplit: boolean;
|
||||
contaId: string | undefined;
|
||||
cartaoId: string | undefined;
|
||||
categoriaId: string | undefined;
|
||||
installmentCount: string;
|
||||
recurrenceCount: string;
|
||||
dueDate: string;
|
||||
boletoPaymentDate: string;
|
||||
note: string;
|
||||
isSettled: boolean | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial state overrides for lancamento form
|
||||
*/
|
||||
export type LancamentoFormOverrides = {
|
||||
defaultCartaoId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds initial form state from lancamento data and defaults
|
||||
*/
|
||||
export function buildLancamentoInitialState(
|
||||
lancamento?: LancamentoItem,
|
||||
defaultPagadorId?: string | null,
|
||||
preferredPeriod?: string,
|
||||
overrides?: LancamentoFormOverrides
|
||||
): LancamentoFormState {
|
||||
const purchaseDate = lancamento?.purchaseDate
|
||||
? lancamento.purchaseDate.slice(0, 10)
|
||||
: overrides?.defaultPurchaseDate ?? "";
|
||||
|
||||
const paymentMethod =
|
||||
lancamento?.paymentMethod ??
|
||||
overrides?.defaultPaymentMethod ??
|
||||
LANCAMENTO_PAYMENT_METHODS[0];
|
||||
|
||||
const derivedPeriod = derivePeriodFromDate(purchaseDate);
|
||||
const fallbackPeriod =
|
||||
preferredPeriod && /^\d{4}-\d{2}$/.test(preferredPeriod)
|
||||
? preferredPeriod
|
||||
: derivedPeriod;
|
||||
const fallbackPagadorId = lancamento?.pagadorId ?? defaultPagadorId ?? null;
|
||||
const boletoPaymentDate =
|
||||
lancamento?.boletoPaymentDate ??
|
||||
(paymentMethod === "Boleto" && (lancamento?.isSettled ?? false)
|
||||
? getTodayDateString()
|
||||
: "");
|
||||
|
||||
return {
|
||||
purchaseDate,
|
||||
period:
|
||||
lancamento?.period && /^\d{4}-\d{2}$/.test(lancamento.period)
|
||||
? lancamento.period
|
||||
: fallbackPeriod,
|
||||
name: lancamento?.name ?? "",
|
||||
transactionType:
|
||||
lancamento?.transactionType ?? LANCAMENTO_TRANSACTION_TYPES[0],
|
||||
amount:
|
||||
typeof lancamento?.amount === "number"
|
||||
? (Math.round(Math.abs(lancamento.amount) * 100) / 100).toFixed(2)
|
||||
: "",
|
||||
condition: lancamento?.condition ?? LANCAMENTO_CONDITIONS[0],
|
||||
paymentMethod,
|
||||
pagadorId: fallbackPagadorId ?? undefined,
|
||||
secondaryPagadorId: undefined,
|
||||
isSplit: false,
|
||||
contaId:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? undefined
|
||||
: lancamento?.contaId ?? undefined,
|
||||
cartaoId:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined
|
||||
: undefined,
|
||||
categoriaId: lancamento?.categoriaId ?? undefined,
|
||||
installmentCount: lancamento?.installmentCount
|
||||
? String(lancamento.installmentCount)
|
||||
: "",
|
||||
recurrenceCount: lancamento?.recurrenceCount
|
||||
? String(lancamento.recurrenceCount)
|
||||
: "",
|
||||
dueDate: lancamento?.dueDate ?? "",
|
||||
boletoPaymentDate,
|
||||
note: lancamento?.note ?? "",
|
||||
isSettled:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
: lancamento?.isSettled ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies field dependencies when form state changes
|
||||
* This function encapsulates the business logic for field interdependencies
|
||||
*/
|
||||
export function applyFieldDependencies(
|
||||
key: keyof LancamentoFormState,
|
||||
value: LancamentoFormState[keyof LancamentoFormState],
|
||||
currentState: LancamentoFormState,
|
||||
_periodDirty: boolean
|
||||
): Partial<LancamentoFormState> {
|
||||
const updates: Partial<LancamentoFormState> = {};
|
||||
|
||||
// Removed automatic period update when purchase date changes
|
||||
// if (key === "purchaseDate" && typeof value === "string") {
|
||||
// if (!periodDirty) {
|
||||
// updates.period = derivePeriodFromDate(value);
|
||||
// }
|
||||
// }
|
||||
|
||||
// When condition changes, clear irrelevant fields
|
||||
if (key === "condition" && typeof value === "string") {
|
||||
if (value !== "Parcelado") {
|
||||
updates.installmentCount = "";
|
||||
}
|
||||
if (value !== "Recorrente") {
|
||||
updates.recurrenceCount = "";
|
||||
}
|
||||
}
|
||||
|
||||
// When payment method changes, adjust related fields
|
||||
if (key === "paymentMethod" && typeof value === "string") {
|
||||
if (value === "Cartão de crédito") {
|
||||
updates.contaId = undefined;
|
||||
updates.isSettled = null;
|
||||
} else {
|
||||
updates.cartaoId = undefined;
|
||||
updates.isSettled = currentState.isSettled ?? false;
|
||||
}
|
||||
|
||||
// Clear boleto-specific fields if not boleto
|
||||
if (value !== "Boleto") {
|
||||
updates.dueDate = "";
|
||||
updates.boletoPaymentDate = "";
|
||||
} else if (currentState.isSettled || (updates.isSettled !== null && updates.isSettled !== undefined)) {
|
||||
// Set today's date for boleto payment if settled
|
||||
const settled = updates.isSettled ?? currentState.isSettled;
|
||||
if (settled) {
|
||||
updates.boletoPaymentDate = currentState.boletoPaymentDate || getTodayDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When split is disabled, clear secondary pagador
|
||||
if (key === "isSplit" && value === false) {
|
||||
updates.secondaryPagadorId = undefined;
|
||||
}
|
||||
|
||||
// When primary pagador changes, clear secondary if it matches
|
||||
if (key === "pagadorId" && typeof value === "string") {
|
||||
const secondaryValue = currentState.secondaryPagadorId;
|
||||
if (secondaryValue && secondaryValue === value) {
|
||||
updates.secondaryPagadorId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// When isSettled changes and payment method is Boleto
|
||||
if (key === "isSettled" && currentState.paymentMethod === "Boleto") {
|
||||
if (value === true) {
|
||||
updates.boletoPaymentDate = currentState.boletoPaymentDate || getTodayDateString();
|
||||
} else if (value === false) {
|
||||
updates.boletoPaymentDate = "";
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
94
lib/lancamentos/formatting-helpers.ts
Normal file
94
lib/lancamentos/formatting-helpers.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Formatting helpers for displaying lancamento data
|
||||
*/
|
||||
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
*/
|
||||
function capitalize(value: string): string {
|
||||
return value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currency formatter for pt-BR locale (BRL)
|
||||
*/
|
||||
export const currencyFormatter = new Intl.NumberFormat("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
});
|
||||
|
||||
/**
|
||||
* Date formatter for pt-BR locale (dd/mm/yyyy)
|
||||
*/
|
||||
export const dateFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
/**
|
||||
* Month formatter for pt-BR locale (Month Year)
|
||||
*/
|
||||
export const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats a date string to localized format
|
||||
* @param value - ISO date string or null
|
||||
* @returns Formatted date string or "—"
|
||||
* @example formatDate("2024-01-15") => "15/01/2024"
|
||||
*/
|
||||
export function formatDate(value?: string | null): string {
|
||||
if (!value) return "—";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "—";
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a period (YYYY-MM) to localized month label
|
||||
* @param value - Period string (YYYY-MM) or null
|
||||
* @returns Formatted period string or "—"
|
||||
* @example formatPeriod("2024-01") => "Janeiro 2024"
|
||||
*/
|
||||
export function formatPeriod(value?: string | null): string {
|
||||
if (!value) return "—";
|
||||
const [year, month] = value.split("-").map(Number);
|
||||
if (!year || !month) return value;
|
||||
const date = new Date(year, month - 1, 1);
|
||||
return capitalize(monthFormatter.format(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a condition string with proper capitalization
|
||||
* @param value - Condition string or null
|
||||
* @returns Formatted condition string or "—"
|
||||
* @example formatCondition("vista") => "À vista"
|
||||
*/
|
||||
export function formatCondition(value?: string | null): string {
|
||||
if (!value) return "—";
|
||||
if (value.toLowerCase() === "vista") return "À vista";
|
||||
return capitalize(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the badge variant for a transaction type
|
||||
* @param type - Transaction type (Receita/Despesa)
|
||||
* @returns Badge variant
|
||||
*/
|
||||
export function getTransactionBadgeVariant(type?: string | null): "default" | "destructive" | "secondary" {
|
||||
if (!type) return "secondary";
|
||||
return type.toLowerCase() === "receita" ? "default" : "destructive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats currency value
|
||||
* @param value - Numeric value
|
||||
* @returns Formatted currency string
|
||||
* @example formatCurrency(1234.56) => "R$ 1.234,56"
|
||||
*/
|
||||
export function formatCurrency(value: number): string {
|
||||
return currencyFormatter.format(value);
|
||||
}
|
||||
521
lib/lancamentos/page-helpers.ts
Normal file
521
lib/lancamentos/page-helpers.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
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 { eq, ilike, 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<string, string | string[] | undefined>
|
||||
| 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;
|
||||
};
|
||||
|
||||
type CartaoSluggedOption = BaseSluggedOption & {
|
||||
kind: "cartao";
|
||||
logo: string | null;
|
||||
};
|
||||
|
||||
export type SluggedFilters = {
|
||||
pagadorFiltersRaw: PagadorSluggedOption[];
|
||||
categoriaFiltersRaw: CategoriaSluggedOption[];
|
||||
contaFiltersRaw: ContaSluggedOption[];
|
||||
cartaoFiltersRaw: CartaoSluggedOption[];
|
||||
};
|
||||
|
||||
export type SlugMaps = {
|
||||
pagador: Map<string, string>;
|
||||
categoria: Map<string, string>;
|
||||
conta: Map<string, string>;
|
||||
cartao: Map<string, string>;
|
||||
};
|
||||
|
||||
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<string, number>();
|
||||
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
|
||||
): SelectOption => ({
|
||||
value,
|
||||
label: normalizeLabel(label),
|
||||
role: role ?? null,
|
||||
group: group ?? null,
|
||||
slug: slug ?? null,
|
||||
avatarUrl: avatarUrl ?? null,
|
||||
logo: logo ?? null,
|
||||
icon: icon ?? 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,
|
||||
};
|
||||
});
|
||||
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
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 = <T extends { label: string }>(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 }) =>
|
||||
toOption(id, label, undefined, undefined, slug, undefined, logo)
|
||||
)
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user