100 lines
3.8 KiB
JavaScript
100 lines
3.8 KiB
JavaScript
// BaseFileDriver.mjs
|
|
import { DeviceInterface } from './DeviceInterface.mjs';
|
|
import { fmtHMSUTC, fmtHM } from '../utils/dates.mjs';
|
|
import * as intervalsCross from '../strategies/intervals/cross-day.mjs';
|
|
import * as intervalsSame from '../strategies/intervals/same-day.mjs';
|
|
|
|
/**
|
|
* Template Method para drivers basados en archivos .txt
|
|
* Define el pipeline y delega el parseo de línea en this.parserStrategy.parseLine
|
|
*/
|
|
export class BaseFileDriver extends DeviceInterface {
|
|
constructor(opts = {}) {
|
|
super(opts);
|
|
if (!this.parserStrategy || typeof this.parserStrategy.parseLine !== 'function') {
|
|
throw new Error('BaseFileDriver requiere parserStrategy.parseLine(line)');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} text contenido completo del .txt en UTF-8
|
|
*/
|
|
async processFile(text) {
|
|
if (!text || typeof text !== 'string') {
|
|
this.setStatus('Elegí un .txt válido');
|
|
return { parsedRows: [], pairs: [], payloadDB: [], missing_docs: [], error: 'Archivo vacío o inválido' };
|
|
}
|
|
|
|
this.setStatus('Leyendo archivo…');
|
|
|
|
// 1) Parseo línea a línea (Strategy)
|
|
const lines = text.split(/\n/);
|
|
const parsedRows = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const r = this.parserStrategy.parseLine(lines[i]);
|
|
if (r) parsedRows.push(r);
|
|
if ((i & 511) === 0) this.emit('progress', { at: i, total: lines.length });
|
|
}
|
|
|
|
// 2) Resolver nombres por documento (inyectado)
|
|
const uniqueDocs = [...new Set(parsedRows.map(r => r.doc))];
|
|
this.setStatus(`Leyendo archivo… | consultando ${uniqueDocs.length} documentos…`);
|
|
const map = await this._safeNamesResolver(uniqueDocs);
|
|
|
|
// 3) Detectar documentos faltantes
|
|
const missing_docs = uniqueDocs.filter(d => {
|
|
const hit = map?.[d];
|
|
if (!hit) return true;
|
|
if (typeof hit.found === 'boolean') return !hit.found;
|
|
return !(hit?.nombre || '').trim() && !(hit?.apellido || '').trim();
|
|
});
|
|
|
|
if (missing_docs.length) {
|
|
this.setStatus('Hay documentos sin usuario. Corrigí y volvé a procesar.');
|
|
return { parsedRows, pairs: [], payloadDB: [], missing_docs,
|
|
error: `No se encontraron ${missing_docs.length} documento(s) en la base` };
|
|
}
|
|
|
|
// 4) Enriquecer nombre desde DB
|
|
parsedRows.forEach(r => {
|
|
const hit = map?.[r.doc];
|
|
if (hit && (hit.nombre || hit.apellido)) r.name = `${hit.nombre || ''} ${hit.apellido || ''}`.trim();
|
|
});
|
|
|
|
// 5) Construcción de intervalos (Strategy)
|
|
const pairs = (this.intervalBuilder === 'sameDay')
|
|
? intervalsSame.buildIntervals(parsedRows)
|
|
: intervalsCross.buildIntervalsCrossDay(parsedRows);
|
|
|
|
// 6) Payload "raw" para DB
|
|
const payloadDB = parsedRows.map(r => ({
|
|
doc: r.doc, isoDate: r.isoDate, time: r.time, mode: r.mode || null
|
|
}));
|
|
|
|
this.setStatus(`${parsedRows.length} registros · ${pairs.length} intervalos`);
|
|
return { parsedRows, pairs, payloadDB, missing_docs: [] };
|
|
}
|
|
|
|
exportCSV(pairs) {
|
|
const list = Array.isArray(pairs) ? pairs : [];
|
|
if (!list.length) return '';
|
|
const head = ['documento','nombre','fecha','desde','hasta','duracion_hhmm','duracion_min','obs'];
|
|
const rows = list.map(p => {
|
|
const iso = p.isoDate || p.fecha || '';
|
|
const desdeStr = (p.desde_ms!=null) ? fmtHMSUTC(p.desde_ms) : '';
|
|
const hastaStr = (p.hasta_ms!=null) ? fmtHMSUTC(p.hasta_ms) : '';
|
|
const durStr = (p.durMins!=null) ? fmtHM(p.durMins) : '';
|
|
const durMin = (p.durMins!=null) ? Math.round(p.durMins) : '';
|
|
return [
|
|
p.doc, p.name || '', iso, desdeStr, hastaStr, durStr, durMin, p.obs || ''
|
|
].map(v => `"${String(v).replaceAll('"','""')}"`).join(',');
|
|
});
|
|
return head.join(',') + '\n' + rows.join('\n');
|
|
}
|
|
|
|
async _safeNamesResolver(docs) {
|
|
try { return await this.namesResolver(docs); }
|
|
catch { return {}; }
|
|
}
|
|
}
|