mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,234 +1,234 @@
|
||||
"use server";
|
||||
|
||||
import { lancamentos, pagadores } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import {
|
||||
fetchPagadorBoletoStats,
|
||||
fetchPagadorCardUsage,
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { displayPeriod } from "@/lib/utils/period";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { Resend } from "resend";
|
||||
import { z } from "zod";
|
||||
import { lancamentos, pagadores } from "@/db/schema";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
fetchPagadorBoletoStats,
|
||||
fetchPagadorCardUsage,
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { displayPeriod } from "@/lib/utils/period";
|
||||
|
||||
const inputSchema = z.object({
|
||||
pagadorId: z.string().uuid("Pagador inválido."),
|
||||
period: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}$/, "Período inválido. Informe no formato AAAA-MM."),
|
||||
pagadorId: z.string().uuid("Pagador inválido."),
|
||||
period: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}$/, "Período inválido. Informe no formato AAAA-MM."),
|
||||
});
|
||||
|
||||
type ActionResult =
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string };
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
value.toLocaleString("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
value.toLocaleString("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const formatDate = (value: Date | null | undefined) => {
|
||||
if (!value) return "—";
|
||||
return value.toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
if (!value) return "—";
|
||||
return value.toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
// Escapa HTML para prevenir XSS
|
||||
const escapeHtml = (text: string | null | undefined): string => {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
type LancamentoRow = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
paymentMethod: string | null;
|
||||
condition: string | null;
|
||||
amount: number;
|
||||
transactionType: string | null;
|
||||
purchaseDate: Date | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
paymentMethod: string | null;
|
||||
condition: string | null;
|
||||
amount: number;
|
||||
transactionType: string | null;
|
||||
purchaseDate: Date | null;
|
||||
};
|
||||
|
||||
type BoletoItem = {
|
||||
name: string;
|
||||
amount: number;
|
||||
dueDate: Date | null;
|
||||
name: string;
|
||||
amount: number;
|
||||
dueDate: Date | null;
|
||||
};
|
||||
|
||||
type ParceladoItem = {
|
||||
name: string;
|
||||
totalAmount: number;
|
||||
installmentCount: number;
|
||||
currentInstallment: number;
|
||||
installmentAmount: number;
|
||||
purchaseDate: Date | null;
|
||||
name: string;
|
||||
totalAmount: number;
|
||||
installmentCount: number;
|
||||
currentInstallment: number;
|
||||
installmentAmount: number;
|
||||
purchaseDate: Date | null;
|
||||
};
|
||||
|
||||
type SummaryPayload = {
|
||||
pagadorName: string;
|
||||
periodLabel: string;
|
||||
monthlyBreakdown: Awaited<ReturnType<typeof fetchPagadorMonthlyBreakdown>>;
|
||||
historyData: Awaited<ReturnType<typeof fetchPagadorHistory>>;
|
||||
cardUsage: Awaited<ReturnType<typeof fetchPagadorCardUsage>>;
|
||||
boletoStats: Awaited<ReturnType<typeof fetchPagadorBoletoStats>>;
|
||||
boletos: BoletoItem[];
|
||||
lancamentos: LancamentoRow[];
|
||||
parcelados: ParceladoItem[];
|
||||
pagadorName: string;
|
||||
periodLabel: string;
|
||||
monthlyBreakdown: Awaited<ReturnType<typeof fetchPagadorMonthlyBreakdown>>;
|
||||
historyData: Awaited<ReturnType<typeof fetchPagadorHistory>>;
|
||||
cardUsage: Awaited<ReturnType<typeof fetchPagadorCardUsage>>;
|
||||
boletoStats: Awaited<ReturnType<typeof fetchPagadorBoletoStats>>;
|
||||
boletos: BoletoItem[];
|
||||
lancamentos: LancamentoRow[];
|
||||
parcelados: ParceladoItem[];
|
||||
};
|
||||
|
||||
const buildSectionHeading = (label: string) =>
|
||||
`<h3 style="font-size:16px;margin:24px 0 8px 0;color:#0f172a;">${label}</h3>`;
|
||||
`<h3 style="font-size:16px;margin:24px 0 8px 0;color:#0f172a;">${label}</h3>`;
|
||||
|
||||
const buildSummaryHtml = ({
|
||||
pagadorName,
|
||||
periodLabel,
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletos,
|
||||
lancamentos,
|
||||
parcelados,
|
||||
pagadorName,
|
||||
periodLabel,
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletos,
|
||||
lancamentos,
|
||||
parcelados,
|
||||
}: SummaryPayload) => {
|
||||
// Calcular máximo de despesas para barras de progresso
|
||||
const maxDespesas = Math.max(...historyData.map((p) => p.despesas), 1);
|
||||
// Calcular máximo de despesas para barras de progresso
|
||||
const maxDespesas = Math.max(...historyData.map((p) => p.despesas), 1);
|
||||
|
||||
const historyRows =
|
||||
historyData.length > 0
|
||||
? historyData
|
||||
.map((point) => {
|
||||
const percentage = (point.despesas / maxDespesas) * 100;
|
||||
const barColor =
|
||||
point.despesas > maxDespesas * 0.8
|
||||
? "#ef4444"
|
||||
: point.despesas > maxDespesas * 0.5
|
||||
? "#f59e0b"
|
||||
: "#10b981";
|
||||
const historyRows =
|
||||
historyData.length > 0
|
||||
? historyData
|
||||
.map((point) => {
|
||||
const percentage = (point.despesas / maxDespesas) * 100;
|
||||
const barColor =
|
||||
point.despesas > maxDespesas * 0.8
|
||||
? "#ef4444"
|
||||
: point.despesas > maxDespesas * 0.5
|
||||
? "#f59e0b"
|
||||
: "#10b981";
|
||||
|
||||
return `
|
||||
return `
|
||||
<tr>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;font-weight:500;">${escapeHtml(
|
||||
point.label
|
||||
)}</td>
|
||||
point.label,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div style="flex:1;background:#f1f5f9;border-radius:6px;height:24px;overflow:hidden;">
|
||||
<div style="background:${barColor};height:100%;width:${percentage}%;transition:width 0.3s;"></div>
|
||||
</div>
|
||||
<span style="font-weight:600;min-width:100px;text-align:right;">${formatCurrency(
|
||||
point.despesas
|
||||
)}</span>
|
||||
point.despesas,
|
||||
)}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("")
|
||||
: `<tr><td colspan="2" style="padding:16px;text-align:center;color:#94a3b8;">Sem histórico suficiente.</td></tr>`;
|
||||
})
|
||||
.join("")
|
||||
: `<tr><td colspan="2" style="padding:16px;text-align:center;color:#94a3b8;">Sem histórico suficiente.</td></tr>`;
|
||||
|
||||
const cardUsageRows =
|
||||
cardUsage.length > 0
|
||||
? cardUsage
|
||||
.map(
|
||||
(item) => `
|
||||
const cardUsageRows =
|
||||
cardUsage.length > 0
|
||||
? cardUsage
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;font-weight:500;">${escapeHtml(
|
||||
item.name
|
||||
)}</td>
|
||||
item.name,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:right;font-weight:600;">${formatCurrency(
|
||||
item.amount
|
||||
)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="2" style="padding:16px;text-align:center;color:#94a3b8;">Sem gastos com cartão neste período.</td></tr>`;
|
||||
item.amount,
|
||||
)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="2" style="padding:16px;text-align:center;color:#94a3b8;">Sem gastos com cartão neste período.</td></tr>`;
|
||||
|
||||
const boletoRows =
|
||||
boletos.length > 0
|
||||
? boletos
|
||||
.map(
|
||||
(item) => `
|
||||
const boletoRows =
|
||||
boletos.length > 0
|
||||
? boletos
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;font-weight:500;">${escapeHtml(
|
||||
item.name
|
||||
)}</td>
|
||||
item.name,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">${
|
||||
item.dueDate ? formatDate(item.dueDate) : "—"
|
||||
}</td>
|
||||
item.dueDate ? formatDate(item.dueDate) : "—"
|
||||
}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:right;font-weight:600;">${formatCurrency(
|
||||
item.amount
|
||||
)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="3" style="padding:16px;text-align:center;color:#94a3b8;">Sem boletos neste período.</td></tr>`;
|
||||
item.amount,
|
||||
)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="3" style="padding:16px;text-align:center;color:#94a3b8;">Sem boletos neste período.</td></tr>`;
|
||||
|
||||
const lancamentoRows =
|
||||
lancamentos.length > 0
|
||||
? lancamentos
|
||||
.map(
|
||||
(item) => `
|
||||
const lancamentoRows =
|
||||
lancamentos.length > 0
|
||||
? lancamentos
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;white-space:nowrap;">${formatDate(
|
||||
item.purchaseDate
|
||||
)}</td>
|
||||
item.purchaseDate,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">${
|
||||
escapeHtml(item.name) || "Sem descrição"
|
||||
}</td>
|
||||
escapeHtml(item.name) || "Sem descrição"
|
||||
}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">${
|
||||
escapeHtml(item.condition) || "—"
|
||||
}</td>
|
||||
escapeHtml(item.condition) || "—"
|
||||
}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">${
|
||||
escapeHtml(item.paymentMethod) || "—"
|
||||
}</td>
|
||||
escapeHtml(item.paymentMethod) || "—"
|
||||
}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:right;font-weight:600;">${formatCurrency(
|
||||
item.amount
|
||||
)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="5" style="padding:16px;text-align:center;color:#94a3b8;">Nenhum lançamento registrado no período.</td></tr>`;
|
||||
item.amount,
|
||||
)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="5" style="padding:16px;text-align:center;color:#94a3b8;">Nenhum lançamento registrado no período.</td></tr>`;
|
||||
|
||||
const parceladoRows =
|
||||
parcelados.length > 0
|
||||
? parcelados
|
||||
.map(
|
||||
(item) => `
|
||||
const parceladoRows =
|
||||
parcelados.length > 0
|
||||
? parcelados
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;white-space:nowrap;">${formatDate(
|
||||
item.purchaseDate
|
||||
)}</td>
|
||||
item.purchaseDate,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;">${
|
||||
escapeHtml(item.name) || "Sem descrição"
|
||||
}</td>
|
||||
escapeHtml(item.name) || "Sem descrição"
|
||||
}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:center;">${
|
||||
item.currentInstallment
|
||||
}/${item.installmentCount}</td>
|
||||
item.currentInstallment
|
||||
}/${item.installmentCount}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:right;font-weight:600;">${formatCurrency(
|
||||
item.installmentAmount
|
||||
)}</td>
|
||||
item.installmentAmount,
|
||||
)}</td>
|
||||
<td style="padding:10px 12px;border-bottom:1px solid #e2e8f0;text-align:right;color:#64748b;">${formatCurrency(
|
||||
item.totalAmount
|
||||
)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="5" style="padding:16px;text-align:center;color:#94a3b8;">Nenhum lançamento parcelado neste período.</td></tr>`;
|
||||
item.totalAmount,
|
||||
)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="5" style="padding:16px;text-align:center;color:#94a3b8;">Nenhum lançamento parcelado neste período.</td></tr>`;
|
||||
|
||||
return `
|
||||
return `
|
||||
<div style="margin:0 auto;max-width:800px;background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',Arial,sans-serif;color:#0f172a;line-height:1.6;">
|
||||
<!-- Preheader invisível (melhora a prévia no cliente de e-mail) -->
|
||||
<span style="display:none;visibility:hidden;opacity:0;color:transparent;height:0;width:0;overflow:hidden;">Resumo mensal e detalhes de gastos por cartão, boletos e lançamentos.</span>
|
||||
@@ -237,8 +237,8 @@ const buildSummaryHtml = ({
|
||||
<div style="background:linear-gradient(90deg,#dc5a3a,#ea744e);padding:28px 24px;border-radius:12px 12px 0 0;">
|
||||
<h1 style="margin:0 0 6px 0;font-size:26px;font-weight:800;letter-spacing:-0.3px;color:#ffffff;">Resumo Financeiro</h1>
|
||||
<p style="margin:0;font-size:15px;color:#ffece6;">${escapeHtml(
|
||||
periodLabel
|
||||
)}</p>
|
||||
periodLabel,
|
||||
)}</p>
|
||||
</div>
|
||||
|
||||
<!-- Cartão principal -->
|
||||
@@ -246,8 +246,8 @@ const buildSummaryHtml = ({
|
||||
<!-- Saudação -->
|
||||
<p style="margin:0 0 24px 0;font-size:15px;color:#334155;">
|
||||
Olá <strong>${escapeHtml(
|
||||
pagadorName
|
||||
)}</strong>, segue o consolidado do mês:
|
||||
pagadorName,
|
||||
)}</strong>, segue o consolidado do mês:
|
||||
</p>
|
||||
|
||||
<!-- Totais do mês -->
|
||||
@@ -258,27 +258,27 @@ const buildSummaryHtml = ({
|
||||
<td style="padding:16px 18px;background:#fff7f5;border-bottom:1px solid #f1f5f9;font-size:15px;color:#475569;">Total gasto</td>
|
||||
<td style="padding:16px 18px;background:#fff7f5;border-bottom:1px solid #f1f5f9;text-align:right;">
|
||||
<strong style="font-size:22px;color:#0f172a;">${formatCurrency(
|
||||
monthlyBreakdown.totalExpenses
|
||||
)}</strong>
|
||||
monthlyBreakdown.totalExpenses,
|
||||
)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 18px;font-size:14px;color:#64748b;">💳 Cartões</td>
|
||||
<td style="padding:12px 18px;text-align:right;"><strong style="font-size:15px;color:#0f172a;">${formatCurrency(
|
||||
monthlyBreakdown.paymentSplits.card
|
||||
)}</strong></td>
|
||||
monthlyBreakdown.paymentSplits.card,
|
||||
)}</strong></td>
|
||||
</tr>
|
||||
<tr style="background:#fcfcfd;">
|
||||
<td style="padding:12px 18px;font-size:14px;color:#64748b;">📄 Boletos</td>
|
||||
<td style="padding:12px 18px;text-align:right;"><strong style="font-size:15px;color:#0f172a;">${formatCurrency(
|
||||
monthlyBreakdown.paymentSplits.boleto
|
||||
)}</strong></td>
|
||||
monthlyBreakdown.paymentSplits.boleto,
|
||||
)}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:12px 18px;font-size:14px;color:#64748b;">⚡ Pix/Débito/Dinheiro</td>
|
||||
<td style="padding:12px 18px;text-align:right;"><strong style="font-size:15px;color:#0f172a;">${formatCurrency(
|
||||
monthlyBreakdown.paymentSplits.instant
|
||||
)}</strong></td>
|
||||
monthlyBreakdown.paymentSplits.instant,
|
||||
)}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -305,8 +305,8 @@ const buildSummaryHtml = ({
|
||||
<td style="color:#475569;font-weight:700;font-size:15px;">Total</td>
|
||||
<td style="text-align:right;">
|
||||
<strong style="font-size:18px;color:#0f172a;">${formatCurrency(
|
||||
monthlyBreakdown.paymentSplits.card
|
||||
)}</strong>
|
||||
monthlyBreakdown.paymentSplits.card,
|
||||
)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -333,8 +333,8 @@ const buildSummaryHtml = ({
|
||||
<td style="color:#475569;font-weight:700;font-size:15px;">Total</td>
|
||||
<td style="text-align:right;">
|
||||
<strong style="font-size:18px;color:#0f172a;">${formatCurrency(
|
||||
boletoStats.totalAmount
|
||||
)}</strong>
|
||||
boletoStats.totalAmount,
|
||||
)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -396,207 +396,207 @@ const buildSummaryHtml = ({
|
||||
};
|
||||
|
||||
export async function sendPagadorSummaryAction(
|
||||
input: z.infer<typeof inputSchema>
|
||||
input: z.infer<typeof inputSchema>,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { pagadorId, period } = inputSchema.parse(input);
|
||||
const user = await getUser();
|
||||
try {
|
||||
const { pagadorId, period } = inputSchema.parse(input);
|
||||
const user = await getUser();
|
||||
|
||||
const pagadorRow = await db.query.pagadores.findFirst({
|
||||
where: and(eq(pagadores.id, pagadorId), eq(pagadores.userId, user.id)),
|
||||
});
|
||||
const pagadorRow = await db.query.pagadores.findFirst({
|
||||
where: and(eq(pagadores.id, pagadorId), eq(pagadores.userId, user.id)),
|
||||
});
|
||||
|
||||
if (!pagadorRow) {
|
||||
return { success: false, error: "Pagador não encontrado." };
|
||||
}
|
||||
if (!pagadorRow) {
|
||||
return { success: false, error: "Pagador não encontrado." };
|
||||
}
|
||||
|
||||
if (!pagadorRow.email) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Cadastre um e-mail para conseguir enviar o resumo.",
|
||||
};
|
||||
}
|
||||
if (!pagadorRow.email) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Cadastre um e-mail para conseguir enviar o resumo.",
|
||||
};
|
||||
}
|
||||
|
||||
const resendApiKey = process.env.RESEND_API_KEY;
|
||||
const resendFrom =
|
||||
process.env.RESEND_FROM_EMAIL ?? "Opensheets <onboarding@resend.dev>";
|
||||
const resendApiKey = process.env.RESEND_API_KEY;
|
||||
const resendFrom =
|
||||
process.env.RESEND_FROM_EMAIL ?? "Opensheets <onboarding@resend.dev>";
|
||||
|
||||
if (!resendApiKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Serviço de e-mail não configurado (RESEND_API_KEY ausente).",
|
||||
};
|
||||
}
|
||||
if (!resendApiKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Serviço de e-mail não configurado (RESEND_API_KEY ausente).",
|
||||
};
|
||||
}
|
||||
|
||||
const resend = new Resend(resendApiKey);
|
||||
const resend = new Resend(resendApiKey);
|
||||
|
||||
const [
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletoRows,
|
||||
lancamentoRows,
|
||||
parceladoRows,
|
||||
] = await Promise.all([
|
||||
fetchPagadorMonthlyBreakdown({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorHistory({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorCardUsage({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorBoletoStats({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
db
|
||||
.select({
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.paymentMethod, "Boleto")
|
||||
)
|
||||
)
|
||||
.orderBy(desc(lancamentos.dueDate)),
|
||||
db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
condition: lancamentos.condition,
|
||||
amount: lancamentos.amount,
|
||||
transactionType: lancamentos.transactionType,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate)),
|
||||
db
|
||||
.select({
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
installmentCount: lancamentos.installmentCount,
|
||||
currentInstallment: lancamentos.currentInstallment,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.condition, "Parcelado"),
|
||||
eq(lancamentos.isAnticipated, false)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate)),
|
||||
]);
|
||||
const [
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletoRows,
|
||||
lancamentoRows,
|
||||
parceladoRows,
|
||||
] = await Promise.all([
|
||||
fetchPagadorMonthlyBreakdown({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorHistory({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorCardUsage({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
fetchPagadorBoletoStats({
|
||||
userId: user.id,
|
||||
pagadorId,
|
||||
period,
|
||||
}),
|
||||
db
|
||||
.select({
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.paymentMethod, "Boleto"),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.dueDate)),
|
||||
db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
condition: lancamentos.condition,
|
||||
amount: lancamentos.amount,
|
||||
transactionType: lancamentos.transactionType,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate)),
|
||||
db
|
||||
.select({
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
installmentCount: lancamentos.installmentCount,
|
||||
currentInstallment: lancamentos.currentInstallment,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.pagadorId, pagadorId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.condition, "Parcelado"),
|
||||
eq(lancamentos.isAnticipated, false),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate)),
|
||||
]);
|
||||
|
||||
const normalizedBoletos: BoletoItem[] = boletoRows.map((row) => ({
|
||||
name: row.name ?? "Sem descrição",
|
||||
amount: Math.abs(Number(row.amount ?? 0)),
|
||||
dueDate: row.dueDate,
|
||||
}));
|
||||
const normalizedBoletos: BoletoItem[] = boletoRows.map((row) => ({
|
||||
name: row.name ?? "Sem descrição",
|
||||
amount: Math.abs(Number(row.amount ?? 0)),
|
||||
dueDate: row.dueDate,
|
||||
}));
|
||||
|
||||
const normalizedLancamentos: LancamentoRow[] = lancamentoRows.map(
|
||||
(row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
paymentMethod: row.paymentMethod,
|
||||
condition: row.condition,
|
||||
transactionType: row.transactionType,
|
||||
purchaseDate: row.purchaseDate,
|
||||
amount: Number(row.amount ?? 0),
|
||||
})
|
||||
);
|
||||
const normalizedLancamentos: LancamentoRow[] = lancamentoRows.map(
|
||||
(row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
paymentMethod: row.paymentMethod,
|
||||
condition: row.condition,
|
||||
transactionType: row.transactionType,
|
||||
purchaseDate: row.purchaseDate,
|
||||
amount: Number(row.amount ?? 0),
|
||||
}),
|
||||
);
|
||||
|
||||
const normalizedParcelados: ParceladoItem[] = parceladoRows.map((row) => {
|
||||
const installmentAmount = Math.abs(Number(row.amount ?? 0));
|
||||
const installmentCount = row.installmentCount ?? 1;
|
||||
const totalAmount = installmentAmount * installmentCount;
|
||||
const normalizedParcelados: ParceladoItem[] = parceladoRows.map((row) => {
|
||||
const installmentAmount = Math.abs(Number(row.amount ?? 0));
|
||||
const installmentCount = row.installmentCount ?? 1;
|
||||
const totalAmount = installmentAmount * installmentCount;
|
||||
|
||||
return {
|
||||
name: row.name ?? "Sem descrição",
|
||||
installmentAmount,
|
||||
installmentCount,
|
||||
currentInstallment: row.currentInstallment ?? 1,
|
||||
totalAmount,
|
||||
purchaseDate: row.purchaseDate,
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: row.name ?? "Sem descrição",
|
||||
installmentAmount,
|
||||
installmentCount,
|
||||
currentInstallment: row.currentInstallment ?? 1,
|
||||
totalAmount,
|
||||
purchaseDate: row.purchaseDate,
|
||||
};
|
||||
});
|
||||
|
||||
const html = buildSummaryHtml({
|
||||
pagadorName: pagadorRow.name,
|
||||
periodLabel: displayPeriod(period),
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletos: normalizedBoletos,
|
||||
lancamentos: normalizedLancamentos,
|
||||
parcelados: normalizedParcelados,
|
||||
});
|
||||
const html = buildSummaryHtml({
|
||||
pagadorName: pagadorRow.name,
|
||||
periodLabel: displayPeriod(period),
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletos: normalizedBoletos,
|
||||
lancamentos: normalizedLancamentos,
|
||||
parcelados: normalizedParcelados,
|
||||
});
|
||||
|
||||
await resend.emails.send({
|
||||
from: resendFrom,
|
||||
to: pagadorRow.email,
|
||||
subject: `Resumo Financeiro | ${displayPeriod(period)}`,
|
||||
html,
|
||||
});
|
||||
await resend.emails.send({
|
||||
from: resendFrom,
|
||||
to: pagadorRow.email,
|
||||
subject: `Resumo Financeiro | ${displayPeriod(period)}`,
|
||||
html,
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const now = new Date();
|
||||
|
||||
await db
|
||||
.update(pagadores)
|
||||
.set({ lastMailAt: now })
|
||||
.where(
|
||||
and(eq(pagadores.id, pagadorRow.id), eq(pagadores.userId, user.id))
|
||||
);
|
||||
await db
|
||||
.update(pagadores)
|
||||
.set({ lastMailAt: now })
|
||||
.where(
|
||||
and(eq(pagadores.id, pagadorRow.id), eq(pagadores.userId, user.id)),
|
||||
);
|
||||
|
||||
revalidatePath(`/pagadores/${pagadorRow.id}`);
|
||||
revalidatePath(`/pagadores/${pagadorRow.id}`);
|
||||
|
||||
return { success: true, message: "Resumo enviado com sucesso." };
|
||||
} catch (error) {
|
||||
// Log estruturado em desenvolvimento
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("[sendPagadorSummaryAction]", error);
|
||||
}
|
||||
return { success: true, message: "Resumo enviado com sucesso." };
|
||||
} catch (error) {
|
||||
// Log estruturado em desenvolvimento
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("[sendPagadorSummaryAction]", error);
|
||||
}
|
||||
|
||||
// Tratar erros de validação separadamente
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
// Tratar erros de validação separadamente
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
|
||||
// Não expor detalhes do erro para o usuário
|
||||
return {
|
||||
success: false,
|
||||
error: "Não foi possível enviar o resumo. Tente novamente mais tarde.",
|
||||
};
|
||||
}
|
||||
// Não expor detalhes do erro para o usuário
|
||||
return {
|
||||
success: false,
|
||||
error: "Não foi possível enviar o resumo. Tente novamente mais tarde.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,95 @@
|
||||
import { lancamentos, pagadorShares, user as usersTable, contas, cartoes, categorias, pagadores } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import { and, desc, eq, type SQL } from "drizzle-orm";
|
||||
import {
|
||||
cartoes,
|
||||
categorias,
|
||||
contas,
|
||||
lancamentos,
|
||||
pagadores,
|
||||
pagadorShares,
|
||||
user as usersTable,
|
||||
} from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export type ShareData = {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export async function fetchPagadorShares(
|
||||
pagadorId: string
|
||||
pagadorId: string,
|
||||
): Promise<ShareData[]> {
|
||||
const shareRows = await db
|
||||
.select({
|
||||
id: pagadorShares.id,
|
||||
sharedWithUserId: pagadorShares.sharedWithUserId,
|
||||
createdAt: pagadorShares.createdAt,
|
||||
userName: usersTable.name,
|
||||
userEmail: usersTable.email,
|
||||
})
|
||||
.from(pagadorShares)
|
||||
.innerJoin(
|
||||
usersTable,
|
||||
eq(pagadorShares.sharedWithUserId, usersTable.id)
|
||||
)
|
||||
.where(eq(pagadorShares.pagadorId, pagadorId));
|
||||
const shareRows = await db
|
||||
.select({
|
||||
id: pagadorShares.id,
|
||||
sharedWithUserId: pagadorShares.sharedWithUserId,
|
||||
createdAt: pagadorShares.createdAt,
|
||||
userName: usersTable.name,
|
||||
userEmail: usersTable.email,
|
||||
})
|
||||
.from(pagadorShares)
|
||||
.innerJoin(usersTable, eq(pagadorShares.sharedWithUserId, usersTable.id))
|
||||
.where(eq(pagadorShares.pagadorId, pagadorId));
|
||||
|
||||
return shareRows.map((share) => ({
|
||||
id: share.id,
|
||||
userId: share.sharedWithUserId,
|
||||
name: share.userName ?? "Usuário",
|
||||
email: share.userEmail ?? "email não informado",
|
||||
createdAt: share.createdAt?.toISOString() ?? new Date().toISOString(),
|
||||
}));
|
||||
return shareRows.map((share) => ({
|
||||
id: share.id,
|
||||
userId: share.sharedWithUserId,
|
||||
name: share.userName ?? "Usuário",
|
||||
email: share.userEmail ?? "email não informado",
|
||||
createdAt: share.createdAt?.toISOString() ?? new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchCurrentUserShare(
|
||||
pagadorId: string,
|
||||
userId: string
|
||||
pagadorId: string,
|
||||
userId: string,
|
||||
): Promise<{ id: string; createdAt: string } | null> {
|
||||
const shareRow = await db.query.pagadorShares.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: and(
|
||||
eq(pagadorShares.pagadorId, pagadorId),
|
||||
eq(pagadorShares.sharedWithUserId, userId)
|
||||
),
|
||||
});
|
||||
const shareRow = await db.query.pagadorShares.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: and(
|
||||
eq(pagadorShares.pagadorId, pagadorId),
|
||||
eq(pagadorShares.sharedWithUserId, userId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!shareRow) {
|
||||
return null;
|
||||
}
|
||||
if (!shareRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: shareRow.id,
|
||||
createdAt: shareRow.createdAt?.toISOString() ?? new Date().toISOString(),
|
||||
};
|
||||
return {
|
||||
id: shareRow.id,
|
||||
createdAt: shareRow.createdAt?.toISOString() ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPagadorLancamentos(filters: SQL[]) {
|
||||
const lancamentoRows = await db
|
||||
.select({
|
||||
lancamento: lancamentos,
|
||||
pagador: pagadores,
|
||||
conta: contas,
|
||||
cartao: cartoes,
|
||||
categoria: categorias,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.where(and(...filters))
|
||||
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
|
||||
const lancamentoRows = await db
|
||||
.select({
|
||||
lancamento: lancamentos,
|
||||
pagador: pagadores,
|
||||
conta: contas,
|
||||
cartao: cartoes,
|
||||
categoria: categorias,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.where(and(...filters))
|
||||
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
|
||||
|
||||
// Transformar resultado para o formato esperado
|
||||
return lancamentoRows.map((row: any) => ({
|
||||
...row.lancamento,
|
||||
pagador: row.pagador,
|
||||
conta: row.conta,
|
||||
cartao: row.cartao,
|
||||
categoria: row.categoria,
|
||||
}));
|
||||
// Transformar resultado para o formato esperado
|
||||
return lancamentoRows.map((row: any) => ({
|
||||
...row.lancamento,
|
||||
pagador: row.pagador,
|
||||
conta: row.conta,
|
||||
cartao: row.cartao,
|
||||
categoria: row.categoria,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -5,80 +5,80 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
* Layout: MonthPicker + Info do pagador + Tabs (Visão Geral / Lançamentos)
|
||||
*/
|
||||
export default function PagadorDetailsLoading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
{/* Month Picker placeholder */}
|
||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
{/* Month Picker placeholder */}
|
||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||
|
||||
{/* Info do Pagador (sempre visível) */}
|
||||
<div className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<Skeleton className="size-20 rounded-full bg-foreground/10" />
|
||||
{/* Info do Pagador (sempre visível) */}
|
||||
<div className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<Skeleton className="size-20 rounded-full bg-foreground/10" />
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* Nome + Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-7 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-20 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* Nome + Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-7 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-20 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
|
||||
{/* Email */}
|
||||
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-2 rounded-full bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-2 rounded-full bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-2 border-b">
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
</div>
|
||||
{/* Tabs */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-2 border-b">
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Conteúdo da aba Visão Geral (grid de cards) */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Card de resumo mensal */}
|
||||
<div className="rounded-2xl border p-6 space-y-4 lg:col-span-2">
|
||||
<Skeleton className="h-6 w-48 rounded-2xl bg-foreground/10" />
|
||||
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-7 w-full rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Conteúdo da aba Visão Geral (grid de cards) */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Card de resumo mensal */}
|
||||
<div className="rounded-2xl border p-6 space-y-4 lg:col-span-2">
|
||||
<Skeleton className="h-6 w-48 rounded-2xl bg-foreground/10" />
|
||||
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-7 w-full rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outros cards */}
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-5 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-32 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
<div className="space-y-3 pt-4">
|
||||
<Skeleton className="h-5 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-5 w-3/4 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-5 w-1/2 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
{/* Outros cards */}
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-5 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-32 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
<div className="space-y-3 pt-4">
|
||||
<Skeleton className="h-5 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-5 w-3/4 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-5 w-1/2 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,435 +1,443 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import type {
|
||||
ContaCartaoFilterOption,
|
||||
LancamentoFilterOption,
|
||||
LancamentoItem,
|
||||
SelectOption,
|
||||
} from "@/components/lancamentos/types";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { PagadorCardUsageCard } from "@/components/pagadores/details/pagador-card-usage-card";
|
||||
import { PagadorHistoryCard } from "@/components/pagadores/details/pagador-history-card";
|
||||
import { PagadorInfoCard } from "@/components/pagadores/details/pagador-info-card";
|
||||
import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card";
|
||||
import { PagadorMonthlySummaryCard } from "@/components/pagadores/details/pagador-monthly-summary-card";
|
||||
import { PagadorBoletoCard } from "@/components/pagadores/details/pagador-payment-method-cards";
|
||||
import { PagadorSharingCard } from "@/components/pagadores/details/pagador-sharing-card";
|
||||
import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import type {
|
||||
ContaCartaoFilterOption,
|
||||
LancamentoFilterOption,
|
||||
LancamentoItem,
|
||||
SelectOption,
|
||||
} from "@/components/lancamentos/types";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { pagadores } from "@/db/schema";
|
||||
import type { pagadores } from "@/db/schema";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import {
|
||||
buildLancamentoWhere,
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
buildSlugMaps,
|
||||
extractLancamentoSearchFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
getSingleParam,
|
||||
mapLancamentosData,
|
||||
type LancamentoSearchFilters,
|
||||
type ResolvedSearchParams,
|
||||
type SlugMaps,
|
||||
type SluggedFilters,
|
||||
buildLancamentoWhere,
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
buildSlugMaps,
|
||||
extractLancamentoSearchFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
getSingleParam,
|
||||
type LancamentoSearchFilters,
|
||||
mapLancamentosData,
|
||||
type ResolvedSearchParams,
|
||||
type SluggedFilters,
|
||||
type SlugMaps,
|
||||
} from "@/lib/lancamentos/page-helpers";
|
||||
import { getPagadorAccess } from "@/lib/pagadores/access";
|
||||
import {
|
||||
fetchPagadorBoletoStats,
|
||||
fetchPagadorCardUsage,
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
import {
|
||||
fetchPagadorBoletoStats,
|
||||
fetchPagadorCardUsage,
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchPagadorLancamentos, fetchPagadorShares, fetchCurrentUserShare } from "./data";
|
||||
fetchCurrentUserShare,
|
||||
fetchPagadorLancamentos,
|
||||
fetchPagadorShares,
|
||||
} from "./data";
|
||||
|
||||
type PageSearchParams = Promise<ResolvedSearchParams>;
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ pagadorId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
params: Promise<{ pagadorId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
};
|
||||
|
||||
const capitalize = (value: string) =>
|
||||
value.length ? value.charAt(0).toUpperCase().concat(value.slice(1)) : value;
|
||||
value.length ? value.charAt(0).toUpperCase().concat(value.slice(1)) : value;
|
||||
|
||||
const EMPTY_FILTERS: LancamentoSearchFilters = {
|
||||
transactionFilter: null,
|
||||
conditionFilter: null,
|
||||
paymentFilter: null,
|
||||
pagadorFilter: null,
|
||||
categoriaFilter: null,
|
||||
contaCartaoFilter: null,
|
||||
searchFilter: null,
|
||||
transactionFilter: null,
|
||||
conditionFilter: null,
|
||||
paymentFilter: null,
|
||||
pagadorFilter: null,
|
||||
categoriaFilter: null,
|
||||
contaCartaoFilter: null,
|
||||
searchFilter: null,
|
||||
};
|
||||
|
||||
const createEmptySlugMaps = (): SlugMaps => ({
|
||||
pagador: new Map(),
|
||||
categoria: new Map(),
|
||||
conta: new Map(),
|
||||
cartao: new Map(),
|
||||
pagador: new Map(),
|
||||
categoria: new Map(),
|
||||
conta: new Map(),
|
||||
cartao: new Map(),
|
||||
});
|
||||
|
||||
type OptionSet = ReturnType<typeof buildOptionSets>;
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { pagadorId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
const { pagadorId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
|
||||
const access = await getPagadorAccess(userId, pagadorId);
|
||||
const access = await getPagadorAccess(userId, pagadorId);
|
||||
|
||||
if (!access) {
|
||||
notFound();
|
||||
}
|
||||
if (!access) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { pagador, canEdit } = access;
|
||||
const dataOwnerId = pagador.userId;
|
||||
const { pagador, canEdit } = access;
|
||||
const dataOwnerId = pagador.userId;
|
||||
|
||||
const periodoParamRaw = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const {
|
||||
period: selectedPeriod,
|
||||
monthName,
|
||||
year,
|
||||
} = parsePeriodParam(periodoParamRaw);
|
||||
const periodLabel = `${capitalize(monthName)} de ${year}`;
|
||||
const periodoParamRaw = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const {
|
||||
period: selectedPeriod,
|
||||
monthName,
|
||||
year,
|
||||
} = parsePeriodParam(periodoParamRaw);
|
||||
const periodLabel = `${capitalize(monthName)} de ${year}`;
|
||||
|
||||
const allSearchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
|
||||
const searchFilters = canEdit
|
||||
? allSearchFilters
|
||||
: {
|
||||
...EMPTY_FILTERS,
|
||||
searchFilter: allSearchFilters.searchFilter, // Permitir busca mesmo em modo read-only
|
||||
};
|
||||
const allSearchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
|
||||
const searchFilters = canEdit
|
||||
? allSearchFilters
|
||||
: {
|
||||
...EMPTY_FILTERS,
|
||||
searchFilter: allSearchFilters.searchFilter, // Permitir busca mesmo em modo read-only
|
||||
};
|
||||
|
||||
let filterSources: Awaited<
|
||||
ReturnType<typeof fetchLancamentoFilterSources>
|
||||
> | null = null;
|
||||
let loggedUserFilterSources: Awaited<
|
||||
ReturnType<typeof fetchLancamentoFilterSources>
|
||||
> | null = null;
|
||||
let sluggedFilters: SluggedFilters;
|
||||
let slugMaps: SlugMaps;
|
||||
let filterSources: Awaited<
|
||||
ReturnType<typeof fetchLancamentoFilterSources>
|
||||
> | null = null;
|
||||
let loggedUserFilterSources: Awaited<
|
||||
ReturnType<typeof fetchLancamentoFilterSources>
|
||||
> | null = null;
|
||||
let sluggedFilters: SluggedFilters;
|
||||
let slugMaps: SlugMaps;
|
||||
|
||||
if (canEdit) {
|
||||
filterSources = await fetchLancamentoFilterSources(dataOwnerId);
|
||||
sluggedFilters = buildSluggedFilters(filterSources);
|
||||
slugMaps = buildSlugMaps(sluggedFilters);
|
||||
} else {
|
||||
// Buscar opções do usuário logado para usar ao importar
|
||||
loggedUserFilterSources = await fetchLancamentoFilterSources(userId);
|
||||
sluggedFilters = {
|
||||
pagadorFiltersRaw: [],
|
||||
categoriaFiltersRaw: [],
|
||||
contaFiltersRaw: [],
|
||||
cartaoFiltersRaw: [],
|
||||
};
|
||||
slugMaps = createEmptySlugMaps();
|
||||
}
|
||||
if (canEdit) {
|
||||
filterSources = await fetchLancamentoFilterSources(dataOwnerId);
|
||||
sluggedFilters = buildSluggedFilters(filterSources);
|
||||
slugMaps = buildSlugMaps(sluggedFilters);
|
||||
} else {
|
||||
// Buscar opções do usuário logado para usar ao importar
|
||||
loggedUserFilterSources = await fetchLancamentoFilterSources(userId);
|
||||
sluggedFilters = {
|
||||
pagadorFiltersRaw: [],
|
||||
categoriaFiltersRaw: [],
|
||||
contaFiltersRaw: [],
|
||||
cartaoFiltersRaw: [],
|
||||
};
|
||||
slugMaps = createEmptySlugMaps();
|
||||
}
|
||||
|
||||
const filters = buildLancamentoWhere({
|
||||
userId: dataOwnerId,
|
||||
period: selectedPeriod,
|
||||
filters: searchFilters,
|
||||
slugMaps,
|
||||
pagadorId: pagador.id,
|
||||
});
|
||||
const filters = buildLancamentoWhere({
|
||||
userId: dataOwnerId,
|
||||
period: selectedPeriod,
|
||||
filters: searchFilters,
|
||||
slugMaps,
|
||||
pagadorId: pagador.id,
|
||||
});
|
||||
|
||||
const sharesPromise = canEdit
|
||||
? fetchPagadorShares(pagador.id)
|
||||
: Promise.resolve([]);
|
||||
const sharesPromise = canEdit
|
||||
? fetchPagadorShares(pagador.id)
|
||||
: Promise.resolve([]);
|
||||
|
||||
const currentUserSharePromise = !canEdit
|
||||
? fetchCurrentUserShare(pagador.id, userId)
|
||||
: Promise.resolve(null);
|
||||
const currentUserSharePromise = !canEdit
|
||||
? fetchCurrentUserShare(pagador.id, userId)
|
||||
: Promise.resolve(null);
|
||||
|
||||
const [
|
||||
lancamentoRows,
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
shareRows,
|
||||
currentUserShare,
|
||||
estabelecimentos,
|
||||
] = await Promise.all([
|
||||
fetchPagadorLancamentos(filters),
|
||||
fetchPagadorMonthlyBreakdown({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorHistory({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorCardUsage({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorBoletoStats({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
sharesPromise,
|
||||
currentUserSharePromise,
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
const [
|
||||
lancamentoRows,
|
||||
monthlyBreakdown,
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
shareRows,
|
||||
currentUserShare,
|
||||
estabelecimentos,
|
||||
] = await Promise.all([
|
||||
fetchPagadorLancamentos(filters),
|
||||
fetchPagadorMonthlyBreakdown({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorHistory({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorCardUsage({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorBoletoStats({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
sharesPromise,
|
||||
currentUserSharePromise,
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
|
||||
const mappedLancamentos = mapLancamentosData(lancamentoRows);
|
||||
const lancamentosData = canEdit
|
||||
? mappedLancamentos
|
||||
: mappedLancamentos.map((item) => ({ ...item, readonly: true }));
|
||||
const mappedLancamentos = mapLancamentosData(lancamentoRows);
|
||||
const lancamentosData = canEdit
|
||||
? mappedLancamentos
|
||||
: mappedLancamentos.map((item) => ({ ...item, readonly: true }));
|
||||
|
||||
const pagadorSharesData = shareRows;
|
||||
const pagadorSharesData = shareRows;
|
||||
|
||||
let optionSets: OptionSet;
|
||||
let loggedUserOptionSets: OptionSet | null = null;
|
||||
let effectiveSluggedFilters = sluggedFilters;
|
||||
let optionSets: OptionSet;
|
||||
let loggedUserOptionSets: OptionSet | null = null;
|
||||
let effectiveSluggedFilters = sluggedFilters;
|
||||
|
||||
if (canEdit && filterSources) {
|
||||
optionSets = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
});
|
||||
} else {
|
||||
effectiveSluggedFilters = {
|
||||
pagadorFiltersRaw: [
|
||||
{
|
||||
id: pagador.id,
|
||||
label: pagador.name,
|
||||
slug: pagador.id,
|
||||
role: pagador.role,
|
||||
},
|
||||
],
|
||||
categoriaFiltersRaw: [],
|
||||
contaFiltersRaw: [],
|
||||
cartaoFiltersRaw: [],
|
||||
};
|
||||
optionSets = buildReadOnlyOptionSets(lancamentosData, pagador);
|
||||
if (canEdit && filterSources) {
|
||||
optionSets = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
});
|
||||
} else {
|
||||
effectiveSluggedFilters = {
|
||||
pagadorFiltersRaw: [
|
||||
{
|
||||
id: pagador.id,
|
||||
label: pagador.name,
|
||||
slug: pagador.id,
|
||||
role: pagador.role,
|
||||
},
|
||||
],
|
||||
categoriaFiltersRaw: [],
|
||||
contaFiltersRaw: [],
|
||||
cartaoFiltersRaw: [],
|
||||
};
|
||||
optionSets = buildReadOnlyOptionSets(lancamentosData, pagador);
|
||||
|
||||
// Construir opções do usuário logado para usar ao importar
|
||||
if (loggedUserFilterSources) {
|
||||
const loggedUserSluggedFilters = buildSluggedFilters(loggedUserFilterSources);
|
||||
loggedUserOptionSets = buildOptionSets({
|
||||
...loggedUserSluggedFilters,
|
||||
pagadorRows: loggedUserFilterSources.pagadorRows,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Construir opções do usuário logado para usar ao importar
|
||||
if (loggedUserFilterSources) {
|
||||
const loggedUserSluggedFilters = buildSluggedFilters(
|
||||
loggedUserFilterSources,
|
||||
);
|
||||
loggedUserOptionSets = buildOptionSets({
|
||||
...loggedUserSluggedFilters,
|
||||
pagadorRows: loggedUserFilterSources.pagadorRows,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const pagadorSlug =
|
||||
effectiveSluggedFilters.pagadorFiltersRaw.find(
|
||||
(item) => item.id === pagador.id
|
||||
)?.slug ?? null;
|
||||
const pagadorSlug =
|
||||
effectiveSluggedFilters.pagadorFiltersRaw.find(
|
||||
(item) => item.id === pagador.id,
|
||||
)?.slug ?? null;
|
||||
|
||||
const pagadorFilterOptions = pagadorSlug
|
||||
? optionSets.pagadorFilterOptions.filter(
|
||||
(option) => option.slug === pagadorSlug
|
||||
)
|
||||
: optionSets.pagadorFilterOptions;
|
||||
const pagadorFilterOptions = pagadorSlug
|
||||
? optionSets.pagadorFilterOptions.filter(
|
||||
(option) => option.slug === pagadorSlug,
|
||||
)
|
||||
: optionSets.pagadorFilterOptions;
|
||||
|
||||
const pagadorData = {
|
||||
id: pagador.id,
|
||||
name: pagador.name,
|
||||
email: pagador.email ?? null,
|
||||
avatarUrl: pagador.avatarUrl ?? null,
|
||||
status: pagador.status,
|
||||
note: pagador.note ?? null,
|
||||
role: pagador.role ?? null,
|
||||
isAutoSend: pagador.isAutoSend ?? false,
|
||||
createdAt: pagador.createdAt
|
||||
? pagador.createdAt.toISOString()
|
||||
: new Date().toISOString(),
|
||||
lastMailAt: pagador.lastMailAt ? pagador.lastMailAt.toISOString() : null,
|
||||
shareCode: canEdit ? pagador.shareCode : null,
|
||||
canEdit,
|
||||
};
|
||||
const pagadorData = {
|
||||
id: pagador.id,
|
||||
name: pagador.name,
|
||||
email: pagador.email ?? null,
|
||||
avatarUrl: pagador.avatarUrl ?? null,
|
||||
status: pagador.status,
|
||||
note: pagador.note ?? null,
|
||||
role: pagador.role ?? null,
|
||||
isAutoSend: pagador.isAutoSend ?? false,
|
||||
createdAt: pagador.createdAt
|
||||
? pagador.createdAt.toISOString()
|
||||
: new Date().toISOString(),
|
||||
lastMailAt: pagador.lastMailAt ? pagador.lastMailAt.toISOString() : null,
|
||||
shareCode: canEdit ? pagador.shareCode : null,
|
||||
canEdit,
|
||||
};
|
||||
|
||||
const summaryPreview = {
|
||||
periodLabel,
|
||||
totalExpenses: monthlyBreakdown.totalExpenses,
|
||||
paymentSplits: monthlyBreakdown.paymentSplits,
|
||||
cardUsage: cardUsage.slice(0, 3).map((item) => ({
|
||||
name: item.name,
|
||||
amount: item.amount,
|
||||
})),
|
||||
boletoStats: {
|
||||
totalAmount: boletoStats.totalAmount,
|
||||
paidAmount: boletoStats.paidAmount,
|
||||
pendingAmount: boletoStats.pendingAmount,
|
||||
paidCount: boletoStats.paidCount,
|
||||
pendingCount: boletoStats.pendingCount,
|
||||
},
|
||||
lancamentoCount: lancamentosData.length,
|
||||
};
|
||||
const summaryPreview = {
|
||||
periodLabel,
|
||||
totalExpenses: monthlyBreakdown.totalExpenses,
|
||||
paymentSplits: monthlyBreakdown.paymentSplits,
|
||||
cardUsage: cardUsage.slice(0, 3).map((item) => ({
|
||||
name: item.name,
|
||||
amount: item.amount,
|
||||
})),
|
||||
boletoStats: {
|
||||
totalAmount: boletoStats.totalAmount,
|
||||
paidAmount: boletoStats.paidAmount,
|
||||
pendingAmount: boletoStats.pendingAmount,
|
||||
paidCount: boletoStats.paidCount,
|
||||
pendingCount: boletoStats.pendingCount,
|
||||
},
|
||||
lancamentoCount: lancamentosData.length,
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
|
||||
<Tabs defaultValue="profile" className="w-full">
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="profile">Perfil</TabsTrigger>
|
||||
<TabsTrigger value="painel">Painel</TabsTrigger>
|
||||
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs defaultValue="profile" className="w-full">
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="profile">Perfil</TabsTrigger>
|
||||
<TabsTrigger value="painel">Painel</TabsTrigger>
|
||||
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-4">
|
||||
<section>
|
||||
<PagadorInfoCard
|
||||
pagador={pagadorData}
|
||||
selectedPeriod={selectedPeriod}
|
||||
summary={summaryPreview}
|
||||
/>
|
||||
</section>
|
||||
{canEdit && pagadorData.shareCode ? (
|
||||
<PagadorSharingCard
|
||||
pagadorId={pagador.id}
|
||||
shareCode={pagadorData.shareCode}
|
||||
shares={pagadorSharesData}
|
||||
/>
|
||||
) : null}
|
||||
{!canEdit && currentUserShare ? (
|
||||
<PagadorLeaveShareCard
|
||||
shareId={currentUserShare.id}
|
||||
pagadorName={pagadorData.name}
|
||||
createdAt={currentUserShare.createdAt}
|
||||
/>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
<TabsContent value="profile" className="space-y-4">
|
||||
<section>
|
||||
<PagadorInfoCard
|
||||
pagador={pagadorData}
|
||||
selectedPeriod={selectedPeriod}
|
||||
summary={summaryPreview}
|
||||
/>
|
||||
</section>
|
||||
{canEdit && pagadorData.shareCode ? (
|
||||
<PagadorSharingCard
|
||||
pagadorId={pagador.id}
|
||||
shareCode={pagadorData.shareCode}
|
||||
shares={pagadorSharesData}
|
||||
/>
|
||||
) : null}
|
||||
{!canEdit && currentUserShare ? (
|
||||
<PagadorLeaveShareCard
|
||||
shareId={currentUserShare.id}
|
||||
pagadorName={pagadorData.name}
|
||||
createdAt={currentUserShare.createdAt}
|
||||
/>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="painel" className="space-y-4">
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<PagadorMonthlySummaryCard
|
||||
periodLabel={periodLabel}
|
||||
breakdown={monthlyBreakdown}
|
||||
/>
|
||||
<PagadorHistoryCard data={historyData} />
|
||||
</section>
|
||||
<TabsContent value="painel" className="space-y-4">
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<PagadorMonthlySummaryCard
|
||||
periodLabel={periodLabel}
|
||||
breakdown={monthlyBreakdown}
|
||||
/>
|
||||
<PagadorHistoryCard data={historyData} />
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<PagadorCardUsageCard items={cardUsage} />
|
||||
<PagadorBoletoCard stats={boletoStats} />
|
||||
</section>
|
||||
</TabsContent>
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<PagadorCardUsageCard items={cardUsage} />
|
||||
<PagadorBoletoCard stats={boletoStats} />
|
||||
</section>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lancamentos">
|
||||
<section className="flex flex-col gap-4">
|
||||
<LancamentosSection
|
||||
currentUserId={userId}
|
||||
lancamentos={lancamentosData}
|
||||
pagadorOptions={optionSets.pagadorOptions}
|
||||
splitPagadorOptions={optionSets.splitPagadorOptions}
|
||||
defaultPagadorId={pagador.id}
|
||||
contaOptions={optionSets.contaOptions}
|
||||
cartaoOptions={optionSets.cartaoOptions}
|
||||
categoriaOptions={optionSets.categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={optionSets.categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={optionSets.contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={canEdit}
|
||||
importPagadorOptions={loggedUserOptionSets?.pagadorOptions}
|
||||
importSplitPagadorOptions={loggedUserOptionSets?.splitPagadorOptions}
|
||||
importDefaultPagadorId={loggedUserOptionSets?.defaultPagadorId}
|
||||
importContaOptions={loggedUserOptionSets?.contaOptions}
|
||||
importCartaoOptions={loggedUserOptionSets?.cartaoOptions}
|
||||
importCategoriaOptions={loggedUserOptionSets?.categoriaOptions}
|
||||
/>
|
||||
</section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
);
|
||||
<TabsContent value="lancamentos">
|
||||
<section className="flex flex-col gap-4">
|
||||
<LancamentosSection
|
||||
currentUserId={userId}
|
||||
lancamentos={lancamentosData}
|
||||
pagadorOptions={optionSets.pagadorOptions}
|
||||
splitPagadorOptions={optionSets.splitPagadorOptions}
|
||||
defaultPagadorId={pagador.id}
|
||||
contaOptions={optionSets.contaOptions}
|
||||
cartaoOptions={optionSets.cartaoOptions}
|
||||
categoriaOptions={optionSets.categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={optionSets.categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={optionSets.contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={canEdit}
|
||||
importPagadorOptions={loggedUserOptionSets?.pagadorOptions}
|
||||
importSplitPagadorOptions={
|
||||
loggedUserOptionSets?.splitPagadorOptions
|
||||
}
|
||||
importDefaultPagadorId={loggedUserOptionSets?.defaultPagadorId}
|
||||
importContaOptions={loggedUserOptionSets?.contaOptions}
|
||||
importCartaoOptions={loggedUserOptionSets?.cartaoOptions}
|
||||
importCategoriaOptions={loggedUserOptionSets?.categoriaOptions}
|
||||
/>
|
||||
</section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const normalizeOptionLabel = (
|
||||
value: string | null | undefined,
|
||||
fallback: string
|
||||
value: string | null | undefined,
|
||||
fallback: string,
|
||||
) => (value?.trim().length ? value.trim() : fallback);
|
||||
|
||||
function buildReadOnlyOptionSets(
|
||||
items: LancamentoItem[],
|
||||
pagador: typeof pagadores.$inferSelect
|
||||
items: LancamentoItem[],
|
||||
pagador: typeof pagadores.$inferSelect,
|
||||
): OptionSet {
|
||||
const pagadorLabel = normalizeOptionLabel(pagador.name, "Pagador");
|
||||
const pagadorOptions: SelectOption[] = [
|
||||
{
|
||||
value: pagador.id,
|
||||
label: pagadorLabel,
|
||||
slug: pagador.id,
|
||||
},
|
||||
];
|
||||
const pagadorLabel = normalizeOptionLabel(pagador.name, "Pagador");
|
||||
const pagadorOptions: SelectOption[] = [
|
||||
{
|
||||
value: pagador.id,
|
||||
label: pagadorLabel,
|
||||
slug: pagador.id,
|
||||
},
|
||||
];
|
||||
|
||||
const contaOptionsMap = new Map<string, SelectOption>();
|
||||
const cartaoOptionsMap = new Map<string, SelectOption>();
|
||||
const categoriaOptionsMap = new Map<string, SelectOption>();
|
||||
const contaOptionsMap = new Map<string, SelectOption>();
|
||||
const cartaoOptionsMap = new Map<string, SelectOption>();
|
||||
const categoriaOptionsMap = new Map<string, SelectOption>();
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.contaId && !contaOptionsMap.has(item.contaId)) {
|
||||
contaOptionsMap.set(item.contaId, {
|
||||
value: item.contaId,
|
||||
label: normalizeOptionLabel(item.contaName, "Conta sem nome"),
|
||||
slug: item.contaId,
|
||||
});
|
||||
}
|
||||
if (item.cartaoId && !cartaoOptionsMap.has(item.cartaoId)) {
|
||||
cartaoOptionsMap.set(item.cartaoId, {
|
||||
value: item.cartaoId,
|
||||
label: normalizeOptionLabel(item.cartaoName, "Cartão sem nome"),
|
||||
slug: item.cartaoId,
|
||||
});
|
||||
}
|
||||
if (item.categoriaId && !categoriaOptionsMap.has(item.categoriaId)) {
|
||||
categoriaOptionsMap.set(item.categoriaId, {
|
||||
value: item.categoriaId,
|
||||
label: normalizeOptionLabel(item.categoriaName, "Categoria"),
|
||||
slug: item.categoriaId,
|
||||
});
|
||||
}
|
||||
});
|
||||
items.forEach((item) => {
|
||||
if (item.contaId && !contaOptionsMap.has(item.contaId)) {
|
||||
contaOptionsMap.set(item.contaId, {
|
||||
value: item.contaId,
|
||||
label: normalizeOptionLabel(item.contaName, "Conta sem nome"),
|
||||
slug: item.contaId,
|
||||
});
|
||||
}
|
||||
if (item.cartaoId && !cartaoOptionsMap.has(item.cartaoId)) {
|
||||
cartaoOptionsMap.set(item.cartaoId, {
|
||||
value: item.cartaoId,
|
||||
label: normalizeOptionLabel(item.cartaoName, "Cartão sem nome"),
|
||||
slug: item.cartaoId,
|
||||
});
|
||||
}
|
||||
if (item.categoriaId && !categoriaOptionsMap.has(item.categoriaId)) {
|
||||
categoriaOptionsMap.set(item.categoriaId, {
|
||||
value: item.categoriaId,
|
||||
label: normalizeOptionLabel(item.categoriaName, "Categoria"),
|
||||
slug: item.categoriaId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const contaOptions = Array.from(contaOptionsMap.values());
|
||||
const cartaoOptions = Array.from(cartaoOptionsMap.values());
|
||||
const categoriaOptions = Array.from(categoriaOptionsMap.values());
|
||||
const contaOptions = Array.from(contaOptionsMap.values());
|
||||
const cartaoOptions = Array.from(cartaoOptionsMap.values());
|
||||
const categoriaOptions = Array.from(categoriaOptionsMap.values());
|
||||
|
||||
const pagadorFilterOptions: LancamentoFilterOption[] = [
|
||||
{ slug: pagador.id, label: pagadorLabel },
|
||||
];
|
||||
const pagadorFilterOptions: LancamentoFilterOption[] = [
|
||||
{ slug: pagador.id, label: pagadorLabel },
|
||||
];
|
||||
|
||||
const categoriaFilterOptions: LancamentoFilterOption[] = categoriaOptions.map(
|
||||
(option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
})
|
||||
);
|
||||
const categoriaFilterOptions: LancamentoFilterOption[] = categoriaOptions.map(
|
||||
(option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
}),
|
||||
);
|
||||
|
||||
const contaCartaoFilterOptions: ContaCartaoFilterOption[] = [
|
||||
...contaOptions.map((option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
kind: "conta" as const,
|
||||
})),
|
||||
...cartaoOptions.map((option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
kind: "cartao" as const,
|
||||
})),
|
||||
];
|
||||
const contaCartaoFilterOptions: ContaCartaoFilterOption[] = [
|
||||
...contaOptions.map((option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
kind: "conta" as const,
|
||||
})),
|
||||
...cartaoOptions.map((option) => ({
|
||||
slug: option.value,
|
||||
label: option.label,
|
||||
kind: "cartao" as const,
|
||||
})),
|
||||
];
|
||||
|
||||
return {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions: [],
|
||||
defaultPagadorId: pagador.id,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
};
|
||||
return {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions: [],
|
||||
defaultPagadorId: pagador.id,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user