mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 14:08:47 +02:00
Add manual pension data entry and fix chart to use real historical data
All checks were successful
Deploy to VPS / deploy (push) Successful in 23s
All checks were successful
Deploy to VPS / deploy (push) Successful in 23s
- Add paste-and-preview modal for entering pension fund balances from bank website - Backend upsert logic so n8n PDF uploads overwrite manual entries - Chart now shows actual snapshot data with dynamic month labels - New POST /pensions/manual endpoint for JSON-based fund entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
133
frontend/src/lib/parsePensionPaste.ts
Normal file
133
frontend/src/lib/parsePensionPaste.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
export interface PensionParsedEntry {
|
||||
fund: string;
|
||||
period_start: string; // YYYY-MM-DD
|
||||
period_end: string;
|
||||
saldo_anterior: number;
|
||||
aportes: number;
|
||||
rendimientos: number;
|
||||
retiros: number;
|
||||
traslados: number;
|
||||
comision: number;
|
||||
correccion: number;
|
||||
bonificacion: number;
|
||||
saldo_final: number;
|
||||
}
|
||||
|
||||
function parseAmount(raw: string): number {
|
||||
// "¢ 18,684,764.98" or "¢ -552,213.24" or just "18,684,764.98"
|
||||
const cleaned = raw.replace(/[¢\s]/g, '').replace(/,/g, '');
|
||||
const num = parseFloat(cleaned);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
function parseDateDMY(raw: string): string {
|
||||
// "01/03/2026" → "2026-03-01"
|
||||
const m = raw.match(/(\d{2})\/(\d{2})\/(\d{4})/);
|
||||
if (!m) return '';
|
||||
return `${m[3]}-${m[2]}-${m[1]}`;
|
||||
}
|
||||
|
||||
function extractAmounts(line: string): number[] {
|
||||
const matches = line.match(/¢\s*-?[\d,.]+/g);
|
||||
if (!matches) return [];
|
||||
return matches.map(parseAmount);
|
||||
}
|
||||
|
||||
interface BlockResult {
|
||||
funds: string[];
|
||||
fields: Record<string, number[]>;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
}
|
||||
|
||||
function parseBlock(lines: string[]): BlockResult | null {
|
||||
const result: BlockResult = {
|
||||
funds: [],
|
||||
fields: {},
|
||||
period_start: '',
|
||||
period_end: '',
|
||||
};
|
||||
|
||||
// Detect fund columns from header
|
||||
const headerLine = lines.find((l) => /resumen del per[ií]odo/i.test(l));
|
||||
if (!headerLine) return null;
|
||||
|
||||
if (/\bROP\b/i.test(headerLine) && /\bFCL\b/i.test(headerLine)) {
|
||||
result.funds = ['ROP', 'FCL'];
|
||||
} else if (/voluntario/i.test(headerLine) || /\bVOL\b/i.test(headerLine)) {
|
||||
result.funds = ['VOL'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldMap: [RegExp, string][] = [
|
||||
[/saldo\s*anterior/i, 'saldo_anterior'],
|
||||
[/aportes/i, 'aportes'],
|
||||
[/rendimientos/i, 'rendimientos'],
|
||||
[/retiros/i, 'retiros'],
|
||||
[/traslados/i, 'traslados'],
|
||||
[/comisi[oó]n/i, 'comision'],
|
||||
[/bonificaci[oó]n/i, 'bonificacion'],
|
||||
[/saldo\s*actual/i, 'saldo_final'],
|
||||
];
|
||||
|
||||
for (const line of lines) {
|
||||
for (const [regex, key] of fieldMap) {
|
||||
if (regex.test(line)) {
|
||||
const amounts = extractAmounts(line);
|
||||
if (amounts.length > 0) {
|
||||
result.fields[key] = amounts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Period
|
||||
const periodMatch = line.match(/del\s+(\d{2}\/\d{2}\/\d{4})\s+al\s+(\d{2}\/\d{2}\/\d{4})/i);
|
||||
if (periodMatch) {
|
||||
result.period_start = parseDateDMY(periodMatch[1]);
|
||||
result.period_end = parseDateDMY(periodMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parsePensionPaste(text: string): PensionParsedEntry[] {
|
||||
// Split into blocks by "---" or multiple blank lines
|
||||
const blocks = text.split(/(?:^|\n)-{3,}(?:\n|$)|\n{3,}/);
|
||||
const entries: PensionParsedEntry[] = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const lines = block.split('\n').filter((l) => l.trim());
|
||||
if (lines.length < 3) continue;
|
||||
|
||||
const parsed = parseBlock(lines);
|
||||
if (!parsed || !parsed.period_start || !parsed.period_end) continue;
|
||||
|
||||
for (let i = 0; i < parsed.funds.length; i++) {
|
||||
const fund = parsed.funds[i];
|
||||
const get = (key: string): number => {
|
||||
const vals = parsed.fields[key];
|
||||
if (!vals) return 0;
|
||||
return vals[i] ?? vals[0] ?? 0;
|
||||
};
|
||||
|
||||
entries.push({
|
||||
fund,
|
||||
period_start: parsed.period_start,
|
||||
period_end: parsed.period_end,
|
||||
saldo_anterior: get('saldo_anterior'),
|
||||
aportes: get('aportes'),
|
||||
rendimientos: get('rendimientos'),
|
||||
retiros: get('retiros'),
|
||||
traslados: get('traslados'),
|
||||
comision: get('comision'),
|
||||
correccion: 0,
|
||||
bonificacion: get('bonificacion'),
|
||||
saldo_final: get('saldo_final'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
Reference in New Issue
Block a user