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:
@@ -1,4 +1,4 @@
|
||||
import { RiSettingsLine } from "@remixicon/react";
|
||||
import { RiSettings2Line } from "@remixicon/react";
|
||||
import PageDescription from "@/components/page-description";
|
||||
|
||||
export const metadata = {
|
||||
@@ -13,7 +13,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiSettingsLine />}
|
||||
icon={<RiSettings2Line />}
|
||||
title="Ajustes"
|
||||
subtitle="Gerencie informações da conta, segurança e outras opções para otimizar sua experiência."
|
||||
/>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { PagadoresPage } from "@/components/pagadores/pagadores-page";
|
||||
import { user } from "@/db/schema";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { fetchPagadoresWithAccess } from "@/lib/pagadores/access";
|
||||
import type { PagadorStatus } from "@/lib/pagadores/constants";
|
||||
import {
|
||||
@@ -44,11 +47,21 @@ const resolveStatus = (status: string | null): PagadorStatus => {
|
||||
export default async function Page() {
|
||||
const userId = await getUserId();
|
||||
|
||||
const [pagadorRows, avatarOptions] = await Promise.all([
|
||||
const [pagadorRows, localAvatarOptions, userData] = await Promise.all([
|
||||
fetchPagadoresWithAccess(userId),
|
||||
loadAvatarOptions(),
|
||||
db.query.user.findFirst({
|
||||
columns: { image: true },
|
||||
where: eq(user.id, userId),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Incluir a imagem do Google nas opções se disponível
|
||||
const userImage = userData?.image;
|
||||
const avatarOptions = userImage
|
||||
? [userImage, ...localAvatarOptions]
|
||||
: localAvatarOptions;
|
||||
|
||||
const pagadoresData = pagadorRows
|
||||
.map((pagador) => ({
|
||||
id: pagador.id,
|
||||
|
||||
Reference in New Issue
Block a user