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

@@ -8,127 +8,129 @@ import { z } from "zod";
* UUID schema with custom error message
*/
export const uuidSchema = (entityName: string = "ID") =>
z.string({ message: `${entityName} inválido.` }).uuid(`${entityName} inválido.`);
z
.string({ message: `${entityName} inválido.` })
.uuid(`${entityName} inválido.`);
/**
* Decimal string schema - parses string with comma/period to number
*/
export const decimalSchema = z
.string()
.trim()
.transform((value) => value.replace(/\s/g, "").replace(",", "."))
.refine(
(value) => !Number.isNaN(Number.parseFloat(value)),
"Informe um valor numérico válido."
)
.transform((value) => Number.parseFloat(value));
.string()
.trim()
.transform((value) => value.replace(/\s/g, "").replace(",", "."))
.refine(
(value) => !Number.isNaN(Number.parseFloat(value)),
"Informe um valor numérico válido.",
)
.transform((value) => Number.parseFloat(value));
/**
* Optional/nullable decimal string schema
*/
export const optionalDecimalSchema = z
.string()
.trim()
.optional()
.transform((value) =>
value && value.length > 0 ? value.replace(",", ".") : null
)
.refine(
(value) => value === null || !Number.isNaN(Number.parseFloat(value)),
"Informe um valor numérico válido."
)
.transform((value) => (value === null ? null : Number.parseFloat(value)));
.string()
.trim()
.optional()
.transform((value) =>
value && value.length > 0 ? value.replace(",", ".") : null,
)
.refine(
(value) => value === null || !Number.isNaN(Number.parseFloat(value)),
"Informe um valor numérico válido.",
)
.transform((value) => (value === null ? null : Number.parseFloat(value)));
/**
* Day of month schema (1-31)
*/
export const dayOfMonthSchema = z
.string({ message: "Informe o dia." })
.trim()
.min(1, "Informe o dia.")
.refine((value) => {
const parsed = Number.parseInt(value, 10);
return !Number.isNaN(parsed) && parsed >= 1 && parsed <= 31;
}, "Informe um dia entre 1 e 31.");
.string({ message: "Informe o dia." })
.trim()
.min(1, "Informe o dia.")
.refine((value) => {
const parsed = Number.parseInt(value, 10);
return !Number.isNaN(parsed) && parsed >= 1 && parsed <= 31;
}, "Informe um dia entre 1 e 31.");
/**
* Period schema (YYYY-MM format)
*/
export const periodSchema = z
.string({ message: "Informe o período." })
.trim()
.regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Período inválido.");
.string({ message: "Informe o período." })
.trim()
.regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Período inválido.");
/**
* Optional period schema
*/
export const optionalPeriodSchema = z
.string()
.trim()
.regex(/^(\d{4})-(\d{2})$/, {
message: "Selecione um período válido.",
})
.optional();
.string()
.trim()
.regex(/^(\d{4})-(\d{2})$/, {
message: "Selecione um período válido.",
})
.optional();
/**
* Date string schema
*/
export const dateStringSchema = z
.string({ message: "Informe a data." })
.trim()
.refine((value) => !Number.isNaN(new Date(value).getTime()), {
message: "Data inválida.",
});
.string({ message: "Informe a data." })
.trim()
.refine((value) => !Number.isNaN(new Date(value).getTime()), {
message: "Data inválida.",
});
/**
* Optional date string schema
*/
export const optionalDateStringSchema = z
.string()
.trim()
.refine((value) => !value || !Number.isNaN(new Date(value).getTime()), {
message: "Informe uma data válida.",
})
.optional();
.string()
.trim()
.refine((value) => !value || !Number.isNaN(new Date(value).getTime()), {
message: "Informe uma data válida.",
})
.optional();
/**
* Note/observation schema (max 500 chars, trimmed, nullable)
*/
export const noteSchema = z
.string()
.trim()
.max(500, "A anotação deve ter no máximo 500 caracteres.")
.optional()
.transform((value) => (value && value.length > 0 ? value : null));
.string()
.trim()
.max(500, "A anotação deve ter no máximo 500 caracteres.")
.optional()
.transform((value) => (value && value.length > 0 ? value : null));
/**
* Optional string that becomes null if empty
*/
export const optionalStringToNull = z
.string()
.trim()
.optional()
.transform((value) => (value && value.length > 0 ? value : null));
.string()
.trim()
.optional()
.transform((value) => (value && value.length > 0 ? value : null));
/**
* Required non-empty string schema
*/
export const requiredStringSchema = (fieldName: string) =>
z
.string({ message: `Informe ${fieldName}.` })
.trim()
.min(1, `Informe ${fieldName}.`);
z
.string({ message: `Informe ${fieldName}.` })
.trim()
.min(1, `Informe ${fieldName}.`);
/**
* Amount schema with minimum value validation
*/
export const amountSchema = z.coerce
.number({ message: "Informe o valor." })
.min(0, "Informe um valor maior ou igual a zero.");
.number({ message: "Informe o valor." })
.min(0, "Informe um valor maior ou igual a zero.");
/**
* Positive amount schema
*/
export const positiveAmountSchema = z.coerce
.number({ message: "Informe o valor." })
.positive("Informe um valor maior que zero.");
.number({ message: "Informe o valor." })
.positive("Informe um valor maior que zero.");

