descargar pdf y fotos
This commit is contained in:
@@ -2286,3 +2286,239 @@ select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════
|
||||
PDF — árbol de selección + generar
|
||||
════════════════════════════════ */
|
||||
.pdf-intro {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
background: rgba(167, 139, 250, 0.06);
|
||||
border: 1.5px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.pdf-intro-icon {
|
||||
font-size: 2.2rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 4px 12px rgba(167, 139, 250, 0.3));
|
||||
}
|
||||
|
||||
.pdf-intro-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: var(--ink);
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.pdf-intro-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Lista de selección (árbol) */
|
||||
.pdf-selector {
|
||||
border: 1.5px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--white);
|
||||
padding: 8px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pdf-selector-loading {
|
||||
text-align: center;
|
||||
color: var(--ink-mute);
|
||||
padding: 30px 20px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.pdf-selector-empty {
|
||||
text-align: center;
|
||||
color: var(--ink-mute);
|
||||
padding: 30px 20px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
/* Cada nodo del árbol */
|
||||
.pdf-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pdf-node:hover {
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
.pdf-node-children {
|
||||
margin-left: 22px;
|
||||
border-left: 1.5px dashed var(--line);
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.pdf-node-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.92rem;
|
||||
color: var(--ink);
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdf-node-icon {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-node-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pdf-node-count {
|
||||
font-size: 0.72rem;
|
||||
color: var(--ink-mute);
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
padding: 1px 8px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-node-count.is-zero {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Checkbox custom */
|
||||
.pdf-check {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pdf-check input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--white);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-check input:hover {
|
||||
border-color: var(--violet);
|
||||
}
|
||||
|
||||
.pdf-check input:checked {
|
||||
background: var(--grad-primary);
|
||||
border-color: var(--violet-deep);
|
||||
}
|
||||
|
||||
.pdf-check input:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 1px;
|
||||
width: 6px;
|
||||
height: 11px;
|
||||
border: solid white;
|
||||
border-width: 0 2.5px 2.5px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.pdf-check input:indeterminate {
|
||||
background: var(--violet);
|
||||
border-color: var(--violet-deep);
|
||||
}
|
||||
|
||||
.pdf-check input:indeterminate::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 7px;
|
||||
width: 10px;
|
||||
height: 2.5px;
|
||||
background: white;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.pdf-check input:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Resumen de selección */
|
||||
.pdf-summary {
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pdf-summary-text strong {
|
||||
color: var(--violet-deep);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Acciones del PDF */
|
||||
.pdf-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pdf-actions .btn-ghost {
|
||||
font-size: 0.88rem;
|
||||
padding: 10px 18px;
|
||||
}
|
||||
|
||||
.btn-pdf-generar {
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pdf-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
.pdf-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.btn-pdf-generar {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -273,20 +273,74 @@
|
||||
|
||||
<div class="subtab-panel active" data-subtab-panel="pdf">
|
||||
<div class="view-content-card">
|
||||
<div class="subtab-placeholder">
|
||||
<span class="subtab-placeholder-icon">📄</span>
|
||||
<h3>Descargas en PDF</h3>
|
||||
<p>Próximamente: herramientas para generar y descargar PDFs del catálogo.</p>
|
||||
<div class="pdf-intro">
|
||||
<div class="pdf-intro-icon">📄</div>
|
||||
<div>
|
||||
<h3 class="pdf-intro-title">Generar catálogo en PDF</h3>
|
||||
<p class="pdf-intro-desc">Marca las categorías y subcategorías que quieres incluir. El PDF se generará al instante con foto, referencia, nombre y precio de cada producto.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-selector" id="pdfSelector">
|
||||
<p class="pdf-selector-loading">Cargando categorías…</p>
|
||||
</div>
|
||||
|
||||
<div class="pdf-summary" id="pdfSummary" style="display:none">
|
||||
<span class="pdf-summary-text">
|
||||
<strong id="pdfSummaryCount">0</strong> producto(s) seleccionado(s) en <strong id="pdfSummaryLeaves">0</strong> hoja(s)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-error" id="pdfError" role="alert"></div>
|
||||
|
||||
<div class="pdf-actions">
|
||||
<button type="button" class="btn btn-ghost" id="btnPdfDesmarcarTodo">
|
||||
<span>✕</span> Desmarcar todo
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost" id="btnPdfMarcarTodo">
|
||||
<span>✓</span> Marcar todo
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-pdf-generar" id="btnPdfGenerar" disabled>
|
||||
<span class="btn-text">📄 Generar PDF</span>
|
||||
<span class="btn-spinner" style="display:none">⏳</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subtab-panel" data-subtab-panel="fotos">
|
||||
<div class="view-content-card">
|
||||
<div class="subtab-placeholder">
|
||||
<span class="subtab-placeholder-icon">🖼️</span>
|
||||
<h3>Descargas de fotos</h3>
|
||||
<p>Próximamente: herramientas para descargar lotes de fotos del catálogo.</p>
|
||||
<div class="pdf-intro">
|
||||
<div class="pdf-intro-icon">🖼️</div>
|
||||
<div>
|
||||
<h3 class="pdf-intro-title">Descargar fotos del catálogo</h3>
|
||||
<p class="pdf-intro-desc">Marca las categorías y subcategorías para descargar un ZIP con las fotos de los productos seleccionados.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pdf-selector" id="fotosSelector">
|
||||
<p class="pdf-selector-loading">Cargando categorías…</p>
|
||||
</div>
|
||||
|
||||
<div class="pdf-summary" id="fotosSummary" style="display:none">
|
||||
<span class="pdf-summary-text">
|
||||
<strong id="fotosSummaryCount">0</strong> producto(s) seleccionado(s) en <strong id="fotosSummaryLeaves">0</strong> hoja(s)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-error" id="fotosError" role="alert"></div>
|
||||
|
||||
<div class="pdf-actions">
|
||||
<button type="button" class="btn btn-ghost" id="btnFotosDesmarcarTodo">
|
||||
<span>✕</span> Desmarcar todo
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost" id="btnFotosMarcarTodo">
|
||||
<span>✓</span> Marcar todo
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-pdf-generar" id="btnFotosGenerar" disabled>
|
||||
<span class="btn-text">🖼️ Descargar fotos</span>
|
||||
<span class="btn-spinner" style="display:none">⏳</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -622,6 +676,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="admin.js?v=16"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="admin.js?v=19"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user