diff --git a/prisma/initial_data.ts b/prisma/initial_data.ts index 3ab5380..1c3bcf9 100644 --- a/prisma/initial_data.ts +++ b/prisma/initial_data.ts @@ -30,9 +30,9 @@ export const categories = [ ]; export const variantAttributes = [ - { name: "no aplica" }, { name: "talla" }, { name: "dimensiones" }, + { name: "no aplica" }, ] export const products = [ @@ -357,74 +357,74 @@ export const products = [ export const variantAttributeValues = [ // --- POLOS (talla: S, M, L) --- - { attributeId: 1, productId: 1, value: "S", price: 20.0 }, - { attributeId: 1, productId: 1, value: "M", price: 20.0 }, - { attributeId: 1, productId: 1, value: "L", price: 20.0 }, + { attributeId: 1, productId: 1, value: "Small", price: 20.0 }, + { attributeId: 1, productId: 1, value: "Medium", price: 20.0 }, + { attributeId: 1, productId: 1, value: "Large", price: 20.0 }, - { attributeId: 1, productId: 2, value: "S", price: 20.0 }, - { attributeId: 1, productId: 2, value: "M", price: 20.0 }, - { attributeId: 1, productId: 2, value: "L", price: 20.0 }, + { attributeId: 1, productId: 2, value: "Small", price: 20.0 }, + { attributeId: 1, productId: 2, value: "Medium", price: 20.0 }, + { attributeId: 1, productId: 2, value: "Large", price: 20.0 }, - { attributeId: 1, productId: 3, value: "S", price: 20.0 }, - { attributeId: 1, productId: 3, value: "M", price: 20.0 }, - { attributeId: 1, productId: 3, value: "L", price: 20.0 }, + { attributeId: 1, productId: 3, value: "Small", price: 20.0 }, + { attributeId: 1, productId: 3, value: "Medium", price: 20.0 }, + { attributeId: 1, productId: 3, value: "Large", price: 20.0 }, - { attributeId: 1, productId: 4, value: "S", price: 20.0 }, - { attributeId: 1, productId: 4, value: "M", price: 20.0 }, - { attributeId: 1, productId: 4, value: "L", price: 20.0 }, + { attributeId: 1, productId: 4, value: "Small", price: 20.0 }, + { attributeId: 1, productId: 4, value: "Medium", price: 20.0 }, + { attributeId: 1, productId: 4, value: "Large", price: 20.0 }, - { attributeId: 1, productId: 5, value: "S", price: 25.0 }, - { attributeId: 1, productId: 5, value: "M", price: 25.0 }, - { attributeId: 1, productId: 5, value: "L", price: 25.0 }, + { attributeId: 1, productId: 5, value: "Small", price: 25.0 }, + { attributeId: 1, productId: 5, value: "Medium", price: 25.0 }, + { attributeId: 1, productId: 5, value: "Large", price: 25.0 }, - { attributeId: 1, productId: 6, value: "S", price: 25.0 }, - { attributeId: 1, productId: 6, value: "M", price: 25.0 }, - { attributeId: 1, productId: 6, value: "L", price: 25.0 }, + { attributeId: 1, productId: 6, value: "Small", price: 25.0 }, + { attributeId: 1, productId: 6, value: "Medium", price: 25.0 }, + { attributeId: 1, productId: 6, value: "Large", price: 25.0 }, - { attributeId: 1, productId: 7, value: "S", price: 25.0 }, - { attributeId: 1, productId: 7, value: "M", price: 25.0 }, - { attributeId: 1, productId: 7, value: "L", price: 25.0 }, + { attributeId: 1, productId: 7, value: "Small", price: 25.0 }, + { attributeId: 1, productId: 7, value: "Medium", price: 25.0 }, + { attributeId: 1, productId: 7, value: "Large", price: 25.0 }, - { attributeId: 1, productId: 8, value: "S", price: 15.0 }, - { attributeId: 1, productId: 8, value: "M", price: 15.0 }, - { attributeId: 1, productId: 8, value: "L", price: 15.0 }, + { attributeId: 1, productId: 8, value: "Small", price: 15.0 }, + { attributeId: 1, productId: 8, value: "Medium", price: 15.0 }, + { attributeId: 1, productId: 8, value: "Large", price: 15.0 }, - { attributeId: 1, productId: 9, value: "S", price: 15.0 }, - { attributeId: 1, productId: 9, value: "M", price: 15.0 }, - { attributeId: 1, productId: 9, value: "L", price: 15.0 }, + { attributeId: 1, productId: 9, value: "Small", price: 15.0 }, + { attributeId: 1, productId: 9, value: "Medium", price: 15.0 }, + { attributeId: 1, productId: 9, value: "Large", price: 15.0 }, // --- STICKERS (dimensiones: 3x3, 6x6, 9x9) --- - { attributeId: 2, productId: 10, value: "3x3", price: 2.99 }, - { attributeId: 2, productId: 10, value: "5x5", price: 3.99 }, - { attributeId: 2, productId: 10, value: "10x10", price: 4.99 }, + { attributeId: 2, productId: 10, value: "3x3 cm", price: 2.99 }, + { attributeId: 2, productId: 10, value: "5x5 cm", price: 3.99 }, + { attributeId: 2, productId: 10, value: "10x10 cm", price: 4.99 }, - { attributeId: 2, productId: 11, value: "3x3", price: 2.49 }, - { attributeId: 2, productId: 11, value: "5x5", price: 3.49 }, - { attributeId: 2, productId: 11, value: "10x10", price: 4.49 }, + { attributeId: 2, productId: 11, value: "3x3 cm", price: 2.49 }, + { attributeId: 2, productId: 11, value: "5x5 cm", price: 3.49 }, + { attributeId: 2, productId: 11, value: "10x10 cm", price: 4.49 }, - { attributeId: 2, productId: 12, value: "3x3", price: 3.99 }, - { attributeId: 2, productId: 12, value: "5x5", price: 4.99 }, - { attributeId: 2, productId: 12, value: "10x10", price: 5.99 }, + { attributeId: 2, productId: 12, value: "3x3 cm", price: 3.99 }, + { attributeId: 2, productId: 12, value: "5x5 cm", price: 4.99 }, + { attributeId: 2, productId: 12, value: "10x10 cm", price: 5.99 }, - { attributeId: 2, productId: 13, value: "3x3", price: 2.99 }, - { attributeId: 2, productId: 13, value: "5x5", price: 3.99 }, - { attributeId: 2, productId: 13, value: "10x10", price: 4.99 }, + { attributeId: 2, productId: 13, value: "3x3 cm", price: 2.99 }, + { attributeId: 2, productId: 13, value: "5x5 cm", price: 3.99 }, + { attributeId: 2, productId: 13, value: "10x10 cm", price: 4.99 }, - { attributeId: 2, productId: 14, value: "3x3", price: 2.49 }, - { attributeId: 2, productId: 14, value: "5x5", price: 3.49 }, - { attributeId: 2, productId: 14, value: "10x10", price: 4.49 }, + { attributeId: 2, productId: 14, value: "3x3 cm", price: 2.49 }, + { attributeId: 2, productId: 14, value: "5x5 cm", price: 3.49 }, + { attributeId: 2, productId: 14, value: "10x10 cm", price: 4.49 }, - { attributeId: 2, productId: 15, value: "3x3", price: 2.49 }, - { attributeId: 2, productId: 15, value: "5x5", price: 3.49 }, - { attributeId: 2, productId: 15, value: "10x10", price: 4.49 }, + { attributeId: 2, productId: 15, value: "3x3 cm", price: 2.49 }, + { attributeId: 2, productId: 15, value: "5x5 cm", price: 3.49 }, + { attributeId: 2, productId: 15, value: "10x10 cm", price: 4.49 }, - { attributeId: 2, productId: 16, value: "3x3", price: 2.99 }, - { attributeId: 2, productId: 16, value: "5x5", price: 3.99 }, - { attributeId: 2, productId: 16, value: "10x10", price: 4.99 }, + { attributeId: 2, productId: 16, value: "3x3 cm", price: 2.99 }, + { attributeId: 2, productId: 16, value: "5x5 cm", price: 3.99 }, + { attributeId: 2, productId: 16, value: "10x10 cm", price: 4.99 }, - { attributeId: 2, productId: 17, value: "3x3", price: 2.99 }, - { attributeId: 2, productId: 17, value: "5x5", price: 3.99 }, - { attributeId: 2, productId: 17, value: "10x10", price: .99 }, + { attributeId: 2, productId: 17, value: "3x3 cm", price: 2.99 }, + { attributeId: 2, productId: 17, value: "5x5 cm", price: 3.99 }, + { attributeId: 2, productId: 17, value: "10x10 cm", price: 4.99 }, // --- TAZAS (no aplica: Único) --- { attributeId: 3, productId: 18, value: "Único", price: 14.99 }, diff --git a/src/routes/category/components/product-card/index.tsx b/src/routes/category/components/product-card/index.tsx index 23c402b..e5b5c90 100644 --- a/src/routes/category/components/product-card/index.tsx +++ b/src/routes/category/components/product-card/index.tsx @@ -31,18 +31,25 @@ export function ProductCard({ product }: ProductCardProps) {

{product.title}

{product.description}

- {isSticker ? ( -
-

- Desde -

-

- S/{product.minPrice} - S/{product.maxPrice} -

-
+ {product.categoryId === 3 ? ( +
+

+ Desde +

+

+ S/{product.minPrice} - S/{product.maxPrice} +

+
) : ( -

S/{product.price}

- )} +
+

+ Precio +

+

+ S/{product.price} +

+
+ )}
{product.isOnSale && ( diff --git a/src/routes/product/index.tsx b/src/routes/product/index.tsx index 545544c..247bbab 100644 --- a/src/routes/product/index.tsx +++ b/src/routes/product/index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Form, useNavigation } from "react-router"; import { Button, Container, Separator } from "@/components/ui"; @@ -22,7 +22,10 @@ export default function Product({ loaderData }: Route.ComponentProps) { const { product } = loaderData; const navigation = useNavigation(); const cartLoading = navigation.state === "submitting"; - const [selectedSize, setSelectedSize] = useState("Medium"); + + // Estados para manejar variantes + const [selectedVariant, setSelectedVariant] = useState(null); + const [currentPrice, setCurrentPrice] = useState(0); if (!product) { return ; @@ -30,6 +33,46 @@ export default function Product({ loaderData }: Route.ComponentProps) { const showSizeSelector = product.categoryId === 1 || product.categoryId === 3; + // Verificar si el producto tiene variantes + const hasVariants = product.variantAttributeValues && product.variantAttributeValues.length > 0; + + // Verificar si debe mostrar selectores (solo polos y stickers) + const shouldShowVariants = hasVariants && (product.categoryId === 1 || product.categoryId === 3); + + // Agrupar variantes por atributo (en caso de que un producto tenga múltiples tipos de atributos) + const variantGroups = shouldShowVariants + ? product.variantAttributeValues.reduce((groups, variant) => { + const attributeName = variant.variantAttribute.name; + if (!groups[attributeName]) { + groups[attributeName] = []; + } + groups[attributeName].push(variant); + return groups; + }, {} as Record) + : {}; + + // Inicializar precio y variante seleccionada + useEffect(() => { + if (hasVariants) { + // Seleccionar la primera variante por defecto + const firstVariant = product.variantAttributeValues[0]; + setSelectedVariant(firstVariant.id); + setCurrentPrice(firstVariant.price); + } else { + // Si no hay variantes, usar el precio base del producto (asumiendo que existe) + setCurrentPrice(product.price || 0); + } + }, [product]); + + // Manejar cambio de variante + const handleVariantChange = (variantId: number) => { + setSelectedVariant(variantId); + const variant = product.variantAttributeValues.find(v => v.id === variantId); + if (variant) { + setCurrentPrice(variant.price); + } + }; + const getAttributeValueId = () => { // AQUI TRAER EL AttributeValueId con el cambio de SEBAS if ( !product.variantAttributeValues || @@ -41,29 +84,6 @@ export default function Product({ loaderData }: Route.ComponentProps) { return product.variantAttributeValues[0].id; }; - const getSizeOptions = () => { - if (product.categoryId === 3) { - return { - label: "Dimensiones", - options: [ - { value: "Small", label: "3x3 cm" }, - { value: "Medium", label: "5x5 cm" }, - { value: "Large", label: "10x10 cm" }, - ], - }; - } else { - return { - label: "Talla", - options: [ - { value: "Small", label: "Small" }, - { value: "Medium", label: "Medium" }, - { value: "Large", label: "Large" }, - ], - }; - } - }; - - const sizeOptions = getSizeOptions(); return ( <> diff --git a/src/services/chat-system-prompt.ts b/src/services/chat-system-prompt.ts index 30401b0..38426db 100644 --- a/src/services/chat-system-prompt.ts +++ b/src/services/chat-system-prompt.ts @@ -13,32 +13,70 @@ export function generateSystemPrompt({ products, userCart, }: SystemPromptConfig): string { - const onSaleProducts = products.filter((p) => p.isOnSale); - const salesSection = - onSaleProducts.length > 0 - ? ` + + // Procesar productos con información de variantes + const processedProducts = products.map(product => { + const category = categories.find((c) => c.id === product.categoryId); + + // Formatear precio según si tiene variantes o no + let priceDisplay = ""; + if (product.price) { + priceDisplay = `S/${product.price}`; + } else if (product.minPrice && product.maxPrice) { + priceDisplay = `S/${product.minPrice} - S/${product.maxPrice}`; + } + + // Formatear variantes según el tipo + let variantDisplay = ""; + if (product.variants && product.variants.length > 0 && product.variantType !== 'único') { + switch (product.variantType) { + case 'talla': + const sizes = product.variants.map(v => v.value).join(", "); + variantDisplay = `\n- 👕 Tallas disponibles: ${sizes}`; + break; + case 'dimensión': + const dimensions = product.variants + .map(v => `${v.value} (S/${v.price})`) + .join(", "); + variantDisplay = `\n- 📐 Dimensiones: ${dimensions}`; + break; + default: + const options = product.variants + .map(v => `${v.value} (S/${v.price})`) + .join(", "); + variantDisplay = `\n- ⚙️ Opciones: ${options}`; + } + } + + return { + ...product, + categoryTitle: category?.title || "Sin categoría", + priceDisplay, + variantDisplay + }; + }); + + // Procesar productos en oferta + const onSaleProducts = processedProducts.filter((p) => p.isOnSale); + const salesSection = onSaleProducts.length > 0 + ? ` ## 🔥 PRODUCTOS EN OFERTA ESPECIAL: ${onSaleProducts - .map( - (product) => ` -- **${product.title}** - S/${product.price} ⚡ [Ver oferta](/products/${product.id}) -` - ) + .map(product => ` +- **${product.title}** - ${product.priceDisplay} ⚡ [Ver oferta](/products/${product.id})`) .join("")} ` - : ""; + : ""; + // Procesar carrito del usuario const cartSection = userCart?.items?.length ? ` ## 🛒 CARRITO ACTUAL DEL USUARIO: El usuario tiene actualmente ${userCart.items.length} producto(s) en su carrito: ${userCart.items - .map( - (item) => ` + .map(item => ` - **${item.product.title}** (Cantidad: ${item.quantity}) - S/${item.product.price} - Link: [Ver producto](/products/${item.product.id}) -` - ) + Link: [Ver producto](/products/${item.product.id})`) .join("")} **IMPORTANTE**: Usa esta información para hacer recomendaciones inteligentes: @@ -51,6 +89,25 @@ ${userCart.items ` : ""; + // Generar categorías + const categoriesSection = categories + .map(cat => ` +**${cat.title}** (${cat.slug}) +- Descripción: ${cat.description} +- Link: [Ver categoría](/category/${cat.slug})`) + .join("\n"); + + // Generar productos + const productsSection = processedProducts + .map(product => ` +**${product.title}** +- 💰 Precio: ${product.priceDisplay}${product.isOnSale ? " ⚡ ¡EN OFERTA!" : ""} +- 📝 Descripción: ${product.description} +- 🏷️ Categoría: ${product.categoryTitle} +- ✨ Características: ${product.features.join(", ")}${product.variantDisplay} +- 🔗 Link: [Ver producto](/products/${product.id})`) + .join("\n"); + return ` # Asistente Virtual de Full Stock @@ -69,40 +126,42 @@ Eres un asistente virtual especializado en **Full Stock**, una tienda de product ## PRODUCTOS DISPONIBLES: ### Categorías: -${categories - .map( - (cat) => ` -**${cat.title}** (${cat.slug}) -- Descripción: ${cat.description} -- Link: [Ver categoría](/category/${cat.slug}) -` - ) - .join("\n")} +${categoriesSection} ### Productos: -${products - .map((product) => { - const category = categories.find((c) => c.id === product.categoryId); - return ` -**${product.title}** -- 💰 Precio: S/${product.price}${product.isOnSale ? " ⚡ ¡EN OFERTA!" : ""} -- 📝 Descripción: ${product.description} -- 🏷️ Categoría: ${category?.title || "Sin categoría"} -- ✨ Características: ${product.features.join(", ")} -- 🔗 Link: [Ver producto](/products/${product.id}) -`; - }) - .join("\n")} +${productsSection} ${salesSection} ${cartSection} +## MANEJO DE VARIANTES DE PRODUCTOS: +**IMPORTANTE**: Cuando un usuario muestre interés en un producto con variantes: + +### Para POLOS (Tallas): +- Si preguntan por un polo, menciona: "¿Qué talla necesitas: Small, Medium o Large?" +- Ejemplo: "¡El [Polo React](/products/1) está disponible en tallas S, M y L por S/20! ¿Cuál prefieres?" + +### Para STICKERS (Dimensiones): +- Menciona las opciones con precios: "Tenemos 3 tamaños: 3x3cm (S/2.99), 5x5cm (S/3.99) o 10x10cm (S/4.99)" +- Ejemplo: "¡El [Sticker Docker](/products/10) viene en varios tamaños! ¿Prefieres 3x3cm (S/2.99), 5x5cm (S/3.99) o 10x10cm (S/4.99)?" + +### Para PRODUCTOS ÚNICOS (Tazas): +- Procede normal, no menciones variantes +- Ejemplo: "¡La [Taza JavaScript](/products/18) por S/14.99 es perfecta para tu café matutino!" + +### Reglas Generales: +- **SIEMPRE pregunta por la variante** cuando el usuario muestre interés en el producto +- **Incluye precios** solo si varían entre opciones +- **Sé específico** sobre las opciones disponibles +- **Facilita la decisión** con recomendaciones si es necesario + ## INSTRUCCIONES PARA RESPUESTAS: - **MANTÉN LAS RESPUESTAS BREVES Y DIRECTAS** (máximo 2-3 oraciones) - Ve directo al punto, sin explicaciones largas - Cuando recomiendes productos, SIEMPRE incluye el link en formato: [Nombre del Producto](/products/ID) - Para categorías, usa links como: [Categoría](/category/slug) +- **AL MENCIONAR PRODUCTOS CON VARIANTES**, pregunta inmediatamente por la opción preferida - Responde en **Markdown** para dar formato atractivo - Sé específico sobre precios, características y beneficios - Si hay productos en oferta, destácalos con emojis y texto llamativo @@ -123,6 +182,7 @@ ${cartSection} - **Personalización**: Adapta según el nivel o tecnología mencionada - **Storytelling**: Usa curiosidades técnicas o historias para conectar emocionalmente con productos - **Oportunidades educativas**: Si preguntan sobre tecnologías que tienes en productos, educa brevemente y conecta con la venta +- **Variantes como valor**: Destaca las opciones disponibles como ventaja del producto ## LÓGICA DE RECOMENDACIONES BASADAS EN CARRITO: **Si el usuario tiene productos en su carrito y pide recomendaciones:** @@ -144,17 +204,18 @@ Cuando te pregunten sobre tecnologías que tenemos en productos (React, Docker, 4. **Ejemplo**: "Docker usa una ballena porque simboliza transportar contenedores por el océano 🐳 ¡Nuestro [Sticker Docker](/products/X) es perfecto para mostrar tu amor por la containerización!" ## RESPUESTAS A PREGUNTAS COMUNES: -- **Tallas**: "Nuestros polos vienen en tallas S, M, L, XL. ¿Cuál prefieres?" +- **Tallas**: "Nuestros polos vienen en tallas S, M, L. ¿Cuál prefieres?" + - **Envío**: "Manejamos envío a todo el país. ¿A qué ciudad lo necesitas?" - **Materiales**: "Usamos algodón 100% de alta calidad para máxima comodidad" - **Cuidado**: "Para que dure más, lava en agua fría y evita la secadora" -## EJEMPLOS DE RESPUESTAS CORTAS: -- "¡Te recomiendo el [Polo React](/products/1) por S/20.00! 🚀 ¿Qué talla necesitas?" -- "Perfecto para backend: [Polo Backend Developer](/products/3) ⚡ **EN OFERTA** por S/25.00. ¿Te animas?" -- **Ejemplo de pregunta técnica relacionada**: "¡La ballena de Docker representa la facilidad de transportar aplicaciones! 🐳 Nuestro [Sticker Docker](/products/X) captura perfectamente esa filosofía. ¿Te gusta coleccionar stickers de tecnología?" -- **Ejemplo con carrito (React)**: "Veo que tienes el Polo React en tu carrito! Para completar tu look frontend, te recomiendo la [Taza React](/products/Y). ¿Te interesa?" -- **Ejemplo con carrito (Backend)**: "Perfecto, tienes productos backend en tu carrito. El [Sticker Node.js](/products/Z) combinaría genial. ¿Lo agregamos?" +## EJEMPLOS DE RESPUESTAS CORTAS CON VARIANTES: +- "¡Te recomiendo el [Polo React](/products/1) por S/20! 🚀 ¿Qué talla necesitas: S, M o L?" + +- "La [Taza JavaScript](/products/18) por S/14.99 es perfecta para programar. ¿La agregamos?" +- **Ejemplo con carrito (React)**: "Veo que tienes el Polo React! Para completar tu look frontend, ¿te interesa el [Sticker React](/products/Y)? Viene en 3 tamaños diferentes." +- **Ejemplo con carrito (Backend)**: "Perfecto, tienes productos backend. El [Polo Node.js](/products/Z) combinaría genial. ¿Qué talla usas: S, M o L?" ¿En qué puedo ayudarte hoy a encontrar el producto perfecto para ti? 🛒✨ `; diff --git a/src/services/product.service.ts b/src/services/product.service.ts index add02b7..c79e857 100644 --- a/src/services/product.service.ts +++ b/src/services/product.service.ts @@ -5,22 +5,39 @@ import type { VariantAttributeValue } from "@/models/variant-attribute.model"; import { getCategoryBySlug } from "./category.service"; -const formattedProduct = (product: ProductVariantValue) => { - const {variantAttributeValues, ...rest} = product - const prices = variantAttributeValues.map((v: VariantAttributeValue) => Number(v.price)) - const minPrice = Math.min(...prices) - const maxPrice = Math.max(...prices) - if (minPrice === maxPrice) { - return { - ...rest, - price: minPrice - } - } - return { - ...rest, - minPrice, - maxPrice +const formattedProduct = (product: ProductVariantValue) => { + const {variantAttributeValues, ...rest} = product + + const prices = variantAttributeValues.map((v: VariantAttributeValue) => Number(v.price)) + const minPrice = Math.min(...prices) + const maxPrice = Math.max(...prices) + + // Agrupar y formatear variantes + const variants = variantAttributeValues.map(v => ({ + id: v.id, + attributeId: v.attributeId, + value: v.value, + price: Number(v.price) + })) + + // Determinar tipo de variante basado en attributeId + const getVariantType = (attributeId: number) => { + switch (attributeId) { + case 1: return 'talla' + case 2: return 'dimensión' + case 3: return 'único' + default: return 'variante' } + } + + const variantType = variants.length > 0 ? getVariantType(variants[0].attributeId) : 'único' + + return { + ...rest, + variants, + variantType, + ...(minPrice === maxPrice ? { price: minPrice } : { minPrice, maxPrice }) + } } export async function getProductsByCategorySlug( @@ -41,7 +58,11 @@ export async function getProductById(id: number): Promise { const product = await prisma.product.findUnique({ where: { id }, include: { - variantAttributeValues: true + variantAttributeValues: { + include: { + variantAttribute: true + } + } } });