Excel y sesiones.json a las 00

This commit is contained in:
2026-06-05 22:56:35 -05:00
parent aa5e85ae94
commit 984f2f103d
7 changed files with 550 additions and 107 deletions
+62
View File
@@ -1816,3 +1816,65 @@ select {
padding: 8px 10px;
}
}
/* ════════════════════════════════
EXCEL/CSV — sección de asignar nombre + precio
════════════════════════════════ */
.excel-form {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 18px;
}
/* Excel/CSV — hint compacto de formato */
.excel-format-hint {
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: var(--radius-md);
padding: 10px 14px;
margin: 0 0 14px 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 12px;
font-size: 0.85rem;
line-height: 1.4;
color: var(--ink-soft);
}
.excel-format-cols {
font-family: 'SF Mono', Menlo, monospace;
font-size: 0.82rem;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 4px 10px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.excel-format-cols strong {
color: var(--violet-deep);
font-weight: 700;
font-family: inherit;
}
.excel-format-cols .sep {
color: var(--ink-mute);
font-weight: 400;
margin: 0 2px;
}
.excel-format-meta {
color: var(--ink-mute);
font-size: 0.78rem;
}
.excel-summary {
display: flex;
flex-direction: column;
gap: 10px;
padding: 18px;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: var(--radius-md);
}
+150
View File
@@ -46,6 +46,7 @@ document.addEventListener('DOMContentLoaded', async () => {
setupCategoriasModals();
setupProductosUpload();
setupProductosSearch();
setupExcelUpload();
await checkSession();
});
@@ -1327,6 +1328,155 @@ function setupProductosSearch() {
});
}
// ════════════════════════════════
// EXCEL/CSV — asignar nombre + precio por referencia
// ════════════════════════════════
function setupExcelUpload() {
const input = document.getElementById('excelArchivo');
const hint = document.getElementById('excelArchivoHint');
const form = document.getElementById('formExcelUpload');
const btn = document.getElementById('btnProcesarExcel');
if (!input || !form) return;
input.addEventListener('change', () => {
const f = input.files && input.files[0];
if (f) {
const sizeKB = (f.size / 1024).toFixed(1);
hint.textContent = `📄 ${f.name} · ${sizeKB} KB`;
} else {
hint.textContent = 'Ningún archivo seleccionado';
}
btn.disabled = !f;
});
form.addEventListener('submit', submitExcelUpload);
}
async function submitExcelUpload(e) {
e.preventDefault();
const input = document.getElementById('excelArchivo');
const btn = document.getElementById('btnProcesarExcel');
const errEl = document.getElementById('excelError');
const summ = document.getElementById('excelSummary');
const f = input.files && input.files[0];
errEl.textContent = '';
errEl.classList.remove('show');
summ.style.display = 'none';
summ.innerHTML = '';
if (!f) {
errEl.textContent = 'Selecciona un archivo';
errEl.classList.add('show');
return;
}
// Lock UI
btn.disabled = true;
btn.querySelector('.btn-text').textContent = 'Procesando…';
btn.querySelector('.btn-spinner').style.display = 'inline-block';
const fd = new FormData();
fd.append('archivo', f, f.name);
try {
const res = await fetch('/api/productos/excel', { method: 'POST', body: fd, credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Error al procesar el archivo');
renderExcelSummary(data.resumen);
// Refrescar lista de productos disponibles (para que se vean los nuevos nombres)
if (currentView === 'productos' && typeof cargarProductosDisponibles === 'function') {
cargarProductosDisponibles();
}
} catch (err) {
errEl.textContent = err.message;
errEl.classList.add('show');
} finally {
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Procesar archivo';
btn.querySelector('.btn-spinner').style.display = 'none';
}
}
function renderExcelSummary(r) {
const cont = document.getElementById('excelSummary');
cont.innerHTML = '';
if (r.actualizados > 0) {
const row = document.createElement('div');
row.className = 'upload-summary-row is-success';
const items = (r.actualizadosList || []).slice(0, 10).map(a =>
`<li><code>${escapeHtml(a.ref)}</code>${a.fila ? ` <span style="opacity:0.7">(fila ${a.fila})</span>` : ''}</li>`
).join('');
const more = r.actualizadosList && r.actualizadosList.length > 10
? `<li style="opacity:0.7">…y ${r.actualizadosList.length - 10} más</li>` : '';
row.innerHTML = `
<span class="icon">✅</span>
<div class="text">
<strong>${r.actualizados} producto(s) actualizado(s)</strong>
<ul>${items}${more}</ul>
</div>
`;
cont.appendChild(row);
}
if (r.noEncontrados > 0) {
const row = document.createElement('div');
row.className = 'upload-summary-row is-warning';
const items = (r.noEncontradosList || []).slice(0, 10).map(ref =>
`<li><code>${escapeHtml(ref)}</code> <span style="opacity:0.7">(no existe en el catálogo)</span></li>`
).join('');
const more = r.noEncontradosList && r.noEncontradosList.length > 10
? `<li style="opacity:0.7">…y ${r.noEncontradosList.length - 10} más</li>` : '';
row.innerHTML = `
<span class="icon">⚠️</span>
<div class="text">
<strong>${r.noEncontrados} referencia(s) no encontrada(s) — no se crearon productos nuevos</strong>
<ul>${items}${more}</ul>
</div>
`;
cont.appendChild(row);
}
if (r.errores > 0) {
const row = document.createElement('div');
row.className = 'upload-summary-row is-error';
const items = (r.erroresList || []).slice(0, 10).map(e =>
`<li><code>${escapeHtml(e.ref || ('Fila ' + e.fila))}</code> — ${escapeHtml(e.motivo)}</li>`
).join('');
const more = r.erroresList && r.erroresList.length > 10
? `<li style="opacity:0.7">…y ${r.erroresList.length - 10} más</li>` : '';
row.innerHTML = `
<span class="icon">❌</span>
<div class="text">
<strong>${r.errores} error(es) al procesar</strong>
<ul>${items}${more}</ul>
</div>
`;
cont.appendChild(row);
}
if (r.filasVacias > 0) {
const row = document.createElement('div');
row.className = 'upload-summary-row is-warning';
row.innerHTML = `
<span class="icon">️</span>
<div class="text">
<strong>${r.filasVacias} fila(s) vacía(s) ignorada(s)</strong>
</div>
`;
cont.appendChild(row);
}
if (cont.children.length === 0) {
cont.innerHTML = `<div class="upload-summary-row is-warning"><span class="icon">️</span><div class="text"><strong>Nada se actualizó</strong></div></div>`;
}
cont.style.display = 'flex';
}
function abrirAgotarProducto(ref) {
if (!confirm(`¿Marcar el producto "${ref}" como agotado?\n\nSu imagen se moverá a la carpeta "agotados" y dejará de mostrarse en el catálogo público. Podrás reactivarlo después desde la pestaña "Agotados".`)) return;
agotarProducto(ref);
+38 -2
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=4">
<link rel="stylesheet" href="admin.css?v=5">
</head>
<body>
@@ -164,6 +164,42 @@
</form>
</div>
<!-- Sub-sección: Asignar nombre + precio desde Excel/CSV -->
<div class="view-subsection" style="margin-top:28px">
<h2 class="subsection-title">📊 Asignar descripción y precio (Excel/CSV)</h2>
<div class="view-content-card">
<p class="excel-format-hint">
<span class="excel-format-cols">
<strong>A</strong> REFERENCIA
<span class="sep">|</span>
<strong>B</strong> DESCRIPCION
<span class="sep">|</span>
<strong>C</strong> PRECIO
</span>
<span class="excel-format-meta">.xlsx / .csv · encabezados en la primera fila · celdas vacías no sobrescriben</span>
</p>
<form id="formExcelUpload" class="excel-form" autocomplete="off">
<div class="form-group">
<label for="excelArchivo">Archivo Excel/CSV</label>
<input type="file" id="excelArchivo" accept=".xlsx,.xls,.csv" required>
<span class="form-hint" id="excelArchivoHint">Ningún archivo seleccionado</span>
</div>
<div class="excel-summary" id="excelSummary" style="display:none"></div>
<div class="form-error" id="excelError" role="alert"></div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="btnProcesarExcel" disabled>
<span class="btn-text">Procesar archivo</span>
<span class="btn-spinner" style="display:none"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Sub-sección: Lista de productos disponibles -->
<div class="view-subsection">
<div class="subsection-header">
@@ -464,6 +500,6 @@
</div>
</div>
<script src="admin.js?v=4"></script>
<script src="admin.js?v=5"></script>
</body>
</html>