diff --git a/dripzone-app/src/app/admin/error.tsx b/dripzone-app/src/app/admin/error.tsx new file mode 100644 index 0000000..ae69163 --- /dev/null +++ b/dripzone-app/src/app/admin/error.tsx @@ -0,0 +1,100 @@ +'use client'; + +import React, { useEffect } from 'react'; +import Link from 'next/link'; +import { AlertTriangle, RefreshCw, ArrowLeft, LayoutDashboard, Sparkles } from 'lucide-react'; + +export default function AdminError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('[AdminError Boundary]:', error); + + // Si es un ChunkLoadError por un despliegue reciente en Coolify, recargar automáticamente + const isChunkError = + error.message?.includes('Loading chunk') || + error.message?.includes('ChunkLoadError') || + error.message?.includes('Failed to fetch dynamically imported module'); + + if (isChunkError) { + const lastReload = sessionStorage.getItem('dripzone_admin_error_reload'); + const now = Date.now(); + if (!lastReload || now - parseInt(lastReload, 10) > 10000) { + sessionStorage.setItem('dripzone_admin_error_reload', now.toString()); + window.location.reload(); + } + } + }, [error]); + + const handleHardReload = () => { + try { + sessionStorage.removeItem('dripzone_admin_error_reload'); + // Forzar recarga sin caché + window.location.href = window.location.pathname + '?v=' + Date.now(); + } catch (_) { + window.location.reload(); + } + }; + + return ( +
+
+
+ +
+ +
+ +
+

+ Sincronización del Catálogo +

+

+ Se ha actualizado el sistema en producción o hubo una desconexión temporal de módulos. + Haz clic en recargar para actualizar tu navegador con la última versión del catálogo. +

