Excel y sesiones.json a las 00
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user