feat(pagadores): adicionar widget no dashboard e atualizar avatares

- Novo widget de pagadores no dashboard com resumo de transações
- Substituir avatares SVG por PNG com melhor qualidade
- Melhorar seção de pagador no diálogo de lançamentos
- Adicionar ação para buscar pagadores por nome
- Atualizar componentes de seleção (cartões, categorias, contas)
- Melhorias no layout de ajustes e relatórios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-04 01:44:50 +00:00
parent 76702d770f
commit a70a83dd9d
61 changed files with 509 additions and 148 deletions

View File

@@ -143,6 +143,8 @@ const baseFields = z.object({
pagadorId: uuidSchema("Pagador").nullable().optional(),
secondaryPagadorId: uuidSchema("Pagador secundário").optional(),
isSplit: z.boolean().optional().default(false),
primarySplitAmount: z.coerce.number().min(0).optional(),
secondarySplitAmount: z.coerce.number().min(0).optional(),
contaId: uuidSchema("Conta").nullable().optional(),
cartaoId: uuidSchema("Cartão").nullable().optional(),
categoriaId: uuidSchema("Categoria").nullable().optional(),
@@ -234,6 +236,23 @@ const refineLancamento = (
message: "Escolha um pagador diferente para dividir o lançamento.",
});
}
// Validate custom split amounts sum to total
if (
data.primarySplitAmount !== undefined &&
data.secondarySplitAmount !== undefined
) {
const sum = data.primarySplitAmount + data.secondarySplitAmount;
const total = Math.abs(data.amount);
// Allow 1 cent tolerance for rounding differences
if (Math.abs(sum - total) > 0.01) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["primarySplitAmount"],
message: "A soma das divisões deve ser igual ao valor total.",
});
}
}
}
};
@@ -354,17 +373,33 @@ const buildShares = ({
pagadorId,
isSplit,
secondaryPagadorId,
primarySplitAmountCents,
secondarySplitAmountCents,
}: {
totalCents: number;
pagadorId: string | null;
isSplit: boolean;
secondaryPagadorId?: string;
primarySplitAmountCents?: number;
secondarySplitAmountCents?: number;
}): Share[] => {
if (isSplit) {
if (!pagadorId || !secondaryPagadorId) {
throw new Error("Configuração de divisão inválida para o lançamento.");
}
// Use custom split amounts if provided
if (
primarySplitAmountCents !== undefined &&
secondarySplitAmountCents !== undefined
) {
return [
{ pagadorId, amountCents: primarySplitAmountCents },
{ pagadorId: secondaryPagadorId, amountCents: secondarySplitAmountCents },
];
}
// Fallback to equal split
const [primaryAmount, secondaryAmount] = splitAmount(totalCents, 2);
return [
{ pagadorId, amountCents: primaryAmount },
@@ -598,6 +633,12 @@ export async function createLancamentoAction(
pagadorId: data.pagadorId ?? null,
isSplit: data.isSplit ?? false,
secondaryPagadorId: data.secondaryPagadorId,
primarySplitAmountCents: data.primarySplitAmount
? Math.round(data.primarySplitAmount * 100)
: undefined,
secondarySplitAmountCents: data.secondarySplitAmount
? Math.round(data.secondarySplitAmount * 100)
: undefined,
});
const isSeriesLancamento =