View File

@@ -5,19 +5,19 @@
import { z } from "zod";
export const inboxItemSchema = z.object({
sourceApp: z.string().min(1, "sourceApp é obrigatório"),
sourceAppName: z.string().optional(),
originalTitle: z.string().optional(),
originalText: z.string().min(1, "originalText é obrigatório"),
notificationTimestamp: z.string().transform((val) => new Date(val)),
parsedName: z.string().optional(),
parsedAmount: z.coerce.number().optional(),
parsedTransactionType: z.enum(["Despesa", "Receita"]).optional(),
clientId: z.string().optional(), // ID local do app para rastreamento
sourceApp: z.string().min(1, "sourceApp é obrigatório"),
sourceAppName: z.string().optional(),
originalTitle: z.string().optional(),
originalText: z.string().min(1, "originalText é obrigatório"),
notificationTimestamp: z.string().transform((val) => new Date(val)),
parsedName: z.string().optional(),
parsedAmount: z.coerce.number().optional(),
parsedTransactionType: z.enum(["Despesa", "Receita"]).optional(),
clientId: z.string().optional(), // ID local do app para rastreamento
});
export const inboxBatchSchema = z.object({
items: z.array(inboxItemSchema).min(1).max(50),
items: z.array(inboxItemSchema).min(1).max(50),
});
export type InboxItemInput = z.infer<typeof inboxItemSchema>;

View File

@@ -4,30 +4,30 @@ import { z } from "zod";
* Categorias de insights
*/
export const INSIGHT_CATEGORIES = {
behaviors: {
id: "behaviors",
title: "Comportamentos Observados",
icon: "RiEyeLine",
color: "blue",
},
triggers: {
id: "triggers",
title: "Gatilhos de Consumo",
icon: "RiFlashlightLine",
color: "amber",
},
recommendations: {
id: "recommendations",
title: "Recomendações Práticas",
icon: "RiLightbulbLine",
color: "green",
},
improvements: {
id: "improvements",
title: "Melhorias Sugeridas",
icon: "RiRocketLine",
color: "purple",
},
behaviors: {
id: "behaviors",
title: "Comportamentos Observados",
icon: "RiEyeLine",
color: "blue",
},
triggers: {
id: "triggers",
title: "Gatilhos de Consumo",
icon: "RiFlashlightLine",
color: "amber",
},
recommendations: {
id: "recommendations",
title: "Recomendações Práticas",
icon: "RiLightbulbLine",
color: "green",
},
improvements: {
id: "improvements",
title: "Melhorias Sugeridas",
icon: "RiRocketLine",
color: "purple",
},
} as const;
export type InsightCategoryId = keyof typeof INSIGHT_CATEGORIES;
@@ -36,29 +36,29 @@ export type InsightCategoryId = keyof typeof INSIGHT_CATEGORIES;
* Schema para item individual de insight
*/
export const InsightItemSchema = z.object({
text: z.string().min(1),
text: z.string().min(1),
});
/**
* Schema para categoria de insights
*/
export const InsightCategorySchema = z.object({
category: z.enum([
"behaviors",
"triggers",
"recommendations",
"improvements",
]),
items: z.array(InsightItemSchema).min(1).max(6),
category: z.enum([
"behaviors",
"triggers",
"recommendations",
"improvements",
]),
items: z.array(InsightItemSchema).min(1).max(6),
});
/**
* Schema for complete insights response from AI
*/
export const InsightsResponseSchema = z.object({
month: z.string().regex(/^\d{4}-\d{2}$/), // YYYY-MM
generatedAt: z.string(), // ISO datetime
categories: z.array(InsightCategorySchema).length(4),
month: z.string().regex(/^\d{4}-\d{2}$/), // YYYY-MM
generatedAt: z.string(), // ISO datetime
categories: z.array(InsightCategorySchema).length(4),
});
/**