From 7028a5b39721086264bfcb85695cf277464ae060 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sun, 20 Sep 2026 14:31:28 -0500 Subject: [PATCH] fix(admin): resolve node:net error by decoupling date utils & brand detection from server libraries and add client webpack fallbacks --- dripzone-app/next.config.ts | 17 ++ dripzone-app/src/app/admin/productos/page.tsx | 39 ++- dripzone-app/src/lib/brand-detection.ts | 255 ++++++++++++++++++ dripzone-app/src/lib/empty-module.js | 1 + dripzone-app/src/lib/expiration-service.ts | 56 +--- dripzone-app/src/lib/product-date-utils.ts | 45 ++++ 6 files changed, 362 insertions(+), 51 deletions(-) create mode 100644 dripzone-app/src/lib/brand-detection.ts create mode 100644 dripzone-app/src/lib/empty-module.js create mode 100644 dripzone-app/src/lib/product-date-utils.ts diff --git a/dripzone-app/next.config.ts b/dripzone-app/next.config.ts index f368b08..567aa7b 100644 --- a/dripzone-app/next.config.ts +++ b/dripzone-app/next.config.ts @@ -2,6 +2,23 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + serverExternalPackages: ["node-appwrite", "@google/genai"], + turbopack: {}, + webpack: (config, { isServer }) => { + if (!isServer) { + config.resolve = config.resolve || {}; + config.resolve.fallback = { + ...(config.resolve.fallback || {}), + net: false, + tls: false, + fs: false, + child_process: false, + dns: false, + http2: false, + }; + } + return config; + }, images: { remotePatterns: [ { protocol: "https", hostname: "**" }, diff --git a/dripzone-app/src/app/admin/productos/page.tsx b/dripzone-app/src/app/admin/productos/page.tsx index e82efbe..3a02cf0 100644 --- a/dripzone-app/src/app/admin/productos/page.tsx +++ b/dripzone-app/src/app/admin/productos/page.tsx @@ -4,23 +4,52 @@ import React, { useState, useEffect, useMemo } from 'react'; import Link from 'next/link'; import { Package, Plus, Eye, EyeOff, Check, X, Sparkles, Filter, Phone, MessageCircle, Truck, Trash2, Edit, Wand2, ArrowRight, Layers, Palette, CheckSquare, Square, MinusSquare, Split, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Tag, AlertCircle, CheckCircle2, Clock, RefreshCw, Zap, ShieldCheck, ArrowUpDown, History } from 'lucide-react'; import { Product, Supplier } from '@/types'; -import { INITIAL_PRODUCTS, INITIAL_SUPPLIERS } from '@/lib/data-service'; import { formatCOP } from '@/lib/pricing-engine'; import { GroupProductsModal } from '@/components/admin/GroupProductsModal'; import { ManageVariantsModal } from '@/components/admin/ManageVariantsModal'; import { renderClientTemplate, initClientTemplates } from '@/lib/templates-client'; import { OptimizedImage } from '@/components/ui/OptimizedImage'; import { sanitizePublicationTitle, sanitizePublicationText } from '@/lib/sanitizer'; -import { cleanProductName, detectBrandFromText } from '@/lib/gemini-parser'; +import { cleanProductName, detectBrandFromText } from '@/lib/brand-detection'; import { generateAttractiveProductCopy } from '@/lib/attractive-copy'; import { WhatsAppOrderLinkModal } from '@/components/admin/WhatsAppOrderLinkModal'; -import { getProductPublicationDate, getProductAgeInDays, isProductOlderThanDays } from '@/lib/expiration-service'; +import { getProductPublicationDate, getProductAgeInDays, isProductOlderThanDays } from '@/lib/product-date-utils'; import { FormattedDescription } from '@/components/store/FormattedDescription'; import { getCategoryIcon, normalizeCategoryName, matchCategory } from '@/lib/category-helper'; +const CLIENT_INITIAL_SUPPLIERS: Supplier[] = [ + { + id: "sup-jovic", + name: "馃拵 2锔忊儯 DIFUSI脫N JOVIC STORE COLOMBIA馃崁", + phone: "573167455877", + contactPerson: "Asesor Jovic Store", + address: "Centro / Bodega Cali", + notes: "Proveedor oficial Nike, Jordan, Nocta y Vomero calidades Top.", + isActive: true + }, + { + id: "sup-ccmio", + name: "EXCLUSIVE CC M脥O 3B", + phone: "573159876543", + contactPerson: "Despachos CC M脥O 3B", + address: "CC M脥O Local 3B, Cali", + notes: "Especialista en Adidas, Coach, Timberland y Skatewear.", + isActive: true + }, + { + id: "sup-central", + name: "BODEGA CENTRAL MAYORISTA", + phone: "573101234567", + contactPerson: "Log铆stica Despachos", + address: "San Andresito / Cali", + notes: "Despacho r谩pido en 24 horas para separaci贸n inmediata.", + isActive: true + } +]; + export default function AdminProductsPage() { - const [products, setProducts] = useState(INITIAL_PRODUCTS); - const [suppliers, setSuppliers] = useState(INITIAL_SUPPLIERS); + const [products, setProducts] = useState([]); + const [suppliers, setSuppliers] = useState(CLIENT_INITIAL_SUPPLIERS); const [search, setSearch] = useState(''); // Filtros Avanzados para Administradores diff --git a/dripzone-app/src/lib/brand-detection.ts b/dripzone-app/src/lib/brand-detection.ts new file mode 100644 index 0000000..c9edc3a --- /dev/null +++ b/dripzone-app/src/lib/brand-detection.ts @@ -0,0 +1,255 @@ +import { sanitizePublicationTitle } from './sanitizer'; + +export interface BrandSignature { + brand: string; + aliases: RegExp[]; + models: { name: string; pattern: RegExp }[]; +} + +export const BRAND_SIGNATURES: BrandSignature[] = [ + { + brand: 'Adidas', + aliases: [/\badidas\b/i, /\btrefoil\b/i, /\btr[e茅]bol\b/i, /3\s*rayas|tres\s*rayas/i], + models: [ + { name: 'Adidas Samba OG', pattern: /\bsamba\b/i }, + { name: 'Adidas Gazelle Indoor', pattern: /\bgazelle\b/i }, + { name: 'Adidas Campus 00s', pattern: /\bcampus\b/i }, + { name: 'Adidas Handball Spezial', pattern: /\bspezial\b/i }, + { name: 'Adidas Forum Low', pattern: /\bforum\b/i }, + { name: 'Adidas Bad Bunny', pattern: /\bbad\s*bunny\b/i }, + { name: 'Adidas Stan Smith', pattern: /\bstan\s*smith\b/i }, + { name: 'Adidas Superstar', pattern: /\bsuperstar\b/i }, + { name: 'Adidas SL 72', pattern: /\bsl\s*72\b|\bsl72\b/i }, + { name: 'Adidas Response CL', pattern: /\bresponse\s*cl\b/i }, + { name: 'Adidas Yeezy Boost 350', pattern: /\byeezy\b|\b350\s*v2\b|\bfoam\s*runner\b/i }, + { name: 'Adidas Astir', pattern: /\bastir\b/i }, + { name: 'Adidas Ozelia', pattern: /\bozelia\b/i }, + { name: 'Adidas Adizero', pattern: /\badizero\b/i }, + { name: 'Adidas Country OG', pattern: /\bcountry\s*og\b/i }, + { name: 'Adidas NMD R1', pattern: /\bnmd\b/i }, + { name: 'Adidas Ultraboost', pattern: /\bultra\s*boost\b|\bultraboost\b/i }, + ], + }, + { + brand: 'Jordan', + aliases: [/\bjordan\b/i, /\bjumpman\b/i, /\bair\s*jordan\b/i, /\baj\d+\b/i], + models: [ + { name: 'Jordan 4 Retro', pattern: /\b(?:retro\s*4|aj4|jordan\s*4|retro\s*iv)\b/i }, + { name: 'Jordan 1 Retro High OG', pattern: /\b(?:retro\s*1|aj1|jordan\s*1|retro\s*i)\b/i }, + { name: 'Jordan 3 Retro', pattern: /\b(?:retro\s*3|aj3|jordan\s*3|retro\s*iii)\b/i }, + { name: 'Jordan 11 Retro', pattern: /\b(?:retro\s*11|aj11|jordan\s*11|retro\s*xi)\b/i }, + { name: 'Jordan 1 Low Travis Scott', pattern: /\btravis\s*scott\b|\bcactus\s*jack\b/i }, + { name: 'Jordan Spizike Low', pattern: /\bspizike\b/i }, + { name: 'Jordan 5 Retro', pattern: /\b(?:retro\s*5|aj5|jordan\s*5)\b/i }, + { name: 'Jordan 6 Retro', pattern: /\b(?:retro\s*6|aj6|jordan\s*6)\b/i }, + ], + }, + { + brand: 'New Balance', + aliases: [/\bnew\s*balance\b/i, /\bnb\b/i], + models: [ + { name: 'New Balance 9060', pattern: /\b9060\b/i }, + { name: 'New Balance 550', pattern: /\b550\b/i }, + { name: 'New Balance 530', pattern: /\b530\b/i }, + { name: 'New Balance 1906R', pattern: /\b1906\b|\b1906r\b|\b1906d\b/i }, + { name: 'New Balance 2002R', pattern: /\b2002\b|\b2002r\b|\bprotection\s*pack\b/i }, + { name: 'New Balance 327', pattern: /\b327\b/i }, + { name: 'New Balance 574', pattern: /\b574\b/i }, + { name: 'New Balance 990v6', pattern: /\b990\b|\b990v\d\b/i }, + { name: 'New Balance 991', pattern: /\b991\b/i }, + { name: 'New Balance 992', pattern: /\b992\b/i }, + { name: 'New Balance 993', pattern: /\b993\b/i }, + { name: 'New Balance 610', pattern: /\b610\b/i }, + { name: 'New Balance CT302', pattern: /\bct302\b/i }, + ], + }, + { + brand: 'On Running', + aliases: [/\bon\s*running\b/i, /\bon\s*cloud\b/i, /\bcloud\b/i, /\bclould\b/i], + models: [ + { name: 'On Cloudmonster', pattern: /\bcloudmonster\b|\bcloud\s*monster\b/i }, + { name: 'On Cloudtilt Loewe', pattern: /\bcloudtilt\b|\bcloud\s*tilt\b|\bloewe\b/i }, + { name: 'On Cloud 5', pattern: /\bcloud\s*5\b/i }, + { name: 'On Cloud X3', pattern: /\bcloud\s*x\b|\bcloud\s*x3\b/i }, + { name: 'On Cloudsurfer', pattern: /\bcloudsurfer\b|\bcloud\s*surfer\b/i }, + { name: 'On Cloudstratus', pattern: /\bcloudstratus\b|\bcloud\s*stratus\b/i }, + { name: 'On Cloudrunner', pattern: /\bcloudrunner\b|\bcloud\s*runner\b/i }, + { name: 'On The Roger Centre Court', pattern: /\broger\b|\bthe\s*roger\b/i }, + ], + }, + { + brand: 'Puma', + aliases: [/\bpuma\b/i], + models: [ + { name: 'Puma Palermo Special', pattern: /\bpalermo\b/i }, + { name: 'Puma Speedcat OG', pattern: /\bspeedcat\b|\bspeed\s*cat\b/i }, + { name: 'Puma Suede Classic', pattern: /\bsuede\b/i }, + { name: 'Puma Caven 2.0', pattern: /\bcaven\b/i }, + { name: 'Puma Slipstream', pattern: /\bslipstream\b/i }, + { name: 'Puma Mayze Wedge', pattern: /\bmayze\b/i }, + { name: 'Puma Easy Rider', pattern: /\beasy\s*rider\b/i }, + { name: 'Puma RS-X Efekt', pattern: /\brs-x\b|\brsx\b/i }, + { name: 'Puma CA Pro Classic', pattern: /\bca\s*pro\b/i }, + ], + }, + { + brand: 'Asics', + aliases: [/\basics\b/i], + models: [ + { name: 'Asics Gel-Kayano 14', pattern: /\bgel[\s-]*kayano\b|\bkayano\b/i }, + { name: 'Asics Gel-NYC', pattern: /\bgel[\s-]*nyc\b|\bnyc\b/i }, + { name: 'Asics Gel-1130', pattern: /\bgel[\s-]*1130\b|\b1130\b/i }, + { name: 'Asics GT-2160', pattern: /\bgt[\s-]*2160\b|\b2160\b/i }, + { name: 'Asics Gel-Quantum 360', pattern: /\bgel[\s-]*quantum\b|\bquantum\b/i }, + { name: 'Asics Gel-Nimbus 9', pattern: /\bgel[\s-]*nimbus\b|\bnimbus\b/i }, + { name: 'Asics Gel-Lyte III', pattern: /\bgel[\s-]*lyte\b/i }, + { name: 'Asics Novablast 4', pattern: /\bnovablast\b/i }, + ], + }, + { + brand: 'Nike', + aliases: [/\bnike\b/i, /\bswoosh\b/i], + models: [ + { name: 'Nike Air Force 1 07', pattern: /\bair\s*force\b|\baf1\b|\baf-1\b|\bforce\s*1\b/i }, + { name: 'Nike Dunk Low Retro Panda', pattern: /\bdunk\b|\bpanda\b|\bsb\s*dunk\b/i }, + { name: 'Nike Air Max 90', pattern: /\bair\s*max\s*90\b|\bam90\b|\bmax\s*90\b/i }, + { name: 'Nike Air Max 97', pattern: /\bair\s*max\s*97\b|\bam97\b|\bmax\s*97\b/i }, + { name: 'Nike Air Max 270', pattern: /\bair\s*max\s*270\b|\bmax\s*270\b/i }, + { name: 'Nike Air Max Plus TN', pattern: /\btn\b|\bair\s*max\s*plus\b|\btuned\b/i }, + { name: 'Nike Vomero 5', pattern: /\bvomero\b|\bvomero\s*5\b/i }, + { name: 'Nike P-6000 Retro', pattern: /\bp-6000\b|\bp6000\b/i }, + { name: 'Nike Shox TL', pattern: /\bshox\b|\bshox\s*tl\b|\br4\b/i }, + { name: 'Nike Cortez Classic', pattern: /\bcortez\b/i }, + { name: 'Nike Blazer Mid 77', pattern: /\bblazer\b/i }, + { name: 'Nike TC 7900 Chunky', pattern: /\btc\s*7900\b|\btc7900\b/i }, + { name: 'Nike Gamma Force', pattern: /\bgamma\s*force\b/i }, + { name: 'Nike Air VaporMax Plus', pattern: /\bvapormax\b|\bvapor\s*max\b/i }, + { name: 'Nike Pegasus 40', pattern: /\bpegasus\b/i }, + { name: 'Nike Killshot 2', pattern: /\bkillshot\b/i }, + ], + }, + { + brand: 'Vans', + aliases: [/\bvans\b/i, /\boff\s*the\s*wall\b/i], + models: [ + { name: 'Vans Knu Skool Chunky', pattern: /\bknu\s*skool\b|\bknuskool\b|\bknu\b/i }, + { name: 'Vans Old Skool Classic', pattern: /\bold\s*skool\b|\boldskool\b/i }, + ], + }, + { + brand: 'Converse', + aliases: [/\bconverse\b/i, /\ball\s*star\b/i], + models: [ + { name: 'Converse Chuck 70 Vintage', pattern: /\bchuck\s*70\b|\bchuck70\b/i }, + { name: 'Converse Chuck Taylor All Star', pattern: /\bchuck\s*taylor\b|\ball\s*star\b/i }, + ], + }, + { + brand: 'Salomon', + aliases: [/\bsalomon\b/i], + models: [ + { name: 'Salomon XT-6 Gore-Tex', pattern: /\bxt-6\b|\bxt6\b/i }, + { name: 'Salomon XT-4 OG', pattern: /\bxt-4\b|\bxt4\b/i }, + ], + }, + { + brand: 'Crocs', + aliases: [/\bcrocs\b/i], + models: [ + { name: 'Crocs Pollex Clog Salehe Bembury', pattern: /\bpollex\b|\bsalehe\b/i }, + { name: 'Crocs Echo Clog', pattern: /\becho\s*clog\b|\becho\b/i }, + ], + }, +]; + +/** + * Detector determin铆stico y de alta precisi贸n de marca y modelo desde texto (100% seguro para cliente y servidor). + */ +export function detectBrandFromText(text: string): { + brand: string; + modelName?: string; + confidence: number; +} { + const normalized = (text || '').toLowerCase(); + + for (const item of BRAND_SIGNATURES) { + for (const model of item.models) { + if (model.pattern.test(normalized)) { + return { + brand: item.brand, + modelName: model.name, + confidence: 0.96, + }; + } + } + } + + for (const item of BRAND_SIGNATURES) { + for (const alias of item.aliases) { + if (alias.test(normalized)) { + return { + brand: item.brand, + confidence: 0.88, + }; + } + } + } + + return { + brand: 'Urbano / Variado', + confidence: 0.4, + }; +} + +/** + * Limpia y formatea el nombre del producto garantizando que NUNCA se incluyan nombres + * de proveedores, bodegas, canales o grupos de WhatsApp. + */ +export function cleanProductName( + text: string, + detectedBrand: string, + extraForbidden: (string | undefined | null)[] = [] +): string { + const lines = (text || '').split('\n').map((l) => l.trim()).filter((l) => l.length > 0); + for (const line of lines) { + const lower = line.toLowerCase(); + if ( + lower.includes('difusion jovic') || + lower.includes('jovic') || + lower.includes('cc m铆o') || + lower.includes('cc mio') || + lower.includes('bodega central') || + lower.includes('bodega mayorista') || + lower.includes('proveedor') || + lower.includes('canal mayorista') || + lower.includes('canal de difusi贸n') || + lower.includes('canal de difusion') || + lower.includes('grupo mayorista') || + lower.includes('grupo whatsapp') || + lower.includes('team emporio') || + lower.includes('notificaciones') || + lower.includes('medios y prensa') || + lower.includes('calzado mayorista') || + lower.includes('san andresito') + ) { + continue; + } + + const clean = line + .replace(/馃敟|馃敶|鈥硷笍|鉁▅馃尭|馃憫|馃拵|馃泹锔弢馃専|馃捀|馃挵|馃挼|馃挷|\*/g, '') + .replace(/dama\s*y\s*caballero|caballero|dama|unisex|recarga|nueva\s*mercanc[i铆]a|[u煤]ltima\s*unidad/gi, '') + .replace(/c[o贸]digo\s*:\s*[\d-]+/gi, '') + .replace(/\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\$\s*\d+/gi, '') + .replace(/tallas?:.*|precio:.*|eur\s*[\d\/-]+/gi, '') + .replace(/\s+/g, ' ') + .trim(); + + const sanitized = sanitizePublicationTitle(clean, extraForbidden, detectedBrand); + if (sanitized.length >= 3 && !/^\d+$/.test(sanitized) && !/^\d+[\.,]\d+$/.test(sanitized)) { + return sanitized; + } + } + + const defaultGeneric = `${detectedBrand} Urban Edition`; + return sanitizePublicationTitle(defaultGeneric, extraForbidden, detectedBrand); +} diff --git a/dripzone-app/src/lib/empty-module.js b/dripzone-app/src/lib/empty-module.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/dripzone-app/src/lib/empty-module.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/dripzone-app/src/lib/expiration-service.ts b/dripzone-app/src/lib/expiration-service.ts index f2fcc3f..2658763 100644 --- a/dripzone-app/src/lib/expiration-service.ts +++ b/dripzone-app/src/lib/expiration-service.ts @@ -3,53 +3,17 @@ import { dataService } from './data-service'; import { saveProductToAppwrite } from './server-appwrite'; import { registerProductInWhatsAppCatalog } from './whatsapp-catalog'; -/** - * Determina la fecha exacta de publicaci贸n de una zapatilla. - * Prioriza: - * 1. publishedAt - * 2. createdAt - * 3. Timestamp extra铆do del ID 'snk--...' - * 4. Fallback al momento actual - */ -export function getProductPublicationDate(product: Product): Date { - if (!product) return new Date(); - if (product.publishedAt) { - const d = new Date(product.publishedAt); - if (!isNaN(d.getTime())) return d; - } - if (product.createdAt) { - const d = new Date(product.createdAt); - if (!isNaN(d.getTime())) return d; - } - if (product.id && typeof product.id === 'string' && product.id.startsWith('snk-')) { - const parts = product.id.split('-'); - if (parts[1]) { - const ts = parseInt(parts[1], 10); - if (!isNaN(ts) && ts > 1000000000000 && ts < 2500000000000) { - return new Date(ts); - } - } - } - return new Date(); -} +import { + getProductPublicationDate, + getProductAgeInDays, + isProductOlderThanDays, +} from './product-date-utils'; -/** - * Calcula la antig眉edad en d铆as de una publicaci贸n de calzado - */ -export function getProductAgeInDays(product: Product): number { - if (!product) return 0; - const pubDate = getProductPublicationDate(product); - const diffMs = Date.now() - pubDate.getTime(); - return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24))); -} - -/** - * Verifica si una publicaci贸n supera el umbral de d铆as establecido (por defecto 7 d铆as) - */ -export function isProductOlderThanDays(product: Product, daysThreshold: number = 7): boolean { - if (!product) return false; - return getProductAgeInDays(product) >= daysThreshold; -} +export { + getProductPublicationDate, + getProductAgeInDays, + isProductOlderThanDays, +}; export interface ExpirationExecutionResult { success: boolean; diff --git a/dripzone-app/src/lib/product-date-utils.ts b/dripzone-app/src/lib/product-date-utils.ts new file mode 100644 index 0000000..c453aa8 --- /dev/null +++ b/dripzone-app/src/lib/product-date-utils.ts @@ -0,0 +1,45 @@ +import { Product } from '@/types'; + +/** + * Determina la fecha exacta de publicaci贸n de una zapatilla de forma 100% segura para el cliente (navegador). + * No importa m贸dulos de Node.js ni bases de datos de servidor. + */ +export function getProductPublicationDate(product: Product): Date { + if (!product) return new Date(); + if (product.publishedAt) { + const d = new Date(product.publishedAt); + if (!isNaN(d.getTime())) return d; + } + if (product.createdAt) { + const d = new Date(product.createdAt); + if (!isNaN(d.getTime())) return d; + } + if (product.id && typeof product.id === 'string' && product.id.startsWith('snk-')) { + const parts = product.id.split('-'); + if (parts[1]) { + const ts = parseInt(parts[1], 10); + if (!isNaN(ts) && ts > 1000000000000 && ts < 2500000000000) { + return new Date(ts); + } + } + } + return new Date(); +} + +/** + * Calcula la antig眉edad en d铆as de una publicaci贸n de calzado + */ +export function getProductAgeInDays(product: Product): number { + if (!product) return 0; + const pubDate = getProductPublicationDate(product); + const diffMs = Date.now() - pubDate.getTime(); + return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24))); +} + +/** + * Verifica si una publicaci贸n supera el umbral de d铆as establecido (por defecto 7 d铆as) + */ +export function isProductOlderThanDays(product: Product, daysThreshold: number = 7): boolean { + if (!product) return false; + return getProductAgeInDays(product) >= daysThreshold; +}