Files
openmonetis/src/shared/lib/import/xls-parser.ts
2026-05-21 13:46:42 +00:00

158 lines
4.2 KiB
TypeScript

import ExcelJS from "exceljs";
import type {
ImportedTransaction,
ImportStatement,
} from "@/shared/lib/import/types";
/**
* Converte serial number do Excel (1900 date system) para ano/mês/dia.
* Excel trata 1900 como bissexto (serial 60 = 29/02/1900 inexistente).
*/
function excelSerialToDate(
serial: number,
): { y: number; m: number; d: number } | null {
if (serial < 1) return null;
let adjusted = serial;
if (serial > 60) adjusted--;
const baseDate = Date.UTC(1899, 11, 31);
const date = new Date(baseDate + adjusted * 86400000);
return {
y: date.getUTCFullYear(),
m: date.getUTCMonth() + 1,
d: date.getUTCDate(),
};
}
function parseDateValue(value: unknown): string | null {
if (value == null || value === "") return null;
// Excel date serial number
if (typeof value === "number") {
const date = excelSerialToDate(value);
if (!date) return null;
const y = date.y;
const m = String(date.m).padStart(2, "0");
const d = String(date.d).padStart(2, "0");
return `${y}-${m}-${d}`;
}
// ExcelJS pode retornar Date objects
if (value instanceof Date) {
const y = value.getUTCFullYear();
const m = String(value.getUTCMonth() + 1).padStart(2, "0");
const d = String(value.getUTCDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
const str = String(value).trim();
// DD/MM/YYYY
const dmyMatch = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (dmyMatch) {
return `${dmyMatch[3]}-${dmyMatch[2].padStart(2, "0")}-${dmyMatch[1].padStart(2, "0")}`;
}
// YYYY-MM-DD
const isoMatch = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) return str;
return null;
}
function parseAmountValue(value: unknown): number | null {
if (value == null || value === "") return null;
if (typeof value === "number") return Math.abs(value);
const num = Number.parseFloat(
String(value)
.replace(",", ".")
.replace(/[^\d.-]/g, ""),
);
return Number.isNaN(num) ? null : Math.abs(num);
}
export async function parseXls(buffer: ArrayBuffer): Promise<ImportStatement> {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
if (workbook.worksheets.length === 0) {
throw new Error("Arquivo sem abas.");
}
const sheet = workbook.worksheets[0];
if (!sheet || sheet.rowCount < 2) {
throw new Error(
`Planilha vazia ou sem dados (${sheet?.rowCount ?? 0} linha(s) encontrada(s)).`,
);
}
const transactions: ImportedTransaction[] = [];
sheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return; // skip header
// ExcelJS row.values é 1-indexed (values[0] é undefined)
const values = row.values as unknown[];
const date = parseDateValue(values[1]);
const description = values[2] != null ? String(values[2]).trim() : "";
const amount = parseAmountValue(values[3]);
const typeRaw =
values[4] != null ? String(values[4]).toLowerCase().trim() : "";
const transactionType = typeRaw === "receita" ? "income" : "expense";
if (!date || !description || amount === null || amount <= 0) return;
transactions.push({
externalId: null,
date,
amount,
description,
transactionType,
});
});
if (transactions.length === 0) {
throw new Error("Nenhuma transação válida encontrada na planilha.");
}
const dates = transactions.map((t) => t.date).sort();
const period = { from: dates[0], to: dates[dates.length - 1] };
return {
source: "Planilha",
accountNumber: null,
period,
isCreditCard: false,
transactions,
};
}
export async function generateXlsTemplate(): Promise<ArrayBuffer> {
const workbook = new ExcelJS.Workbook();
const ws = workbook.addWorksheet("Lançamentos");
ws.addRows([
["Data", "Descrição", "Valor", "Tipo"],
["01/03/2026", "Ingressos São Januário", 160, "despesa"],
["01/03/2026", "Salário", 3000.0, "receita"],
["01/03/2026", "Posto do Vasco da Gama", 89.9, "despesa"],
]);
ws.getColumn(1).width = 14;
ws.getColumn(2).width = 32;
ws.getColumn(3).width = 12;
ws.getColumn(4).width = 10;
// Dropdown para coluna Tipo (D2:D100)
for (let i = 2; i <= 100; i++) {
ws.getCell(`D${i}`).dataValidation = {
type: "list",
allowBlank: true,
formulae: ['"despesa,receita"'],
};
}
const buffer = await workbook.xlsx.writeBuffer();
return buffer as ArrayBuffer;
}