+
+ + {error.message && ( +
+

+ {error.message} +

+
+ )} + +
+ + +
+ + + + + Dashboard Admin + +
+
+
+
+ ); +} diff --git a/dripzone-app/src/app/admin/productos/page.tsx b/dripzone-app/src/app/admin/productos/page.tsx index 81ffd91..e82efbe 100644 --- a/dripzone-app/src/app/admin/productos/page.tsx +++ b/dripzone-app/src/app/admin/productos/page.tsx @@ -16,7 +16,7 @@ import { generateAttractiveProductCopy } from '@/lib/attractive-copy'; import { WhatsAppOrderLinkModal } from '@/components/admin/WhatsAppOrderLinkModal'; import { getProductPublicationDate, getProductAgeInDays, isProductOlderThanDays } from '@/lib/expiration-service'; import { FormattedDescription } from '@/components/store/FormattedDescription'; -import { getCategoryIcon, normalizeCategoryName } from '@/lib/category-helper'; +import { getCategoryIcon, normalizeCategoryName, matchCategory } from '@/lib/category-helper'; export default function AdminProductsPage() { const [products, setProducts] = useState(INITIAL_PRODUCTS); @@ -470,12 +470,14 @@ export default function AdminProductsPage() { // Dynamic lists for filters const availableBrands = useMemo(() => { - const brands = Array.from(new Set(products.map((p) => p.brand).filter(Boolean))); + if (!Array.isArray(products)) return []; + const brands = Array.from(new Set(products.map((p) => p?.brand).filter(Boolean))); return brands.sort(); }, [products]); const availableCategories = useMemo(() => { - const cats = Array.from(new Set(products.map((p) => p.category).filter(Boolean))); + if (!Array.isArray(products)) return []; + const cats = Array.from(new Set(products.map((p) => p?.category).filter(Boolean))); return cats.sort(); }, [products]); @@ -489,7 +491,9 @@ export default function AdminProductsPage() { let expired = 0; let manuallyKept = 0; - for (const p of products) { + const list = Array.isArray(products) ? products : []; + for (const p of list) { + if (!p) continue; const isOld = isProductOlderThanDays(p, 7); const isActive = p.isActive !== false; if (isActive) { @@ -503,7 +507,7 @@ export default function AdminProductsPage() { } return { - all: products.length, + all: list.length, active, inactive, recent, @@ -513,8 +517,10 @@ export default function AdminProductsPage() { }, [products]); const filtered = useMemo(() => { + if (!Array.isArray(products)) return []; return products .filter((p) => { + if (!p) return false; const pubDate = getProductPublicationDate(p); const isOld = isProductOlderThanDays(p, 7); const isActive = p.isActive !== false; @@ -522,13 +528,14 @@ export default function AdminProductsPage() { // 1. Filtro de búsqueda por texto if (search.trim()) { const q = search.toLowerCase(); + const pSizes = Array.isArray(p.sizes) ? p.sizes : []; const matchesSearch = - p.name.toLowerCase().includes(q) || - p.brand.toLowerCase().includes(q) || + (p.name || '').toLowerCase().includes(q) || + (p.brand || '').toLowerCase().includes(q) || (p.supplierName && p.supplierName.toLowerCase().includes(q)) || (p.category && p.category.toLowerCase().includes(q)) || (p.whatsappMessageId && p.whatsappMessageId.includes(q)) || - p.sizes.some((s) => s.includes(q)); + pSizes.some((s) => typeof s === 'string' && s.toLowerCase().includes(q)); if (!matchesSearch) return false; } @@ -552,17 +559,17 @@ export default function AdminProductsPage() { if (supplierFilter !== 'all' && p.supplierName !== supplierFilter) return false; // 4. Filtro por Marca - if (brandFilter !== 'all' && p.brand.toLowerCase() !== brandFilter.toLowerCase()) return false; + if (brandFilter !== 'all' && (p.brand || '').toLowerCase() !== brandFilter.toLowerCase()) return false; // 5. Filtro por Talla - if (sizeFilter !== 'all' && !p.sizes.includes(sizeFilter)) return false; + if (sizeFilter !== 'all' && (!Array.isArray(p.sizes) || !p.sizes.includes(sizeFilter))) return false; // 6. Filtro por Stock if (stockFilter === 'in_stock' && p.inStock === false) return false; if (stockFilter === 'out_of_stock' && p.inStock !== false) return false; // 7. Filtro por Categoría - if (categoryFilter !== 'all' && (!p.category || !p.category.toLowerCase().includes(categoryFilter.toLowerCase()))) { + if (categoryFilter !== 'all' && !matchCategory(p.category, categoryFilter)) { return false; } @@ -571,15 +578,15 @@ export default function AdminProductsPage() { .sort((a, b) => { const dateA = getProductPublicationDate(a).getTime(); const dateB = getProductPublicationDate(b).getTime(); - const marginA = a.retailPrice - a.supplierPrice; - const marginB = b.retailPrice - b.supplierPrice; + const marginA = (a.retailPrice || 0) - (a.supplierPrice || 0); + const marginB = (b.retailPrice || 0) - (b.supplierPrice || 0); if (sortBy === 'newest') return dateB - dateA; if (sortBy === 'oldest') return dateA - dateB; if (sortBy === 'margin_desc') return marginB - marginA; - if (sortBy === 'price_desc') return b.retailPrice - a.retailPrice; - if (sortBy === 'price_asc') return a.retailPrice - b.retailPrice; - if (sortBy === 'cost_desc') return b.supplierPrice - a.supplierPrice; + if (sortBy === 'price_desc') return (b.retailPrice || 0) - (a.retailPrice || 0); + if (sortBy === 'price_asc') return (a.retailPrice || 0) - (b.retailPrice || 0); + if (sortBy === 'cost_desc') return (b.supplierPrice || 0) - (a.supplierPrice || 0); return 0; }); }, [ @@ -937,9 +944,9 @@ export default function AdminProductsPage() { onChange={(e) => setCategoryFilter(e.target.value)} className="w-full bg-neutral-900 border border-purple-500/30 rounded-xl px-2.5 py-2 text-xs text-purple-300 font-bold outline-hidden focus:border-purple-400 cursor-pointer" > - + {availableCategories.map((c) => { - const count = products.filter((p) => (p.category || '').toLowerCase() === c.toLowerCase()).length; + const count = (products || []).filter((p) => matchCategory(p?.category, c)).length; return (