alcantarm commited on
Commit
ceebd34
·
verified ·
1 Parent(s): 2b4ff97

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +182 -180
script.js CHANGED
@@ -1,189 +1,191 @@
1
  window.addEventListener('DOMContentLoaded', () => {
2
- const CLP = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', maximumFractionDigits: 0 });
3
-
4
- // Inputs y elementos
5
- const input = document.getElementById('invoice-input');
6
- const budgetInput = document.getElementById('budget-input');
7
- const dropzone = document.getElementById('dropzone');
8
- const budgetDropzone = document.getElementById('budget-dropzone');
9
- const tbody = document.getElementById('tbody');
10
- const budgetTbody = document.getElementById('budget-tbody-right');
11
- const total = document.getElementById('total');
12
- const errorEl = document.getElementById('error');
13
- const budgetError = document.getElementById('budget-error');
14
- const processBtn = document.getElementById('process-btn');
15
- const resetBtn = document.getElementById('reset-btn');
16
- const summary = document.getElementById('summary');
17
- const recordsTbody = document.getElementById('records-tbody');
18
-
19
- // Datos
20
- const docs = [];
21
- const records = [];
22
- let budgetFile = null;
23
-
24
- // Funciones auxiliares
25
- const fmtDate = iso => new Date(iso).toLocaleString();
26
- const randomAmount = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
27
- const randomFolio = () => 'F' + Math.floor(100000 + Math.random() * 900000);
28
-
29
- const parseCSV = content => {
30
- const lines = content.split('\n');
31
- for (const line of lines) {
32
- const matches = line.match(/(\d{1,3}(?:\.\d{3})*|\d+)/g);
33
- if (matches && matches.length > 0) {
34
- const amount = parseInt(matches[0].replace(/\./g, ''));
35
- if (!isNaN(amount) && amount > 1000) return amount;
36
- }
37
- }
38
- return null;
39
- };
40
-
41
- // Renderizar tablas
42
- const renderDocs = () => {
43
- total.textContent = `${docs.length} en total`;
44
- tbody.innerHTML = '';
45
- if (!docs.length) {
46
- tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px;">Aún no hay documentos. Sube un PDF o imagen para comenzar.</td></tr>`;
47
- } else {
48
- docs.forEach((d, i) => {
49
- const monto = CLP.format(d.amount);
50
- const tr = document.createElement('tr');
51
- tr.innerHTML = `<td>${i+1}</td><td>${d.folio}</td><td>${d.name}</td><td>${fmtDate(d.loadedAt)}</td><td>${monto}</td>`;
52
- tbody.appendChild(tr);
53
- });
54
- }
55
- };
56
-
57
- const renderBudget = () => {
58
- budgetTbody.innerHTML = '';
59
- if (!budgetFile) {
60
- budgetTbody.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--muted);padding:16px;">Aún no se ha cargado el presupuesto.</td></tr>`;
61
- } else {
62
- const tr = document.createElement('tr');
63
- tr.innerHTML = `<td>${budgetFile.name}</td><td>${fmtDate(budgetFile.loadedAt)}</td><td>${CLP.format(budgetFile.amount)}</td>`;
64
- budgetTbody.appendChild(tr);
65
- }
66
- };
67
-
68
- const updateSummary = () => {
69
- if (!budgetFile || !docs.length) {
70
- summary.innerHTML = '';
71
- return;
72
- }
73
- const totalDocs = docs.reduce((sum, doc) => sum + (doc.amount || 0), 0);
74
- const variation = budgetFile.amount - totalDocs;
75
- const percentVariation = budgetFile.amount > 0 ? (variation / budgetFile.amount * 100) : 0;
76
- summary.innerHTML = `<strong>Total documentos:</strong> ${CLP.format(totalDocs)}<br>
77
- <strong>Presupuesto:</strong> ${CLP.format(budgetFile.amount)}<br>
78
- <strong>Variación:</strong> ${CLP.format(variation)} (${percentVariation.toFixed(1)}%)`;
79
- };
80
-
81
- const renderRecords = () => {
82
- recordsTbody.innerHTML = '';
83
- if (!records.length) {
84
- recordsTbody.innerHTML = `<tr><td class="empty" colspan="5">No hay registros todavía. Guarda un proceso para verlo aquí.</td></tr>`;
85
- } else {
86
- records.forEach((record, i) => {
87
- const tr = document.createElement('tr');
88
- const variationText = record.diff >= 0 ? `+${CLP.format(record.diff)}` : CLP.format(record.diff);
89
- tr.innerHTML = `<td>${record.docsCount}</td><td>${CLP.format(record.docsSum)}</td><td>${CLP.format(record.budget)}</td><td>${variationText}</td>
90
- <td><button class="delete-btn" onclick="deleteRecord(${i})">Eliminar</button></td>`;
91
- recordsTbody.appendChild(tr);
92
- });
93
- }
94
- };
95
-
96
- window.deleteRecord = idx => {
97
- if (!isNaN(idx) && idx >= 0 && idx < records.length) {
98
- records.splice(idx, 1);
99
- renderRecords();
100
- }
101
- };
102
 
103
- // Subir archivos
104
- const addFiles = files => {
105
- const nowIso = new Date().toISOString();
106
- errorEl.textContent = '';
107
- Array.from(files).forEach(f => {
108
- if (f) {
109
- docs.unshift({ name: f.name, loadedAt: nowIso, file: f, amount: randomAmount(10000, 250000), folio: randomFolio() });
110
- } else {
111
- errorEl.textContent = 'Solo se permiten archivos PDF o imágenes.';
112
- }
113
- });
114
- renderDocs();
115
- updateSummary();
116
- };
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- const setBudgetFromFile = f => {
119
- if (!f) return;
120
- if (!f.name.toLowerCase().endsWith('.csv')) {
121
- budgetError.textContent = 'Solo se permite un archivo CSV.';
122
- return;
123
- }
124
- budgetError.textContent = '';
125
- const reader = new FileReader();
126
- reader.onload = e => {
127
- const content = e.target.result;
128
- let amount = parseCSV(content);
129
- if (amount === null) amount = randomAmount(200000, 2000000);
130
- budgetFile = { name: f.name, loadedAt: new Date().toISOString(), amount };
131
- renderBudget();
132
- updateSummary();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  };
134
- reader.readAsText(f);
135
- };
136
 
137
- // Eventos
138
- input.addEventListener('change', () => { addFiles(input.files); input.value = ''; });
139
- budgetInput.addEventListener('change', () => { if (budgetInput.files.length > 0) { setBudgetFromFile(budgetInput.files[0]); budgetInput.value = ''; } });
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- // Drag & drop facturas
142
- ['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag'); }));
143
- ['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, e => {
144
- e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag');
145
- if (e.type === 'drop') addFiles(e.dataTransfer.files);
146
- }));
 
 
 
 
 
 
 
 
147
 
