pestana descargas y subcategorias funcionando
This commit is contained in:
+276
-6
@@ -126,6 +126,102 @@ async function saveCategorias(categorias) {
|
||||
await writeJson('categorias.json', categorias);
|
||||
}
|
||||
|
||||
// ════════════════════════════════
|
||||
// TREE HELPERS — para subcategorías anidadas
|
||||
// Estructura: cada categoría puede tener un array opcional `subcategorias`
|
||||
// con la misma forma, recursivamente. Máximo 3 niveles (cat > sub > sub-sub).
|
||||
// ════════════════════════════════
|
||||
function findCategoryById(categorias, id) {
|
||||
for (const c of categorias) {
|
||||
if (c.id === id) return c;
|
||||
if (Array.isArray(c.subcategorias) && c.subcategorias.length) {
|
||||
const found = findInSubcats(c.subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findInSubcats(subcats, id) {
|
||||
for (const s of subcats) {
|
||||
if (s.id === id) return s;
|
||||
if (Array.isArray(s.subcategorias) && s.subcategorias.length) {
|
||||
const found = findInSubcats(s.subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Devuelve la ruta desde la raíz hasta el nodo con ese id.
|
||||
// Ej: para "morrales" en bolsos>hombre>morrales → [{id:bolsos}, {id:hombre}, {id:morrales}]
|
||||
function getCategoryPath(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 = getCategoryPath(c.subcategorias, id, next);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recolecta todos los ids de subcategorías dentro de un array de subcats.
|
||||
// Usado para validar duplicados al crear una nueva subcategoría.
|
||||
function collectSubcatIds(subcats, acc = new Set()) {
|
||||
if (!Array.isArray(subcats)) return acc;
|
||||
for (const s of subcats) {
|
||||
if (s.id) acc.add(s.id);
|
||||
if (Array.isArray(s.subcategorias)) collectSubcatIds(s.subcategorias, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Quita una subcategoría del árbol (modifica in place). Devuelve true si la encontró.
|
||||
function removeSubcatFromTree(categorias, id) {
|
||||
for (const c of categorias) {
|
||||
if (Array.isArray(c.subcategorias)) {
|
||||
const idx = c.subcategorias.findIndex(s => s.id === id);
|
||||
if (idx !== -1) {
|
||||
c.subcategorias.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
if (removeSubcatFromTree(c.subcategorias, id)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Busca una subcat por id en un array de subcats (cualquier nivel) y la devuelve, o null.
|
||||
function findSubcatRecursive(subcats, id) {
|
||||
if (!Array.isArray(subcats)) return null;
|
||||
for (const s of subcats) {
|
||||
if (s.id === id) return s;
|
||||
if (Array.isArray(s.subcategorias)) {
|
||||
const found = findSubcatRecursive(s.subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Quita una subcat de un array de subcats (cualquier nivel) y la devuelve. O null si no está.
|
||||
function removeSubcatRecursive(subcats, id) {
|
||||
if (!Array.isArray(subcats)) return null;
|
||||
for (let i = 0; i < subcats.length; i++) {
|
||||
if (subcats[i].id === id) {
|
||||
const [removed] = subcats.splice(i, 1);
|
||||
return removed;
|
||||
}
|
||||
if (Array.isArray(subcats[i].subcategorias)) {
|
||||
const found = removeSubcatRecursive(subcats[i].subcategorias, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureSinCategoria() {
|
||||
const categorias = await getCategorias();
|
||||
if (!categorias.some(c => c.id === SIN_CATEGORIA_ID)) {
|
||||
@@ -549,6 +645,131 @@ app.delete('/api/categorias/:id', requireCategoriasManagement, async (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// SUBCATEGORÍAS (anidadas, máx 3 niveles)
|
||||
// CRUD separado del de categorías para validaciones más finas.
|
||||
// No afecta productos todavía (esa integración viene en el siguiente paso).
|
||||
// ════════════════════════════════
|
||||
|
||||
// POST /api/categorias/:id/subcategorias → crear subcat dentro de :id
|
||||
// Body: { nombre, parentSubId? }
|
||||
// - Sin parentSubId: crea subcat nivel 1 (directo dentro de la categoría)
|
||||
// - Con parentSubId: crea sub-subcat (nivel 2) dentro de la subcat indicada
|
||||
app.post('/api/categorias/:id/subcategorias', requireCategoriasManagement, async (req, res) => {
|
||||
const parentId = req.params.id;
|
||||
const { nombre, parentSubId } = req.body || {};
|
||||
|
||||
if (!nombre || !String(nombre).trim() || String(nombre).length > 40) {
|
||||
return res.status(400).json({ error: 'Nombre inválido (1-40 caracteres)' });
|
||||
}
|
||||
if (parentId === SIN_CATEGORIA_ID) {
|
||||
return res.status(400).json({ error: 'No se pueden agregar subcategorías a "Sin categoría"' });
|
||||
}
|
||||
|
||||
try {
|
||||
const categorias = await getCategorias();
|
||||
const parent = categorias.find(c => c.id === parentId);
|
||||
if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });
|
||||
|
||||
// Determinar el array destino: subcategorías de la categoría, o de la sub-sub
|
||||
let targetArray;
|
||||
if (parentSubId) {
|
||||
// Creando sub-sub: el "padre" es una subcat existente
|
||||
const subParent = Array.isArray(parent.subcategorias)
|
||||
? parent.subcategorias.find(s => s.id === parentSubId)
|
||||
: null;
|
||||
if (!subParent) return res.status(404).json({ error: 'Subcategoría padre no encontrada' });
|
||||
if (!Array.isArray(subParent.subcategorias)) subParent.subcategorias = [];
|
||||
targetArray = subParent.subcategorias;
|
||||
} else {
|
||||
// Creando subcat nivel 1
|
||||
if (!Array.isArray(parent.subcategorias)) parent.subcategorias = [];
|
||||
targetArray = parent.subcategorias;
|
||||
}
|
||||
|
||||
// Límite de profundidad: máximo 3 niveles (cat > sub > sub-sub).
|
||||
// Si estamos creando nivel 1 y la categoría ya tiene sub-subcats (de otras ramas),
|
||||
// todavía está OK (cada rama puede tener sub-sub independientes).
|
||||
// Lo que NO permitimos es crear un nivel 3 (porque la URL no lo soporta y la UI no lo ofrece).
|
||||
|
||||
// Generar id (slug) y validar duplicado dentro del padre directo
|
||||
const slug = slugify(nombre);
|
||||
if (!slug) return res.status(400).json({ error: 'Nombre inválido' });
|
||||
|
||||
const idsHermanos = new Set(targetArray.map(s => s.id));
|
||||
if (idsHermanos.has(slug)) {
|
||||
return res.status(409).json({ error: `Ya existe una subcategoría "${slug}" aquí` });
|
||||
}
|
||||
|
||||
targetArray.push({ id: slug, nombre: String(nombre).trim() });
|
||||
await saveCategorias(categorias);
|
||||
|
||||
res.json({ ok: true, subcategoria: { id: slug, nombre: String(nombre).trim() } });
|
||||
} catch (err) {
|
||||
console.error('Create subcategory error:', err);
|
||||
res.status(500).json({ error: 'Error al crear subcategoría' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/categorias/:id/subcategorias/:subId → editar nombre (id inmutable)
|
||||
// Busca recursivamente en el árbol: :id es la categoría raíz, :subId puede estar en cualquier nivel.
|
||||
app.put('/api/categorias/:id/subcategorias/:subId', requireCategoriasManagement, async (req, res) => {
|
||||
const { id: parentId, subId } = req.params;
|
||||
const { nombre } = req.body || {};
|
||||
|
||||
if (!nombre || !String(nombre).trim() || String(nombre).length > 40) {
|
||||
return res.status(400).json({ error: 'Nombre inválido (1-40 caracteres)' });
|
||||
}
|
||||
|
||||
try {
|
||||
const categorias = await getCategorias();
|
||||
const parent = categorias.find(c => c.id === parentId);
|
||||
if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });
|
||||
|
||||
const sub = findSubcatRecursive(parent.subcategorias, subId);
|
||||
if (!sub) return res.status(404).json({ error: 'Subcategoría no encontrada' });
|
||||
|
||||
sub.nombre = String(nombre).trim();
|
||||
await saveCategorias(categorias);
|
||||
res.json({ ok: true, subcategoria: { id: sub.id, nombre: sub.nombre } });
|
||||
} catch (err) {
|
||||
console.error('Edit subcategory error:', err);
|
||||
res.status(500).json({ error: 'Error al editar subcategoría' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/categorias/:id/subcategorias/:subId → eliminar (mover productos al padre)
|
||||
// Solo se permite borrar hojas (subcats sin sub-subcats). Si tiene sub-subcats, debe borrarlas primero.
|
||||
app.delete('/api/categorias/:id/subcategorias/:subId', requireCategoriasManagement, async (req, res) => {
|
||||
const { id: parentId, subId } = req.params;
|
||||
|
||||
try {
|
||||
const categorias = await getCategorias();
|
||||
const parent = categorias.find(c => c.id === parentId);
|
||||
if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });
|
||||
|
||||
const sub = findSubcatRecursive(parent.subcategorias, subId);
|
||||
if (!sub) return res.status(404).json({ error: 'Subcategoría no encontrada' });
|
||||
|
||||
if (Array.isArray(sub.subcategorias) && sub.subcategorias.length) {
|
||||
return res.status(400).json({ error: 'Primero elimina las sub-subcategorías' });
|
||||
}
|
||||
|
||||
// Quitar del árbol (recursivo, encuentra el padre correcto y splice)
|
||||
removeSubcatRecursive(parent.subcategorias, subId);
|
||||
// Mantener array limpio: si la raíz quedó sin subcats, eliminamos la propiedad
|
||||
if (Array.isArray(parent.subcategorias) && parent.subcategorias.length === 0) {
|
||||
delete parent.subcategorias;
|
||||
}
|
||||
|
||||
await saveCategorias(categorias);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('Delete subcategory error:', err);
|
||||
res.status(500).json({ error: 'Error al eliminar subcategoría' });
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// GESTIÓN DE USUARIOS (admin o permiso 'usuarios')
|
||||
// ════════════════════════════════
|
||||
@@ -718,6 +939,7 @@ function parseFilename(name) {
|
||||
|
||||
app.post('/api/productos/upload', requireProductosManagement, upload.array('fotos', MAX_FILES), async (req, res) => {
|
||||
const categoria = (req.body.categoria || '').trim();
|
||||
const subRaw = req.body.sub || '[]';
|
||||
const files = req.files || [];
|
||||
|
||||
if (!categoria) {
|
||||
@@ -727,6 +949,15 @@ app.post('/api/productos/upload', requireProductosManagement, upload.array('foto
|
||||
return res.status(400).json({ error: 'No se enviaron fotos' });
|
||||
}
|
||||
|
||||
// Parsear la cadena de subcategorías (viene como JSON string desde el form data)
|
||||
let subPath;
|
||||
try {
|
||||
subPath = JSON.parse(subRaw);
|
||||
if (!Array.isArray(subPath)) throw new Error();
|
||||
} catch {
|
||||
return res.status(400).json({ error: 'Subcategoría inválida' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Verificar que la categoría existe y NO es sin-categoria
|
||||
const categorias = await getCategorias();
|
||||
@@ -738,8 +969,46 @@ app.post('/api/productos/upload', requireProductosManagement, upload.array('foto
|
||||
return res.status(400).json({ error: 'No se puede subir productos a la categoría del sistema' });
|
||||
}
|
||||
|
||||
// Crear carpeta destino si no existe
|
||||
const dstDir = path.join(IMG_DIR, categoria);
|
||||
// Validar la cadena de subcats contra el árbol:
|
||||
// - La categoría tiene subcats → subPath DEBE tener al menos 1 elemento (la hoja final)
|
||||
// - La categoría NO tiene subcats → subPath debe estar vacío
|
||||
// - Cada sub debe existir en su nivel y el último debe ser HOJA (sin sub-subs)
|
||||
const catTieneSubcats = Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0;
|
||||
|
||||
if (catTieneSubcats && subPath.length === 0) {
|
||||
return res.status(400).json({ error: `"${cat.nombre}" tiene subcategorías, debes elegir una hoja` });
|
||||
}
|
||||
if (!catTieneSubcats && subPath.length > 0) {
|
||||
return res.status(400).json({ error: `"${cat.nombre}" no tiene subcategorías` });
|
||||
}
|
||||
|
||||
// Caminar el árbol validando cada nivel
|
||||
let nivelActual = cat.subcategorias || [];
|
||||
for (let i = 0; i < subPath.length; i++) {
|
||||
const id = subPath[i];
|
||||
const nodo = nivelActual.find(s => s.id === id);
|
||||
if (!nodo) {
|
||||
return res.status(400).json({ error: `Subcategoría "${id}" no encontrada en nivel ${i + 1}` });
|
||||
}
|
||||
const esUltimo = i === subPath.length - 1;
|
||||
if (esUltimo) {
|
||||
// La hoja final NO debe tener sub-subcategorías (solo las hojas reciben productos)
|
||||
if (Array.isArray(nodo.subcategorias) && nodo.subcategorias.length > 0) {
|
||||
return res.status(400).json({ error: `"${nodo.nombre}" tiene sub-subcategorías, debes elegir una de ellas` });
|
||||
}
|
||||
} else {
|
||||
// No es el último → debe tener subcategorías para seguir bajando
|
||||
if (!Array.isArray(nodo.subcategorias) || nodo.subcategorias.length === 0) {
|
||||
return res.status(400).json({ error: `"${nodo.nombre}" no tiene sub-subcategorías` });
|
||||
}
|
||||
nivelActual = nodo.subcategorias;
|
||||
}
|
||||
}
|
||||
|
||||
// Construir el path destino: <cat> o <cat>/<sub1> o <cat>/<sub1>/<sub2>
|
||||
const pathParts = [categoria, ...subPath];
|
||||
const relDir = pathParts.join('/');
|
||||
const dstDir = path.join(IMG_DIR, relDir);
|
||||
await fs.mkdir(dstDir, { recursive: true });
|
||||
|
||||
// Cargar productos existentes
|
||||
@@ -782,10 +1051,11 @@ app.post('/api/productos/upload', requireProductosManagement, upload.array('foto
|
||||
.toFile(outPath);
|
||||
|
||||
productos.push({
|
||||
id: ref,
|
||||
ref: ref,
|
||||
cat: categoria,
|
||||
img: `${categoria}/${outName}`
|
||||
id: ref,
|
||||
ref: ref,
|
||||
cat: categoria,
|
||||
sub: subPath,
|
||||
img: `${relDir}/${outName}`
|
||||
});
|
||||
creados.push({ ref });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user