pestana descargas y subcategorias funcionando
This commit is contained in:
@@ -766,19 +766,39 @@ function renderCategorias(categorias, productos) {
|
||||
grid.innerHTML = categorias.map(c => {
|
||||
const count = countByCat[c.id] || 0;
|
||||
const isSistema = c.id === 'sin-categoria';
|
||||
const subcats = Array.isArray(c.subcategorias) ? c.subcategorias : [];
|
||||
const tieneSubcats = subcats.length > 0;
|
||||
|
||||
return `
|
||||
<div class="categoria-card ${isSistema ? 'categoria-sistema' : ''}">
|
||||
${isSistema ? '<span class="categoria-sistema-badge">Sistema</span>' : ''}
|
||||
<div class="categoria-icon">${c.icono || '📦'}</div>
|
||||
<div class="categoria-nombre">${escapeHtml(c.nombre)}</div>
|
||||
<div class="categoria-id">${escapeHtml(c.id)}</div>
|
||||
<div class="categoria-count">${count} item${count === 1 ? '' : 's'}</div>
|
||||
${!isSistema ? `
|
||||
<div class="categoria-actions">
|
||||
<button class="btn-icon" onclick="abrirEditarCategoria('${escapeAttr(c.id)}')" title="Editar" type="button">✏️</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="abrirEliminarCategoria('${escapeAttr(c.id)}','${escapeAttr(c.nombre)}')" title="Eliminar" type="button">🗑️</button>
|
||||
<div class="categoria-header">
|
||||
<div class="categoria-icon">${c.icono || '📦'}</div>
|
||||
<div class="categoria-meta">
|
||||
<div class="categoria-nombre">${escapeHtml(c.nombre)}</div>
|
||||
<div class="categoria-id">${escapeHtml(c.id)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="categoria-count">${count} item${count === 1 ? '' : 's'}</div>
|
||||
${!isSistema ? `
|
||||
<div class="categoria-actions">
|
||||
<div class="menu-dropdown">
|
||||
<button class="btn-icon-tiny menu-toggle" type="button" aria-label="Más acciones" aria-haspopup="true" aria-expanded="false">⋯</button>
|
||||
<div class="menu-panel" hidden>
|
||||
<div class="menu-header" title="${escapeAttr(c.nombre)}">${escapeHtml(c.nombre)}</div>
|
||||
<button type="button" class="menu-item" data-action="add-sub-cat" data-id="${escapeAttr(c.id)}">
|
||||
<span class="menu-icon">+</span> Agregar subcategoría
|
||||
</button>
|
||||
<button type="button" class="menu-item" data-action="edit-cat" data-id="${escapeAttr(c.id)}">
|
||||
<span class="menu-icon">✏️</span> Editar nombre e icono
|
||||
</button>
|
||||
<button type="button" class="menu-item menu-item-danger" data-action="delete-cat" data-id="${escapeAttr(c.id)}" data-name="${escapeAttr(c.nombre)}">
|
||||
<span class="menu-icon">🗑️</span> Eliminar categoría
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${tieneSubcats ? `<div class="subcat-tree">${renderSubcatRows(subcats, [c.id])}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -937,6 +957,319 @@ function setupCategoriasModals() {
|
||||
document.getElementById('btnCrearCategoria').addEventListener('click', abrirCrearCategoria);
|
||||
document.getElementById('formCategoria').addEventListener('submit', guardarCategoria);
|
||||
document.getElementById('btnConfirmarEliminarCategoria').addEventListener('click', confirmarEliminarCategoria);
|
||||
document.getElementById('formSubcategoria').addEventListener('submit', guardarSubcategoria);
|
||||
document.getElementById('btnConfirmarEliminarSubcat').addEventListener('click', confirmarEliminarSubcategoria);
|
||||
setupKebabMenus();
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
// MENÚ KEBAB (drop-down ⋯) — para categoría y subcats
|
||||
// - Click en .menu-toggle → abre/cierra
|
||||
// - Click en .menu-item → ejecuta acción, cierra
|
||||
// - Click fuera / Escape → cierra todos
|
||||
// - Al abrir, marca .menu-active en el contenedor para que no se oculte al perder hover
|
||||
// ════════════════════════════════
|
||||
function setupKebabMenus() {
|
||||
document.addEventListener('click', e => {
|
||||
// Click en el toggle ⋯
|
||||
const toggle = e.target.closest('.menu-toggle');
|
||||
if (toggle) {
|
||||
e.stopPropagation();
|
||||
const dropdown = toggle.parentElement;
|
||||
const panel = dropdown.querySelector('.menu-panel');
|
||||
if (!panel) return;
|
||||
const wasOpen = !panel.hidden;
|
||||
closeAllKebabMenus();
|
||||
if (!wasOpen) {
|
||||
panel.hidden = false;
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
// Marcar la card como host para que flote sobre los vecinos
|
||||
const card = dropdown.closest('.categoria-card');
|
||||
if (card) card.classList.add('menu-host');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Click en un item del menú
|
||||
const item = e.target.closest('.menu-item');
|
||||
if (item) {
|
||||
const action = item.dataset.action;
|
||||
closeAllKebabMenus();
|
||||
if (action === 'add-sub-cat') abrirCrearSubcategoria(item.dataset.id);
|
||||
else if (action === 'add-sub-sub-cat') abrirCrearSubcatHijo(item.dataset.cat, item.dataset.sub);
|
||||
else if (action === 'edit-subcat') abrirEditarSubcategoria(item.dataset.cat, item.dataset.sub);
|
||||
else if (action === 'delete-subcat') abrirEliminarSubcategoria(item.dataset.cat, item.dataset.sub, item.dataset.name);
|
||||
else if (action === 'edit-cat') abrirEditarCategoria(item.dataset.id);
|
||||
else if (action === 'delete-cat') abrirEliminarCategoria(item.dataset.id, item.dataset.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click fuera de cualquier dropdown
|
||||
if (!e.target.closest('.menu-dropdown')) {
|
||||
closeAllKebabMenus();
|
||||
}
|
||||
});
|
||||
|
||||
// Escape cierra todos
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeAllKebabMenus();
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllKebabMenus() {
|
||||
document.querySelectorAll('.menu-panel').forEach(p => p.hidden = true);
|
||||
document.querySelectorAll('.menu-toggle').forEach(b => b.setAttribute('aria-expanded', 'false'));
|
||||
document.querySelectorAll('.categoria-card.menu-host')
|
||||
.forEach(c => c.classList.remove('menu-host'));
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
// SUBCATEGORÍAS (CRUD sobre el árbol anidado)
|
||||
// Estado: editingSubcat = { parentId, subId } cuando se está editando
|
||||
// deletingSubcat = { parentId, subId, nombre } cuando se va a eliminar
|
||||
// ════════════════════════════════
|
||||
let editingSubcat = null;
|
||||
let deletingSubcat = null;
|
||||
|
||||
// Render recursivo de subcategorías como filas anidadas dentro de la card de la categoría
|
||||
function renderSubcatRows(subcats, parentPath) {
|
||||
if (!Array.isArray(subcats) || subcats.length === 0) return '';
|
||||
// parentPath = [c.id] → render nivel 2 (subs). El + aparece (pueden tener sub-subs).
|
||||
// parentPath = [c.id, s.id] → render nivel 3 (sub-subs). El + NO aparece (límite 3 niveles).
|
||||
const puedenTenerHijos = parentPath.length === 1;
|
||||
return subcats.map(s => {
|
||||
const path = [...parentPath, s.id];
|
||||
const tieneSubSub = Array.isArray(s.subcategorias) && s.subcategorias.length > 0;
|
||||
return `
|
||||
<div class="subcat-row" data-subcat-id="${escapeAttr(s.id)}">
|
||||
<div class="subcat-info">
|
||||
<span class="subcat-name">${escapeHtml(s.nombre)}</span>
|
||||
<span class="subcat-id">${escapeHtml(s.id)}</span>
|
||||
</div>
|
||||
<div class="subcat-actions">
|
||||
<div class="menu-dropdown">
|
||||
<button class="btn-icon-tiny menu-toggle" type="button" aria-label="Más acciones" aria-haspopup="true" aria-expanded="false">⋯</button>
|
||||
<div class="menu-panel" hidden>
|
||||
<div class="menu-header" title="${escapeAttr(s.nombre)}">${escapeHtml(s.nombre)}</div>
|
||||
${puedenTenerHijos ? `<button type="button" class="menu-item" data-action="add-sub-sub-cat" data-cat="${escapeAttr(parentPath[0])}" data-sub="${escapeAttr(s.id)}">
|
||||
<span class="menu-icon">+</span> Agregar sub-subcategoría
|
||||
</button>` : ''}
|
||||
<button type="button" class="menu-item" data-action="edit-subcat" data-cat="${escapeAttr(parentPath[0])}" data-sub="${escapeAttr(s.id)}">
|
||||
<span class="menu-icon">✏️</span> Editar nombre
|
||||
</button>
|
||||
<button type="button" class="menu-item menu-item-danger" data-action="delete-subcat" data-cat="${escapeAttr(parentPath[0])}" data-sub="${escapeAttr(s.id)}" data-name="${escapeAttr(s.nombre)}">
|
||||
<span class="menu-icon">🗑️</span> Eliminar subcategoría
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${tieneSubSub ? `<div class="subcat-children">${renderSubcatRows(s.subcategorias, path)}</div>` : ''}
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function abrirCrearSubcategoria(parentId) {
|
||||
editingSubcat = null;
|
||||
document.getElementById('modalSubcategoriaTitulo').textContent = 'Crear subcategoría';
|
||||
document.getElementById('btnGuardarSubcategoria').textContent = 'Crear';
|
||||
document.getElementById('subcatNombre').value = '';
|
||||
document.getElementById('subcatIdHint').textContent = '';
|
||||
document.getElementById('subcategoriaError').classList.remove('show');
|
||||
document.getElementById('subcategoriaError').textContent = '';
|
||||
// Mostrar breadcrumb del padre (solo la categoría)
|
||||
const categorias = await fetch('/api/categorias', { credentials: 'include' }).then(r => r.json());
|
||||
const parent = categorias.find(c => c.id === parentId);
|
||||
if (parent) {
|
||||
document.getElementById('subcatBreadcrumb').innerHTML =
|
||||
`<span class="bc-cat">${parent.icono || '📦'} ${escapeHtml(parent.nombre)}</span><span class="bc-arrow">›</span><span class="bc-new">Nueva subcategoría</span>`;
|
||||
}
|
||||
abrirModal('modalSubcategoria');
|
||||
|
||||
document.getElementById('subcatNombre').oninput = e => {
|
||||
const slug = slugifyLocal(e.target.value);
|
||||
document.getElementById('subcatIdHint').textContent = slug ? `ID: ${slug}` : '';
|
||||
};
|
||||
|
||||
// Guardar el parentId en el dataset del form para usarlo al enviar
|
||||
document.getElementById('formSubcategoria').dataset.parentId = parentId;
|
||||
document.getElementById('formSubcategoria').dataset.subId = '';
|
||||
}
|
||||
|
||||
async function abrirCrearSubcatHijo(parentCatId, parentSubId) {
|
||||
// Para crear una sub-subcategoría, el "padre" es la subcategoría (no la categoría raíz)
|
||||
editingSubcat = null;
|
||||
document.getElementById('modalSubcategoriaTitulo').textContent = 'Crear sub-subcategoría';
|
||||
document.getElementById('btnGuardarSubcategoria').textContent = 'Crear';
|
||||
document.getElementById('subcatNombre').value = '';
|
||||
document.getElementById('subcatIdHint').textContent = '';
|
||||
document.getElementById('subcategoriaError').classList.remove('show');
|
||||
document.getElementById('subcategoriaError').textContent = '';
|
||||
|
||||
const categorias = await fetch('/api/categorias', { credentials: 'include' }).then(r => r.json());
|
||||
const path = getCategoryPathCliente(categorias, parentSubId);
|
||||
if (path) {
|
||||
const crumbs = path.map(n => `<span class="bc-cat">${n.icono || '📦'} ${escapeHtml(n.nombre)}</span><span class="bc-arrow">›</span>`).join('');
|
||||
document.getElementById('subcatBreadcrumb').innerHTML = crumbs + '<span class="bc-new">Nueva sub-subcategoría</span>';
|
||||
}
|
||||
abrirModal('modalSubcategoria');
|
||||
|
||||
document.getElementById('subcatNombre').oninput = e => {
|
||||
const slug = slugifyLocal(e.target.value);
|
||||
document.getElementById('subcatIdHint').textContent = slug ? `ID: ${slug}` : '';
|
||||
};
|
||||
|
||||
document.getElementById('formSubcategoria').dataset.parentId = parentCatId;
|
||||
document.getElementById('formSubcategoria').dataset.subId = parentSubId;
|
||||
}
|
||||
|
||||
async function abrirEditarSubcategoria(parentCatId, subId) {
|
||||
const categorias = await fetch('/api/categorias', { credentials: 'include' }).then(r => r.json());
|
||||
const sub = findSubcatCliente(categorias, subId);
|
||||
if (!sub) return;
|
||||
|
||||
editingSubcat = { parentCatId, subId };
|
||||
document.getElementById('modalSubcategoriaTitulo').textContent = 'Editar subcategoría';
|
||||
document.getElementById('btnGuardarSubcategoria').textContent = 'Guardar cambios';
|
||||
document.getElementById('subcatNombre').value = sub.nombre;
|
||||
document.getElementById('subcatIdHint').textContent = `ID: ${sub.id} (no se puede cambiar)`;
|
||||
document.getElementById('subcategoriaError').classList.remove('show');
|
||||
document.getElementById('subcategoriaError').textContent = '';
|
||||
|
||||
const path = getCategoryPathCliente(categorias, subId);
|
||||
if (path) {
|
||||
const crumbs = path.slice(0, -1).map(n => `<span class="bc-cat">${n.icono || '📦'} ${escapeHtml(n.nombre)}</span><span class="bc-arrow">›</span>`).join('');
|
||||
document.getElementById('subcatBreadcrumb').innerHTML = crumbs + `<span class="bc-cat">${escapeHtml(sub.nombre)}</span>`;
|
||||
}
|
||||
abrirModal('modalSubcategoria');
|
||||
|
||||
document.getElementById('formSubcategoria').dataset.parentId = parentCatId;
|
||||
document.getElementById('formSubcategoria').dataset.subId = subId;
|
||||
}
|
||||
|
||||
function abrirEliminarSubcategoria(parentCatId, subId, nombre) {
|
||||
deletingSubcat = { parentCatId, subId, nombre };
|
||||
document.getElementById('eliminarSubcatNombre').textContent = nombre;
|
||||
document.getElementById('eliminarSubcatError').classList.remove('show');
|
||||
document.getElementById('eliminarSubcatError').textContent = '';
|
||||
abrirModal('modalEliminarSubcategoria');
|
||||
}
|
||||
|
||||
// Helpers cliente (espejo de los del backend)
|
||||
function getCategoryPathCliente(categorias, id, trail = []) {
|
||||
for (const c of categorias) {
|
||||
const next = [...trail, c];
|
||||
if (c.id === id) return next;
|
||||
if (Array.isArray(c.subcategorias) && c.subcategorias.length) {
|
||||
const found = getCategoryPathCliente(c.subcategorias, id, next);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function findSubcatCliente(categorias, id) {
|
||||
for (const c of categorias) {
|
||||
if (Array.isArray(c.subcategorias)) {
|
||||
const found = findInSubcatsCliente(c.subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function findInSubcatsCliente(subcats, id) {
|
||||
for (const s of subcats) {
|
||||
if (s.id === id) return s;
|
||||
if (Array.isArray(s.subcategorias) && s.subcategorias.length) {
|
||||
const found = findInSubcatsCliente(s.subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function guardarSubcategoria(e) {
|
||||
e.preventDefault();
|
||||
const nombre = document.getElementById('subcatNombre').value.trim();
|
||||
const errEl = document.getElementById('subcategoriaError');
|
||||
const btn = document.getElementById('btnGuardarSubcategoria');
|
||||
const form = document.getElementById('formSubcategoria');
|
||||
const parentId = form.dataset.parentId;
|
||||
const subId = form.dataset.subId;
|
||||
|
||||
if (!nombre) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = editingSubcat ? 'Guardando…' : 'Creando…';
|
||||
|
||||
try {
|
||||
let url, method, body;
|
||||
|
||||
if (editingSubcat) {
|
||||
// EDITAR: PUT /api/categorias/:parentId/subcategorias/:subId
|
||||
url = `/api/categorias/${encodeURIComponent(parentId)}/subcategorias/${encodeURIComponent(subId)}`;
|
||||
method = 'PUT';
|
||||
body = { nombre };
|
||||
} else if (subId) {
|
||||
// CREAR sub-subcat: POST con parentSubId en el body
|
||||
url = `/api/categorias/${encodeURIComponent(parentId)}/subcategorias`;
|
||||
method = 'POST';
|
||||
body = { nombre, parentSubId: subId };
|
||||
} else {
|
||||
// CREAR subcat nivel 1: POST simple
|
||||
url = `/api/categorias/${encodeURIComponent(parentId)}/subcategorias`;
|
||||
method = 'POST';
|
||||
body = { nombre };
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Error al guardar');
|
||||
|
||||
editingSubcat = null;
|
||||
cerrarModal('modalSubcategoria');
|
||||
await cargarCategorias();
|
||||
cargarEstadisticas();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.add('show');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = editingSubcat ? 'Guardar cambios' : 'Crear';
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmarEliminarSubcategoria() {
|
||||
if (!deletingSubcat) return;
|
||||
|
||||
const btn = document.getElementById('btnConfirmarEliminarSubcat');
|
||||
const errEl = document.getElementById('eliminarSubcatError');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Eliminando…';
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/categorias/${encodeURIComponent(deletingSubcat.parentCatId)}/subcategorias/${encodeURIComponent(deletingSubcat.subId)}`,
|
||||
{ method: 'DELETE', credentials: 'include' }
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Error al eliminar');
|
||||
|
||||
deletingSubcat = null;
|
||||
cerrarModal('modalEliminarSubcategoria');
|
||||
await cargarCategorias();
|
||||
cargarEstadisticas();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.add('show');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Sí, eliminar';
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
@@ -971,6 +1304,9 @@ async function cargarCategoriasEnUpload() {
|
||||
const categorias = await res.json();
|
||||
if (!Array.isArray(categorias)) throw new Error('Respuesta no es array');
|
||||
|
||||
// Cachear el árbol completo (con subcategorías) en el módulo para la cascada
|
||||
uploadCategoriasTree = categorias;
|
||||
|
||||
const seleccionPrevia = select.value;
|
||||
select.innerHTML = '<option value="">— Selecciona una categoría —</option>';
|
||||
categorias
|
||||
@@ -982,22 +1318,138 @@ async function cargarCategoriasEnUpload() {
|
||||
select.appendChild(opt);
|
||||
});
|
||||
if (seleccionPrevia) select.value = seleccionPrevia;
|
||||
|
||||
console.log(`[upload] Categorías cargadas: ${categorias.filter(c => c.id !== SIN_CAT_ID).length}`);
|
||||
} catch (err) {
|
||||
console.error('[upload] Error cargando categorías:', err);
|
||||
// Fallback visible para el usuario
|
||||
const error = document.getElementById('uploadError');
|
||||
if (error) error.textContent = 'No se pudieron cargar las categorías. Recarga la página.';
|
||||
}
|
||||
}
|
||||
|
||||
// Árbol completo cacheado para la cascada del upload
|
||||
let uploadCategoriasTree = [];
|
||||
|
||||
// Se ejecuta cuando el usuario cambia la categoría
|
||||
function onCategoriaChange() {
|
||||
const catId = document.getElementById('uploadCategoria').value;
|
||||
const subGrp = document.getElementById('uploadSubcatGroup');
|
||||
const subSel = document.getElementById('uploadSubcategoria');
|
||||
const subSubGrp = document.getElementById('uploadSubSubcatGroup');
|
||||
const subSubSel = document.getElementById('uploadSubSubcategoria');
|
||||
|
||||
// Reset
|
||||
subSel.innerHTML = '<option value="">— Selecciona una subcategoría —</option>';
|
||||
subSubSel.innerHTML = '<option value="">— Selecciona una sub-subcategoría —</option>';
|
||||
subGrp.style.display = 'none';
|
||||
subSubGrp.style.display = 'none';
|
||||
|
||||
if (!catId) {
|
||||
actualizarBreadcrumbDestino();
|
||||
updateSubmitButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const cat = uploadCategoriasTree.find(c => c.id === catId);
|
||||
if (!cat) return;
|
||||
|
||||
if (Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0) {
|
||||
subGrp.style.display = '';
|
||||
cat.subcategorias.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = `${s.icono || '📂'} ${s.nombre}`;
|
||||
subSel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
actualizarBreadcrumbDestino();
|
||||
updateSubmitButton();
|
||||
}
|
||||
|
||||
// Se ejecuta cuando el usuario cambia la subcategoría
|
||||
function onSubcategoriaChange() {
|
||||
const catId = document.getElementById('uploadCategoria').value;
|
||||
const subId = document.getElementById('uploadSubcategoria').value;
|
||||
const subSubGrp = document.getElementById('uploadSubSubcatGroup');
|
||||
const subSubSel = document.getElementById('uploadSubSubcategoria');
|
||||
|
||||
subSubSel.innerHTML = '<option value="">— Selecciona una sub-subcategoría —</option>';
|
||||
subSubGrp.style.display = 'none';
|
||||
|
||||
if (!catId || !subId) {
|
||||
actualizarBreadcrumbDestino();
|
||||
updateSubmitButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const cat = uploadCategoriasTree.find(c => c.id === catId);
|
||||
if (!cat || !Array.isArray(cat.subcategorias)) return;
|
||||
|
||||
const sub = cat.subcategorias.find(s => s.id === subId);
|
||||
if (!sub) return;
|
||||
|
||||
if (Array.isArray(sub.subcategorias) && sub.subcategorias.length > 0) {
|
||||
subSubGrp.style.display = '';
|
||||
sub.subcategorias.forEach(ss => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ss.id;
|
||||
opt.textContent = `${ss.icono || '📂'} ${ss.nombre}`;
|
||||
subSubSel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
actualizarBreadcrumbDestino();
|
||||
updateSubmitButton();
|
||||
}
|
||||
|
||||
// Muestra el breadcrumb del destino actual (cat > sub1 > sub2)
|
||||
function actualizarBreadcrumbDestino() {
|
||||
const cont = document.getElementById('uploadDestino');
|
||||
const path = document.getElementById('uploadDestinoPath');
|
||||
if (!cont || !path) return;
|
||||
|
||||
const catId = document.getElementById('uploadCategoria').value;
|
||||
const subId = document.getElementById('uploadSubcategoria').value;
|
||||
const subSubId= document.getElementById('uploadSubSubcategoria').value;
|
||||
|
||||
if (!catId) { cont.style.display = 'none'; return; }
|
||||
|
||||
const cat = uploadCategoriasTree.find(c => c.id === catId);
|
||||
if (!cat) { cont.style.display = 'none'; return; }
|
||||
|
||||
const parts = [`${cat.icono || '📦'} ${cat.nombre}`];
|
||||
if (subId) {
|
||||
const sub = (cat.subcategorias || []).find(s => s.id === subId);
|
||||
if (sub) {
|
||||
parts.push(`${sub.icono || '📂'} ${sub.nombre}`);
|
||||
if (subSubId) {
|
||||
const subSub = (sub.subcategorias || []).find(s => s.id === subSubId);
|
||||
if (subSub) parts.push(`${subSub.icono || '📂'} ${subSub.nombre}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.textContent = parts.join(' › ');
|
||||
cont.style.display = '';
|
||||
}
|
||||
|
||||
// Devuelve el array `sub` que se enviará al backend (la cadena hasta la hoja)
|
||||
function getSubPathActual() {
|
||||
const catId = document.getElementById('uploadCategoria').value;
|
||||
const subId = document.getElementById('uploadSubcategoria').value;
|
||||
const subSubId = document.getElementById('uploadSubSubcategoria').value;
|
||||
|
||||
if (!catId) return [];
|
||||
const path = [];
|
||||
if (subId) path.push(subId);
|
||||
if (subSubId) path.push(subSubId);
|
||||
return path;
|
||||
}
|
||||
|
||||
function setupProductosUpload() {
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const inputFotos = document.getElementById('inputFotos');
|
||||
const btnLimpiar = document.getElementById('btnLimpiarPreview');
|
||||
const form = document.getElementById('formUploadProductos');
|
||||
const select = document.getElementById('uploadCategoria');
|
||||
const selectSub = document.getElementById('uploadSubcategoria');
|
||||
const selectSubSub= document.getElementById('uploadSubSubcategoria');
|
||||
|
||||
if (!dropzone || !inputFotos) return;
|
||||
|
||||
@@ -1034,7 +1486,10 @@ function setupProductosUpload() {
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
select.addEventListener('change', updateSubmitButton);
|
||||
// Cascada: categoría → subcategoría → sub-subcategoría
|
||||
select.addEventListener('change', onCategoriaChange);
|
||||
if (selectSub) selectSub.addEventListener('change', onSubcategoriaChange);
|
||||
if (selectSubSub) selectSubSub.addEventListener('change', updateSubmitButton);
|
||||
|
||||
form.addEventListener('submit', submitUpload);
|
||||
}
|
||||
@@ -1126,10 +1581,27 @@ function renderPreview() {
|
||||
|
||||
function updateSubmitButton() {
|
||||
const btn = document.getElementById('btnSubirProductos');
|
||||
const select = document.getElementById('uploadCategoria');
|
||||
const catId = document.getElementById('uploadCategoria').value;
|
||||
const validos = pendingFiles.filter(p => p.isValid).length;
|
||||
const hayCat = !!select.value;
|
||||
btn.disabled = !(hayCat && validos > 0);
|
||||
if (!catId || validos === 0) {
|
||||
btn.disabled = true;
|
||||
return;
|
||||
}
|
||||
// Validar que la cadena llegue a una hoja
|
||||
const cat = uploadCategoriasTree.find(c => c.id === catId);
|
||||
if (!cat) { btn.disabled = true; return; }
|
||||
const catTieneSubcats = Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0;
|
||||
if (catTieneSubcats) {
|
||||
const subId = document.getElementById('uploadSubcategoria').value;
|
||||
if (!subId) { btn.disabled = true; return; }
|
||||
const sub = cat.subcategorias.find(s => s.id === subId);
|
||||
if (!sub) { btn.disabled = true; return; }
|
||||
if (Array.isArray(sub.subcategorias) && sub.subcategorias.length > 0) {
|
||||
const subSubId = document.getElementById('uploadSubSubcategoria').value;
|
||||
if (!subSubId) { btn.disabled = true; return; }
|
||||
}
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
async function submitUpload(e) {
|
||||
@@ -1158,8 +1630,11 @@ async function submitUpload(e) {
|
||||
btn.querySelector('.btn-text').textContent = 'Subiendo…';
|
||||
btn.querySelector('.btn-spinner').style.display = 'inline-block';
|
||||
|
||||
const subPath = getSubPathActual();
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('categoria', select.value);
|
||||
fd.append('sub', JSON.stringify(subPath));
|
||||
validos.forEach(item => fd.append('fotos', item.file, item.file.name));
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user