// 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 {}; } } }