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:
Felipe Coutinho
2026-01-27 13:15:37 +00:00
parent 8ffe61c59b
commit a7f63fb77a
442 changed files with 66141 additions and 69292 deletions

View File

@@ -1,85 +1,85 @@
import { pagadores } from "@/db/schema";
import { db } from "@/lib/db";
import { inArray } from "drizzle-orm";
import { Resend } from "resend";
import { pagadores } from "@/db/schema";
import { db } from "@/lib/db";
type ActionType = "created" | "deleted";
export type NotificationEntry = {
pagadorId: string;
name: string | null;
amount: number;
transactionType: string | null;
paymentMethod: string | null;
condition: string | null;
purchaseDate: Date | null;
period: string | null;
note: string | null;
pagadorId: string;
name: string | null;
amount: number;
transactionType: string | null;
paymentMethod: string | null;
condition: string | null;
purchaseDate: Date | null;
period: string | null;
note: string | null;
};
export type PagadorNotificationRequest = {
userLabel: string;
action: ActionType;
entriesByPagador: Map<string, NotificationEntry[]>;
userLabel: string;
action: ActionType;
entriesByPagador: Map<string, NotificationEntry[]>;
};
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) => {
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",
});
};
const buildHtmlBody = ({
userLabel,
action,
entries,
userLabel,
action,
entries,
}: {
userLabel: string;
action: ActionType;
entries: NotificationEntry[];
userLabel: string;
action: ActionType;
entries: NotificationEntry[];
}) => {
const actionLabel =
action === "created" ? "Novo lançamento registrado" : "Lançamento removido";
const actionDescription =
action === "created"
? "Um novo lançamento foi registrado em seu nome."
: "Um lançamento anteriormente registrado em seu nome foi removido.";
const actionLabel =
action === "created" ? "Novo lançamento registrado" : "Lançamento removido";
const actionDescription =
action === "created"
? "Um novo lançamento foi registrado em seu nome."
: "Um lançamento anteriormente registrado em seu nome foi removido.";
const rows = entries
.map((entry) => {
const label =
entry.transactionType === "Despesa"
? formatCurrency(Math.abs(entry.amount)) + " (Despesa)"
: formatCurrency(Math.abs(entry.amount)) + " (Receita)";
return `<tr>
const rows = entries
.map((entry) => {
const label =
entry.transactionType === "Despesa"
? `${formatCurrency(Math.abs(entry.amount))} (Despesa)`
: `${formatCurrency(Math.abs(entry.amount))} (Receita)`;
return `<tr>
<td style="padding:8px;border-bottom:1px solid #e2e8f0;">${formatDate(
entry.purchaseDate
)}</td>
entry.purchaseDate,
)}</td>
<td style="padding:8px;border-bottom:1px solid #e2e8f0;">${
entry.name ?? "Sem descrição"
}</td>
entry.name ?? "Sem descrição"
}</td>
<td style="padding:8px;border-bottom:1px solid #e2e8f0;">${
entry.paymentMethod ?? "—"
}</td>
entry.paymentMethod ?? "—"
}</td>
<td style="padding:8px;border-bottom:1px solid #e2e8f0;">${
entry.condition ?? "—"
}</td>
entry.condition ?? "—"
}</td>
<td style="padding:8px;border-bottom:1px solid #e2e8f0;text-align:right;">${label}</td>
</tr>`;
})
.join("");
})
.join("");
return `
return `
<div style="font-family:'Inter',Arial,sans-serif;color:#0f172a;line-height:1.5;">
<h2 style="margin:0 0 8px 0;font-size:20px;">${actionLabel}</h2>
<p style="margin:0 0 16px 0;color:#475569;">${actionDescription}</p>
@@ -107,131 +107,131 @@ const buildHtmlBody = ({
};
export async function sendPagadorAutoEmails({
userLabel,
action,
entriesByPagador,
userLabel,
action,
entriesByPagador,
}: PagadorNotificationRequest) {
"use server";
"use server";
if (entriesByPagador.size === 0) {
return;
}
if (entriesByPagador.size === 0) {
return;
}
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) {
console.warn(
"RESEND_API_KEY não configurada. Envio automático de lançamentos ignorado."
);
return;
}
if (!resendApiKey) {
console.warn(
"RESEND_API_KEY não configurada. Envio automático de lançamentos ignorado.",
);
return;
}
const pagadorIds = Array.from(entriesByPagador.keys());
if (pagadorIds.length === 0) {
return;
}
const pagadorIds = Array.from(entriesByPagador.keys());
if (pagadorIds.length === 0) {
return;
}
const pagadorRows = await db.query.pagadores.findMany({
where: inArray(pagadores.id, pagadorIds),
});
const pagadorRows = await db.query.pagadores.findMany({
where: inArray(pagadores.id, pagadorIds),
});
if (pagadorRows.length === 0) {
return;
}
if (pagadorRows.length === 0) {
return;
}
const resend = new Resend(resendApiKey);
const subjectPrefix =
action === "created" ? "Novo lançamento" : "Lançamento removido";
const resend = new Resend(resendApiKey);
const subjectPrefix =
action === "created" ? "Novo lançamento" : "Lançamento removido";
const results = await Promise.allSettled(
pagadorRows.map(async (pagador) => {
if (!pagador.email || !pagador.isAutoSend) {
return;
}
const results = await Promise.allSettled(
pagadorRows.map(async (pagador) => {
if (!pagador.email || !pagador.isAutoSend) {
return;
}
const entries = entriesByPagador.get(pagador.id);
if (!entries || entries.length === 0) {
return;
}
const entries = entriesByPagador.get(pagador.id);
if (!entries || entries.length === 0) {
return;
}
const html = buildHtmlBody({
userLabel,
action,
entries,
});
const html = buildHtmlBody({
userLabel,
action,
entries,
});
await resend.emails.send({
from: resendFrom,
to: pagador.email,
subject: `${subjectPrefix} - ${pagador.name}`,
html,
});
})
);
await resend.emails.send({
from: resendFrom,
to: pagador.email,
subject: `${subjectPrefix} - ${pagador.name}`,
html,
});
}),
);
// Log any failed email sends
results.forEach((result, index) => {
if (result.status === "rejected") {
const pagador = pagadorRows[index];
console.error(
`Failed to send email notification to ${pagador?.name} (${pagador?.email}):`,
result.reason
);
}
});
// Log any failed email sends
results.forEach((result, index) => {
if (result.status === "rejected") {
const pagador = pagadorRows[index];
console.error(
`Failed to send email notification to ${pagador?.name} (${pagador?.email}):`,
result.reason,
);
}
});
}
export type RawNotificationRecord = {
pagadorId: string | null;
name: string | null;
amount: string | number | null;
transactionType: string | null;
paymentMethod: string | null;
condition: string | null;
purchaseDate: Date | string | null;
period: string | null;
note: string | null;
pagadorId: string | null;
name: string | null;
amount: string | number | null;
transactionType: string | null;
paymentMethod: string | null;
condition: string | null;
purchaseDate: Date | string | null;
period: string | null;
note: string | null;
};
export const buildEntriesByPagador = (
records: RawNotificationRecord[]
records: RawNotificationRecord[],
): Map<string, NotificationEntry[]> => {
const map = new Map<string, NotificationEntry[]>();
const map = new Map<string, NotificationEntry[]>();
records.forEach((record) => {
if (!record.pagadorId) {
return;
}
records.forEach((record) => {
if (!record.pagadorId) {
return;
}
const amount =
typeof record.amount === "number"
? record.amount
: Number(record.amount ?? 0);
const purchaseDate =
record.purchaseDate instanceof Date
? record.purchaseDate
: record.purchaseDate
? new Date(record.purchaseDate)
: null;
const amount =
typeof record.amount === "number"
? record.amount
: Number(record.amount ?? 0);
const purchaseDate =
record.purchaseDate instanceof Date
? record.purchaseDate
: record.purchaseDate
? new Date(record.purchaseDate)
: null;
const entry: NotificationEntry = {
pagadorId: record.pagadorId,
name: record.name ?? null,
amount,
transactionType: record.transactionType ?? null,
paymentMethod: record.paymentMethod ?? null,
condition: record.condition ?? null,
purchaseDate,
period: record.period ?? null,
note: record.note ?? null,
};
const entry: NotificationEntry = {
pagadorId: record.pagadorId,
name: record.name ?? null,
amount,
transactionType: record.transactionType ?? null,
paymentMethod: record.paymentMethod ?? null,
condition: record.condition ?? null,
purchaseDate,
period: record.period ?? null,
note: record.note ?? null,
};
const list = map.get(record.pagadorId) ?? [];
list.push(entry);
map.set(record.pagadorId, list);
});
const list = map.get(record.pagadorId) ?? [];
list.push(entry);
map.set(record.pagadorId, list);
});
return map;
return map;
};