descargar pdf y fotos
This commit is contained in:
@@ -259,11 +259,11 @@ function setupSubtabs() {
|
||||
|
||||
// ════════════════════════════════
|
||||
// DESCARGAS (vista + subtabs pdf / fotos)
|
||||
// Funcionalidad TBD — placeholders por ahora
|
||||
// ════════════════════════════════
|
||||
function cargarDescargas() {
|
||||
// Placeholder. Cuando se implemente cada subpestaña, se cargan
|
||||
// los datos del backend (PDFs disponibles, lotes de fotos, etc.).
|
||||
initGestores();
|
||||
pdfGestor.setup();
|
||||
fotosGestor.setup();
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
@@ -2162,3 +2162,383 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('btnConfirmarEliminarProdDef');
|
||||
if (btn) btn.addEventListener('click', confirmarEliminarProductoDef);
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// DESCARGAS — gestor genérico de árbol de selección
|
||||
// Usado por los subtabs PDF y Fotos.
|
||||
// ════════════════════════════════
|
||||
function isSafari() {
|
||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function createDescargaGestor(config) {
|
||||
const {
|
||||
selectorId, summaryId, summaryCountId, summaryLeavesId,
|
||||
btnGenerateId, btnAllId, btnNoneId, errorId,
|
||||
generateUrl, generateBtnText, fallbackFilename,
|
||||
extractZipOnNonSafari = false
|
||||
} = config;
|
||||
|
||||
const state = {
|
||||
arbol: [],
|
||||
productos: [],
|
||||
seleccionHojas: new Set()
|
||||
};
|
||||
|
||||
function contarProductosEnRama(catId, subArr) {
|
||||
return state.productos.filter(p => {
|
||||
if (!p.cat || p.cat === 'sin-categoria') return false;
|
||||
if (!p.img) return false;
|
||||
if (p.img.startsWith('agotados/')) return false;
|
||||
if (p.cat !== catId) return false;
|
||||
const pSub = Array.isArray(p.sub) ? p.sub : [];
|
||||
if (subArr.length === 0) return true;
|
||||
if (pSub.length < subArr.length) return false;
|
||||
for (let i = 0; i < subArr.length; i++) {
|
||||
if (pSub[i] !== subArr[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function renderNodoHtml(nodo, pathArr) {
|
||||
const key = pathArr.join('|');
|
||||
const catId = pathArr[0];
|
||||
const subArr = pathArr.slice(1);
|
||||
const tieneHijos = Array.isArray(nodo.subcategorias) && nodo.subcategorias.length > 0;
|
||||
|
||||
const count = tieneHijos
|
||||
? nodo.subcategorias.reduce(
|
||||
(sum, s) => sum + contarProductosEnRama(catId, [...subArr, s.id]),
|
||||
0
|
||||
)
|
||||
: contarProductosEnRama(catId, subArr);
|
||||
|
||||
const isLeaf = !tieneHijos;
|
||||
const disabled = count === 0 ? 'disabled' : '';
|
||||
const countCls = count === 0 ? 'is-zero' : '';
|
||||
|
||||
let html = `
|
||||
<div class="pdf-node">
|
||||
<label class="pdf-check">
|
||||
<input type="checkbox"
|
||||
data-key="${escapeAttr(key)}"
|
||||
data-has-children="${tieneHijos ? '1' : '0'}"
|
||||
data-leaf="${isLeaf ? '1' : '0'}"
|
||||
${disabled}>
|
||||
</label>
|
||||
<span class="pdf-node-label">
|
||||
<span class="pdf-node-icon">${nodo.icono || '📦'}</span>
|
||||
<span class="pdf-node-name">${escapeHtml(nodo.nombre)}</span>
|
||||
<span class="pdf-node-count ${countCls}">${count}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (tieneHijos) {
|
||||
const childrenHtml = nodo.subcategorias
|
||||
.map(s => renderNodoHtml(s, [...pathArr, s.id]))
|
||||
.join('');
|
||||
html += `<div class="pdf-node-children">${childrenHtml}</div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function cargarArbol() {
|
||||
const cont = document.getElementById(selectorId);
|
||||
cont.innerHTML = '<p class="pdf-selector-loading">Cargando categorías…</p>';
|
||||
|
||||
try {
|
||||
const [resCats, resProds] = await Promise.all([
|
||||
fetch('/api/categorias', { credentials: 'include' }),
|
||||
fetch('/api/productos', { credentials: 'include' })
|
||||
]);
|
||||
if (!resCats.ok) throw new Error('Error al cargar categorías');
|
||||
if (!resProds.ok) throw new Error('Error al cargar productos');
|
||||
|
||||
state.arbol = await resCats.json();
|
||||
state.productos = await resProds.json();
|
||||
state.seleccionHojas = new Set();
|
||||
|
||||
renderArbol();
|
||||
} catch (err) {
|
||||
console.error(`[${selectorId}] Error cargando datos:`, err);
|
||||
cont.innerHTML = '<p class="pdf-selector-empty">Error al cargar. Recarga la pestaña.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderArbol() {
|
||||
const cont = document.getElementById(selectorId);
|
||||
const catsVisibles = state.arbol.filter(c => c.id !== 'sin-categoria');
|
||||
|
||||
if (catsVisibles.length === 0) {
|
||||
cont.innerHTML = '<p class="pdf-selector-empty">No hay categorías para mostrar.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
cont.innerHTML = catsVisibles.map(cat => renderNodoHtml(cat, [cat.id])).join('');
|
||||
|
||||
cont.querySelectorAll('.pdf-check input').forEach(input => {
|
||||
input.addEventListener('change', onCheckChange);
|
||||
});
|
||||
|
||||
actualizarResumen();
|
||||
}
|
||||
|
||||
function onCheckChange(e) {
|
||||
const input = e.target;
|
||||
const key = input.dataset.key;
|
||||
const hasChildren = input.dataset.hasChildren === '1';
|
||||
const isChecked = input.checked;
|
||||
|
||||
if (input.disabled) {
|
||||
input.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
setDescendantsChecked(key, isChecked);
|
||||
} else {
|
||||
if (isChecked) {
|
||||
state.seleccionHojas.add(key);
|
||||
} else {
|
||||
state.seleccionHojas.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
input.indeterminate = false;
|
||||
updateAncestorsState();
|
||||
actualizarResumen();
|
||||
}
|
||||
|
||||
function setDescendantsChecked(parentKey, isChecked) {
|
||||
const cont = document.getElementById(selectorId);
|
||||
const inputs = cont.querySelectorAll(`.pdf-check input[data-key^="${CSS.escape(parentKey)}|"]`);
|
||||
|
||||
inputs.forEach(child => {
|
||||
if (child.disabled) return;
|
||||
child.checked = isChecked;
|
||||
child.indeterminate = false;
|
||||
if (child.dataset.leaf === '1') {
|
||||
if (isChecked) {
|
||||
state.seleccionHojas.add(child.dataset.key);
|
||||
} else {
|
||||
state.seleccionHojas.delete(child.dataset.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAncestorsState() {
|
||||
const cont = document.getElementById(selectorId);
|
||||
const inputs = cont.querySelectorAll('.pdf-check input[data-has-children="1"]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = input.dataset.key;
|
||||
const leafInputs = cont.querySelectorAll(
|
||||
`.pdf-check input[data-leaf="1"][data-key^="${CSS.escape(key)}|"]`
|
||||
);
|
||||
|
||||
let checked = 0, total = 0;
|
||||
leafInputs.forEach(leaf => {
|
||||
if (leaf.disabled) return;
|
||||
total++;
|
||||
if (leaf.checked) checked++;
|
||||
});
|
||||
|
||||
input.checked = total > 0 && checked === total;
|
||||
input.indeterminate = checked > 0 && checked < total;
|
||||
});
|
||||
}
|
||||
|
||||
function marcarTodo() {
|
||||
const cont = document.getElementById(selectorId);
|
||||
cont.querySelectorAll('.pdf-check input').forEach(input => {
|
||||
if (input.disabled) return;
|
||||
input.checked = true;
|
||||
input.indeterminate = false;
|
||||
if (input.dataset.leaf === '1') {
|
||||
state.seleccionHojas.add(input.dataset.key);
|
||||
}
|
||||
});
|
||||
updateAncestorsState();
|
||||
actualizarResumen();
|
||||
}
|
||||
|
||||
function desmarcarTodo() {
|
||||
state.seleccionHojas.clear();
|
||||
const cont = document.getElementById(selectorId);
|
||||
cont.querySelectorAll('.pdf-check input').forEach(input => {
|
||||
input.checked = false;
|
||||
input.indeterminate = false;
|
||||
});
|
||||
actualizarResumen();
|
||||
}
|
||||
|
||||
function actualizarResumen() {
|
||||
const summary = document.getElementById(summaryId);
|
||||
const countEl = document.getElementById(summaryCountId);
|
||||
const leavesEl = document.getElementById(summaryLeavesId);
|
||||
|
||||
let totalProductos = 0;
|
||||
for (const key of state.seleccionHojas) {
|
||||
const parts = key.split('|');
|
||||
const catId = parts[0];
|
||||
const subArr = parts.slice(1);
|
||||
totalProductos += contarProductosEnRama(catId, subArr);
|
||||
}
|
||||
|
||||
if (state.seleccionHojas.size === 0) {
|
||||
summary.style.display = 'none';
|
||||
} else {
|
||||
summary.style.display = '';
|
||||
countEl.textContent = totalProductos;
|
||||
leavesEl.textContent = state.seleccionHojas.size;
|
||||
}
|
||||
|
||||
const btn = document.getElementById(btnGenerateId);
|
||||
if (btn) btn.disabled = state.seleccionHojas.size === 0;
|
||||
}
|
||||
|
||||
async function generar() {
|
||||
const btn = document.getElementById(btnGenerateId);
|
||||
const errEl = document.getElementById(errorId);
|
||||
errEl.textContent = '';
|
||||
errEl.classList.remove('show');
|
||||
|
||||
if (state.seleccionHojas.size === 0) {
|
||||
errEl.textContent = 'Selecciona al menos una hoja';
|
||||
errEl.classList.add('show');
|
||||
return;
|
||||
}
|
||||
|
||||
const seleccion = Array.from(state.seleccionHojas).map(key => {
|
||||
const parts = key.split('|');
|
||||
return { cat: parts[0], sub: parts.slice(1) };
|
||||
});
|
||||
|
||||
btn.disabled = true;
|
||||
const spinner = btn.querySelector('.btn-spinner');
|
||||
const btnText = btn.querySelector('.btn-text');
|
||||
btnText.textContent = 'Generando…';
|
||||
if (spinner) spinner.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
const res = await fetch(generateUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ seleccion })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Error al generar la descarga');
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
|
||||
if (extractZipOnNonSafari && !isSafari()) {
|
||||
const zip = await JSZip.loadAsync(blob);
|
||||
const entries = [];
|
||||
zip.forEach((relPath, zipEntry) => entries.push({ relPath, zipEntry }));
|
||||
btnText.textContent = `Extrayendo ${entries.length} foto(s)…`;
|
||||
for (const { relPath, zipEntry } of entries) {
|
||||
const imgBlob = await zipEntry.async('blob');
|
||||
const imgUrl = URL.createObjectURL(imgBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = imgUrl;
|
||||
a.download = relPath;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
} else {
|
||||
const dispo = res.headers.get('Content-Disposition') || '';
|
||||
const match = dispo.match(/filename="(.+)"/);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = match ? match[1] : fallbackFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.add('show');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = generateBtnText;
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
actualizarResumen();
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const btnGen = document.getElementById(btnGenerateId);
|
||||
const btnAll = document.getElementById(btnAllId);
|
||||
const btnNone = document.getElementById(btnNoneId);
|
||||
|
||||
if (btnGen && !btnGen.dataset.wired) {
|
||||
btnGen.addEventListener('click', generar);
|
||||
btnGen.dataset.wired = '1';
|
||||
}
|
||||
if (btnAll && !btnAll.dataset.wired) {
|
||||
btnAll.addEventListener('click', marcarTodo);
|
||||
btnAll.dataset.wired = '1';
|
||||
}
|
||||
if (btnNone && !btnNone.dataset.wired) {
|
||||
btnNone.addEventListener('click', desmarcarTodo);
|
||||
btnNone.dataset.wired = '1';
|
||||
}
|
||||
|
||||
cargarArbol();
|
||||
}
|
||||
|
||||
return { setup, state, marcarTodo, desmarcarTodo, generar };
|
||||
}
|
||||
|
||||
// Instancias del gestor para cada subtab
|
||||
let pdfGestor;
|
||||
let fotosGestor;
|
||||
|
||||
// Inicialización diferida en cargarDescargas()
|
||||
function initGestores() {
|
||||
if (pdfGestor) return;
|
||||
pdfGestor = createDescargaGestor({
|
||||
selectorId: 'pdfSelector',
|
||||
summaryId: 'pdfSummary',
|
||||
summaryCountId: 'pdfSummaryCount',
|
||||
summaryLeavesId: 'pdfSummaryLeaves',
|
||||
btnGenerateId: 'btnPdfGenerar',
|
||||
btnAllId: 'btnPdfMarcarTodo',
|
||||
btnNoneId: 'btnPdfDesmarcarTodo',
|
||||
errorId: 'pdfError',
|
||||
generateUrl: '/api/descargas/pdf',
|
||||
generateBtnText: '📄 Generar PDF',
|
||||
fallbackFilename: 'catalogo-infinity.pdf'
|
||||
});
|
||||
|
||||
fotosGestor = createDescargaGestor({
|
||||
selectorId: 'fotosSelector',
|
||||
summaryId: 'fotosSummary',
|
||||
summaryCountId: 'fotosSummaryCount',
|
||||
summaryLeavesId: 'fotosSummaryLeaves',
|
||||
btnGenerateId: 'btnFotosGenerar',
|
||||
btnAllId: 'btnFotosMarcarTodo',
|
||||
btnNoneId: 'btnFotosDesmarcarTodo',
|
||||
errorId: 'fotosError',
|
||||
generateUrl: '/api/descargas/fotos',
|
||||
generateBtnText: '🖼️ Descargar fotos',
|
||||
fallbackFilename: 'fotos-infinity.zip',
|
||||
extractZipOnNonSafari: true
|
||||
});
|
||||
}
|
||||
|
||||
// cargarDescargas() ahora usa los gestores
|
||||
// (definido arriba en la sección DESCARGAS)
|
||||
|
||||
Reference in New Issue
Block a user