148
- // Drag & drop presupuesto
149
- ['dragenter', 'dragover'].forEach(evt => budgetDropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); budgetDropzone.classList.add('drag'); }));
150
- ['dragleave', 'drop'].forEach(evt => budgetDropzone.addEventListener(evt, e => {
151
- e.preventDefault(); e.stopPropagation(); budgetDropzone.classList.remove('drag');
152
- if (e.type === 'drop' && e.dataTransfer.files.length > 0) setBudgetFromFile(e.dataTransfer.files[0]);
153
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- processBtn.addEventListener('click', async () => {
156
- if (!budgetFile || !docs.length) {
157
- alert('Carga al menos un PDF/imagen y un CSV de presupuesto.');
158
- return;
159
- }
160
- processBtn.textContent = 'Procesando...'; processBtn.disabled = true;
161
- await new Promise(r => setTimeout(r, 800));
162
- const totalDocs = docs.reduce((sum, d) => sum + d.amount, 0);
163
- records.push({ docsCount: docs.length, docsSum: totalDocs, budget: budgetFile.amount, diff: budgetFile.amount - totalDocs });
164
  renderRecords();
165
- processBtn.textContent = 'Procesar'; processBtn.disabled = false;
166
- });
167
-
168
- resetBtn.addEventListener('click', () => {
169
- if (confirm('¿Estás seguro de que quieres reiniciar todo?')) {
170
- docs.length = 0;
171
- records.length = 0;
172
- budgetFile = null;
173
- errorEl.textContent = '';
174
- budgetError.textContent = '';
175
- input.value = '';
176
- budgetInput.value = '';
177
- renderDocs();
178
- renderBudget();
179
- renderRecords();
180
- updateSummary();
181
- summary.innerHTML = '';
182
- }
183
- });
184
-
185
- // Inicial
186
- renderDocs();
187
- renderBudget();
188
- renderRecords();
189
- });
 
1
  window.addEventListener('DOMContentLoaded', () => {
2
+ const CLP = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', maximumFractionDigits: 0 });
3
+
4
+ // --- Selección de Elementos del DOM ---
5
+ const invoiceInput = document.getElementById('invoice-input');
6
+ const budgetInput = document.getElementById('budget-input');
7
+ const dropzone = document.getElementById('dropzone');
8
+ const budgetDropzone = document.getElementById('budget-dropzone');
9
+ const tbody = document.getElementById('tbody');
10
+ const totalSpan = document.getElementById('total');
11
+ const errorEl = document.getElementById('error');
12
+ const budgetErrorEl = document.getElementById('budget-error');
13
+ const processBtn = document.getElementById('process-btn');
14
+ const resetBtn = document.getElementById('reset-btn');
15
+ const recordsTbody = document.getElementById('records-tbody');
16
+
17
+ // --- NUEVO: Elementos para resumen y barra de progreso ---
18
+ const summaryBudgetEl = document.getElementById('summary-budget');
19
+ const summarySpentEl = document.getElementById('summary-spent');
20
+ const summaryVariationEl = document.getElementById('summary-variation');
21
+ const budgetProgressBar = document.getElementById('budget-progress-bar');
22
+
23
+ // --- Almacenamiento de Datos en Memoria ---
24
+ const docs = [];
25
+ const records = [];
26
+ let budgetFile = null;
27
+
28
+ // --- Funciones de Simulación y Ayuda ---
29
+ const formatDate = isoString => new Date(isoString).toLocaleDateString('es-CL');
30
+ const randomAmount = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
31
+ const randomFolio = () => 'F' + Math.floor(100000 + Math.random() * 900000);
32
+
33
+ const parseCSVForAmount = content => {
34
+ const lines = content.split('\n');
35
+ for (const line of lines) {
36
+ const matches = line.match(/(\d{1,3}(?:\.\d{3})*|\d+)/g);
37
+ if (matches && matches.length > 0) {
38
+ const amount = parseInt(matches[0].replace(/\./g, ''));
39
+ if (!isNaN(amount) && amount > 1000) return amount;
40
+ }
41
+ }
42
+ return null;
43
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ // --- Funciones para Actualizar la Interfaz (Render) ---
46
+ const renderDocs = () => {
47
+ totalSpan.textContent = `${docs.length} en total`;
48
+ tbody.innerHTML = '';
49
+ if (docs.length === 0) {
50
+ tbody.innerHTML = `<tr><td colspan="5" class="empty">Aún no hay documentos.</td></tr>`;
51
+ } else {
52
+ docs.forEach((doc, index) => {
53
+ const tr = document.createElement('tr');
54
+ // --- NUEVO: Lógica de previsualización de imágenes ---
55
+ let documentCell = doc.name;
56
+ if (doc.file && doc.file.type.startsWith('image/')) {
57
+ const imageURL = URL.createObjectURL(doc.file);
58
+ documentCell = `<img src="${imageURL}" alt="${doc.name}" class="thumbnail" />`;
59
+ }
60
+ tr.innerHTML = `
61
+ <td>${index + 1}</td>
62
+ <td>${doc.folio}</td>
63
+ <td>${documentCell}</td>
64
+ <td>${formatDate(doc.loadedAt)}</td>
65
+ <td>${CLP.format(doc.amount)}</td>`;
66
+ tbody.appendChild(tr);
67
+ });
68
+ }
69
+ };
70
 
71
+ const updateSummary = () => {
72
+ const totalDocsAmount = docs.reduce((sum, doc) => sum + (doc.amount || 0), 0);
73
+ const budgetAmount = budgetFile ? budgetFile.amount : 0;
74
+ const variation = budgetAmount - totalDocsAmount;
75
+
76
+ // --- CAMBIO: Actualizar las nuevas tarjetas de resumen ---
77
+ summaryBudgetEl.textContent = CLP.format(budgetAmount);
78
+ summarySpentEl.textContent = CLP.format(totalDocsAmount);
79
+ summaryVariationEl.textContent = CLP.format(variation);
80
+
81
+ // Colores para la variación
82
+ summaryVariationEl.style.color = variation < 0 ? 'var(--red)' : 'var(--green)';
83
+
84
+ // --- NUEVO: Lógica para la barra de progreso ---
85
+ if (budgetAmount > 0) {
86
+ const usagePercent = Math.max(0, (totalDocsAmount / budgetAmount) * 100);
87
+ budgetProgressBar.style.width = `${Math.min(usagePercent, 100)}%`; // No pasar del 100% de ancho
88
+
89
+ if (usagePercent > 100) {
90
+ budgetProgressBar.style.backgroundColor = 'var(--red)';
91
+ } else if (usagePercent > 80) {
92
+ budgetProgressBar.style.backgroundColor = 'var(--yellow)';
93
+ } else {
94
+ budgetProgressBar.style.backgroundColor = 'var(--green)';
95
+ }
96
+ } else {
97
+ budgetProgressBar.style.width = '0%';
98
+ }
99
+ };
100
+
101
+ const renderRecords = () => {
102
+ recordsTbody.innerHTML = '';
103
+ if (records.length === 0) {
104
+ recordsTbody.innerHTML = `<tr><td class="empty" colspan="5">No hay registros todavía.</td></tr>`;
105
+ } else {
106
+ records.forEach((record, index) => {
107
+ const tr = document.createElement('tr');
108
+ const variationColor = record.diff < 0 ? 'var(--red)' : 'var(--green)';
109
+ const sign = record.diff >= 0 ? '+' : '';
110
+ tr.innerHTML = `
111
+ <td>${record.docsCount}</td>
112
+ <td>${CLP.format(record.docsSum)}</td>
113
+ <td>${CLP.format(record.budget)}</td>
114
+ <td style="color: ${variationColor}; font-weight: bold;">${sign}${CLP.format(record.diff)}</td>
115
+ <td><button class="delete-btn" data-index="${index}">Eliminar</button></td>`;
116
+ recordsTbody.appendChild(tr);
117
+ });
118
+ }
119
  };
 
 
120
 
121
+ const addInvoiceFiles = files => {
122
+ errorEl.textContent = '';
123
+ Array.from(files).forEach(file => {
124
+ docs.unshift({
125
+ name: file.name,
126
+ loadedAt: new Date().toISOString(),
127
+ file: file,
128
+ amount: randomAmount(10000, 250000),
129
+ folio: randomFolio()
130
+ });
131
+ });
132
+ renderDocs();
133
+ updateSummary();
134
+ };
135
 
136
+ const setBudgetFile = file => {
137
+ if (!file || !file.name.toLowerCase().endsWith('.csv')) {
138
+ budgetErrorEl.textContent = 'Solo se permite un archivo CSV.'; return;
139
+ }
140
+ budgetErrorEl.textContent = '';
141
+ const reader = new FileReader();
142
+ reader.onload = e => {
143
+ const content = e.target.result;
144
+ let amount = parseCSVForAmount(content) || randomAmount(200000, 2000000);
145
+ budgetFile = { name: file.name, loadedAt: new Date().toISOString(), amount: amount, file: file };
146
+ updateSummary();
147
+ };
148
+ reader.readAsText(file);
149
+ };
150
 
151
+ // --- Asignación de Eventos (sin cambios) ---
152
+ invoiceInput.addEventListener('change', () => { addInvoiceFiles(invoiceInput.files); invoiceInput.value = ''; });
153
+ budgetInput.addEventListener('change', () => { if (budgetInput.files.length > 0) setBudgetFile(budgetInput.files[0]); budgetInput.value = ''; });
154
+ ['dragenter', 'dragover'].forEach(e => dropzone.addEventListener(e, evt => { evt.preventDefault(); evt.stopPropagation(); dropzone.classList.add('drag'); }));
155
+ ['dragleave', 'drop'].forEach(e => dropzone.addEventListener(e, evt => {
156
+ evt.preventDefault(); evt.stopPropagation(); dropzone.classList.remove('drag');
157
+ if (e === 'drop') addInvoiceFiles(evt.dataTransfer.files);
158
+ }));
159
+ ['dragenter', 'dragover'].forEach(e => budgetDropzone.addEventListener(e, evt => { evt.preventDefault(); evt.stopPropagation(); budgetDropzone.classList.add('drag'); }));
160
+ ['dragleave', 'drop'].forEach(e => budgetDropzone.addEventListener(e, evt => {
161
+ evt.preventDefault(); evt.stopPropagation(); budgetDropzone.classList.remove('drag');
162
+ if (e === 'drop' && evt.dataTransfer.files.length > 0) setBudgetFile(evt.dataTransfer.files[0]);
163
+ }));
164
+ processBtn.addEventListener('click', async () => {
165
+ if (!budgetFile || docs.length === 0) { alert('Carga un presupuesto y al menos un documento.'); return; }
166
+ const totalDocs = docs.reduce((sum, d) => sum + d.amount, 0);
167
+ records.push({ docsCount: docs.length, docsSum: totalDocs, budget: budgetFile.amount, diff: budgetFile.amount - totalDocs });
168
+ renderRecords();
169
+ });
170
+ resetBtn.addEventListener('click', () => {
171
+ if (confirm('¿Estás seguro? Se borrarán todos los datos de la simulación.')) {
172
+ docs.length = 0;
173
+ records.length = 0;
174
+ budgetFile = null;
175
+ renderDocs();
176
+ updateSummary();
177
+ renderRecords();
178
+ }
179
+ });
180
+ recordsTbody.addEventListener('click', e => {
181
+ if (e.target.classList.contains('delete-btn')) {
182
+ const index = parseInt(e.target.dataset.index, 10);
183
+ if (!isNaN(index)) { records.splice(index, 1); renderRecords(); }
184
+ }
185
+ });
186
 
187
+ // --- Estado Inicial ---
188
+ updateSummary();
189
+ renderDocs();
 
 
 
 
 
 
190
  renderRecords();
191
+ });