diff --git a/backlog/features.json b/backlog/features.json index f4868bc..944516d 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -2801,6 +2801,44 @@ "close": true }, "completed_at": "2026-08-19T05:34:15Z" + }, + { + "id": "F-050", + "type": "fix", + "title": "Backoffice admin layout actions categories dashboard 404", + "problem": "Several admin pages have stale layout, text-based actions, missing parent selector, and 404s on dashboard", + "goal": "Layout uses full available width, action column uses icon, category parent can be selected and promoted, dashboard stats load without 404", + "scope_in": [ + "admin pages layout", + "action icons", + "category parent selector", + "dashboard stats", + "404 sources" + ], + "scope_out": [ + "no schema change in API", + "no product feature expansion" + ], + "priority": "high", + "risk": "low", + "description": "Problem: Several admin pages have stale layout, text-based actions, missing parent selector, and 404s on dashboard. Goal: Layout uses full available width, action column uses icon, category parent can be selected and promoted, dashboard stats load without 404. Scope IN: admin pages layout, action icons, category parent selector, dashboard stats, 404 sources. Scope OUT: no schema change in API, no product feature expansion. Type: fix. Priority: high. Risk: low.", + "acceptance": [ + "/settings and /products edit sheet use full layout width", + "/tax-rates /shipping /brands action column uses icon+title pattern", + "/categories edit form allows parent selection and promote-to-parent toggle", + "Admin dashboard renders stats without 404", + "No new 404s in admin navigation", + "verify.sh is green" + ], + "status": "done", + "created_at": "2026-08-19", + "gates": { + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-19T05:54:36Z" } ] } diff --git a/project/apps/admin/src/app/(dashboard)/audit/page.tsx b/project/apps/admin/src/app/(dashboard)/audit/page.tsx index ac6926e..636423b 100644 --- a/project/apps/admin/src/app/(dashboard)/audit/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/audit/page.tsx @@ -50,7 +50,7 @@ export default function AuditLogPage() { useEffect(() => { load(); }, [load]); return ( -
+

Log de auditoría

diff --git a/project/apps/admin/src/app/(dashboard)/brands/page.tsx b/project/apps/admin/src/app/(dashboard)/brands/page.tsx index 09d420f..ad0d737 100644 --- a/project/apps/admin/src/app/(dashboard)/brands/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/brands/page.tsx @@ -113,7 +113,7 @@ export default function BrandsPage() { }; return ( -

+

Marcas

@@ -211,7 +211,7 @@ export default function BrandsPage() {
{loading ?
Cargando...
: - error ?
{error}
: + error ?
{error}
: brands.length === 0 ?
No hay marcas
: diff --git a/project/apps/admin/src/app/(dashboard)/categories/page.tsx b/project/apps/admin/src/app/(dashboard)/categories/page.tsx index d9ac880..5462552 100644 --- a/project/apps/admin/src/app/(dashboard)/categories/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/categories/page.tsx @@ -103,14 +103,27 @@ export default function CategoriesPage() { useEffect(() => { load(); }, [load]); - // Listado plano de categorías que son PARENT (contenedores) y pueden tener hijos. - // Excluye la propia categoría que se está editando para evitar auto-anidado. - const parentOptions = useMemo(() => { - const flat = (cats: Category[]): Category[] => - cats.flatMap((c) => [c, ...flat(c.children ?? [])]); - return flat(tree) - .filter((c) => c.isParent && (!editing || c.id !== editing.id)); - }, [tree, editing]); + // Listado plano de categorías, editable: si se está editando, una categoría puede + // pasar a ser parent aunque no lo fuese antes. Mantenemos al usuario fuera de su + // propio ID y fuera de sus descendientes para no producir ciclos. + const allFlat = useMemo(() => { + const flat = (cats: Category[]): Category[] => cats.flatMap((c) => [c, ...flat(c.children ?? [])]); + return flat(tree); + }, [tree]); + + const excludedIds = useMemo(() => { + if (!editing) return new Set(); + const descendants = (id: string, cats: Category[] = allFlat): Set => { + const out = new Set([id]); + for (const c of cats) if (c.parentId === id) for (const d of descendants(c.id, cats)) out.add(d); + return out; + }; + return descendants(editing.id); + }, [editing, allFlat]); + + // Cualquier categoría puede ser padre: las parent son contenedores, las child + // se promocionan en el momento de guardarlas con isParent=true. + const parentOptions = allFlat.filter((c) => !excludedIds.has(c.id)); const openCreate = () => { setEditing(null); @@ -187,7 +200,7 @@ export default function CategoriesPage() { cats.flatMap((c) => [c, ...flat(c.children ?? [])]); return ( -
+

Categorías

{/* FIX-19: tipo parent/child + categoría padre */} -
+

- Solo se listan las categorías marcadas como parent (contenedor). + Cualquier categoría (parent o child) puede ser padre; si la elegida aún no lo es, promuévela abajo.

@@ -251,12 +266,12 @@ export default function CategoriesPage() { setForm((f) => ({ ...f, isParent: e.target.checked }))} className="rounded text-[#2D6A4F] focus:ring-[#2D6A4F]" /> - 📂 Es categoría parent (contenedor) + 📂 Promover a categoría parent (contenedor)

- Una categoría child es una hoja: no puede contener hijos. Si necesita tener subcategorías, márquela como parent. + Una categoría child es una hoja: no puede contener hijos. Marque la casilla para promocionarla; desmárquela para dejarla como hoja.

{/* Descripción */} diff --git a/project/apps/admin/src/app/(dashboard)/cms/page.tsx b/project/apps/admin/src/app/(dashboard)/cms/page.tsx index 9d7e3fc..252ab0b 100644 --- a/project/apps/admin/src/app/(dashboard)/cms/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/cms/page.tsx @@ -73,7 +73,7 @@ export default function CmsPage() { }; return ( -
+

Páginas CMS

@@ -103,7 +103,7 @@ export default function CmsPage() {
{loading ?
Cargando...
: - error ?
{error}
: + error ?
{error}
: items.length === 0 ?
No hay páginas
: items.map(p => (
diff --git a/project/apps/admin/src/app/(dashboard)/customers/[id]/page.tsx b/project/apps/admin/src/app/(dashboard)/customers/[id]/page.tsx index 3fda2c4..45b9ace 100644 --- a/project/apps/admin/src/app/(dashboard)/customers/[id]/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/customers/[id]/page.tsx @@ -211,11 +211,11 @@ export default function CustomerDetailPage() { } }; - if (loading) return
Cargando...
; - if (error || !customer) return
{error || 'No encontrado'}
; + if (loading) return
Cargando...
; + if (error || !customer) return
{error || 'No encontrado'}
; return ( -
+

{customer.email}

diff --git a/project/apps/admin/src/app/(dashboard)/customers/page.tsx b/project/apps/admin/src/app/(dashboard)/customers/page.tsx index fb10395..40b8536 100644 --- a/project/apps/admin/src/app/(dashboard)/customers/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/customers/page.tsx @@ -124,7 +124,7 @@ export default function CustomersPage() { const handleCreated = () => { setMsg('Cliente creado correctamente'); setTimeout(() => setMsg(''), 3000); load(); }; return ( -
+
{/* Header */}
@@ -161,7 +161,7 @@ export default function CustomersPage() { Cargando...
) : error ? ( -
+

{error}

diff --git a/project/apps/admin/src/app/(dashboard)/inventory/page.tsx b/project/apps/admin/src/app/(dashboard)/inventory/page.tsx index 68d62c2..9113554 100644 --- a/project/apps/admin/src/app/(dashboard)/inventory/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/inventory/page.tsx @@ -120,7 +120,7 @@ export default function InventoryPage() { const outOfStockCount = rows.filter((r) => (r.stock?.availableQuantity ?? 0) === 0).length; return ( -
+
{/* Header */}
@@ -190,7 +190,7 @@ export default function InventoryPage() { Cargando inventario...
) : error ? ( -
+

{error}

diff --git a/project/apps/admin/src/app/(dashboard)/layout.tsx b/project/apps/admin/src/app/(dashboard)/layout.tsx index 98c1600..289f8b6 100644 --- a/project/apps/admin/src/app/(dashboard)/layout.tsx +++ b/project/apps/admin/src/app/(dashboard)/layout.tsx @@ -103,7 +103,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
-
+
{children}
diff --git a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx index 138b94f..3ac7ecc 100644 --- a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx @@ -98,7 +98,7 @@ export default function OrderDetailPage() { if (loading) { return ( -
+
Cargando...
); @@ -106,7 +106,7 @@ export default function OrderDetailPage() { if (error || !order) { return ( -
+

{error || 'Pedido no encontrado'}

@@ -107,7 +107,7 @@ export default function PromotionsPage() {
{loading ?
Cargando...
: - error ?
{error}
: + error ?
{error}
: items.length === 0 ?
No hay promociones
:
diff --git a/project/apps/admin/src/app/(dashboard)/reviews/page.tsx b/project/apps/admin/src/app/(dashboard)/reviews/page.tsx index dd9a2eb..7322603 100644 --- a/project/apps/admin/src/app/(dashboard)/reviews/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reviews/page.tsx @@ -49,7 +49,7 @@ export default function ReviewsPage() { }; return ( -
+

Reseñas

{total} reseñas pendientes de moderación

@@ -63,7 +63,7 @@ export default function ReviewsPage() {
{loading ?
Cargando...
: - error ?
{error}
: + error ?
{error}
: items.length === 0 ?
No hay reseñas
: items.map(r => (
diff --git a/project/apps/admin/src/app/(dashboard)/settings/page.tsx b/project/apps/admin/src/app/(dashboard)/settings/page.tsx index 1ecc2d2..efe6058 100644 --- a/project/apps/admin/src/app/(dashboard)/settings/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/settings/page.tsx @@ -60,7 +60,7 @@ export default function SettingsPage() { ); return ( -
+

Ajustes de tienda

Configuración general de la tienda visible para los clientes.

diff --git a/project/apps/admin/src/app/(dashboard)/shipping/page.tsx b/project/apps/admin/src/app/(dashboard)/shipping/page.tsx index 78f4fc4..b61ca37 100644 --- a/project/apps/admin/src/app/(dashboard)/shipping/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/shipping/page.tsx @@ -190,7 +190,7 @@ export default function ShippingPage() { }; return ( -
+

Envíos

{msg && {msg}} diff --git a/project/apps/admin/src/app/(dashboard)/tax-rates/page.tsx b/project/apps/admin/src/app/(dashboard)/tax-rates/page.tsx index ad10508..2e5ef80 100644 --- a/project/apps/admin/src/app/(dashboard)/tax-rates/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/tax-rates/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; import { taxApi, type TaxRate } from '@/lib/api-client'; +import { RowActions } from '@/components/ui/RowActions'; export default function TaxRatesPage() { const [rates, setRates] = useState([]); @@ -37,7 +38,7 @@ export default function TaxRatesPage() { const fmt = (r: TaxRate) => `${r.ratePercent}%`; return ( -
+

Tipos impositivos (IVA)

Configura los tipos de IVA aplicables a los productos.

@@ -104,10 +105,18 @@ export default function TaxRatesPage() {
)} diff --git a/project/apps/admin/src/app/(dashboard)/users/page.tsx b/project/apps/admin/src/app/(dashboard)/users/page.tsx index 8164691..3702753 100644 --- a/project/apps/admin/src/app/(dashboard)/users/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/users/page.tsx @@ -125,7 +125,7 @@ export default function AdminUsersPage() { }; return ( -
+

Usuarios backoffice

{total > 0 ? `${total} usuario${total !== 1 ? 's' : ''}` : ''}

diff --git a/project/apps/admin/src/features/products/components/ProductEditor.tsx b/project/apps/admin/src/features/products/components/ProductEditor.tsx index 1522bac..2c22669 100644 --- a/project/apps/admin/src/features/products/components/ProductEditor.tsx +++ b/project/apps/admin/src/features/products/components/ProductEditor.tsx @@ -168,8 +168,8 @@ export function ProductEditor({ productId }: ProductEditorProps) { ); return ( -
-
+
+

{isCreate ? 'Nuevo producto' : `Editar: ${name}`}

@@ -184,7 +184,7 @@ export function ProductEditor({ productId }: ProductEditorProps) { {success &&
{success}
} {/* Tabs */} -
+
{(['general', 'pricing', 'inventory', 'images', 'seo', 'publish'] as const).map(t => (
- + {editing === r.id ? ( +
+ + +
+ ) : ( + startEdit(r)} editTitle="Editar tipo" /> + )}