From 71ee2976a2ab36af93e4e6225a9e92e9da5d3b30 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 19 Dec 2025 09:28:36 +0100 Subject: [PATCH] Update readmes and imporve UI --- docs/TECHNICAL-DOCUMENTATION-SUMMARY.md | 255 +++++++++++++++--- .../unified-wizard/ItemTypeSelector.tsx | 111 +++++++- .../unified-wizard/UnifiedAddWizard.tsx | 54 +++- .../wizards/PurchaseOrderWizard.tsx | 4 +- .../components/ui/WizardModal/WizardModal.tsx | 243 +++++++++++++---- .../src/features/demo-onboarding/README.md | 14 +- services/alert_processor/README.md | 104 +++++-- services/demo_session/README.md | 95 ++++++- services/orders/README.md | 127 +++++++++ services/production/README.md | 183 ++++++++++++- 10 files changed, 1035 insertions(+), 155 deletions(-) diff --git a/docs/TECHNICAL-DOCUMENTATION-SUMMARY.md b/docs/TECHNICAL-DOCUMENTATION-SUMMARY.md index 7cb139a9..4005325d 100644 --- a/docs/TECHNICAL-DOCUMENTATION-SUMMARY.md +++ b/docs/TECHNICAL-DOCUMENTATION-SUMMARY.md @@ -11,7 +11,7 @@ Bakery-IA is an **AI-powered SaaS platform** designed specifically for the Spani ## Platform Architecture Overview ### System Design -- **Architecture Pattern**: Microservices (18 independent services) +- **Architecture Pattern**: Microservices (21 independent services) - **API Gateway**: Centralized routing with JWT authentication - **Frontend**: React 18 + TypeScript progressive web application - **Database Strategy**: PostgreSQL 17 per service (database-per-service pattern) @@ -45,7 +45,24 @@ Bakery-IA is an **AI-powered SaaS platform** designed specifically for the Spani ## Service Documentation Index -### 📚 Comprehensive READMEs Created (7/20) +### 📚 Comprehensive READMEs Created (15/21) + +**Fully Documented Services:** +1. API Gateway (700+ lines) +2. Frontend Dashboard (800+ lines) +3. Forecasting Service (1,095+ lines) +4. Training Service (850+ lines) +5. AI Insights Service (enhanced) +6. Sales Service (493+ lines) +7. Inventory Service (1,120+ lines) +8. Production Service (394+ lines) +9. Orders Service (833+ lines) +10. Procurement Service (1,343+ lines) +11. Distribution Service (961+ lines) +12. Alert Processor Service (1,800+ lines) +13. Orchestrator Service (enhanced) +14. Demo Session Service (708+ lines) +15. Alert System Architecture (2,800+ lines standalone doc) ### 🎯 **New: Alert System Architecture** ([docs/ALERT-SYSTEM-ARCHITECTURE.md](./ALERT-SYSTEM-ARCHITECTURE.md)) **2,800+ lines | Complete Alert System Documentation** @@ -85,7 +102,7 @@ Bakery-IA is an **AI-powered SaaS platform** designed specifically for the Spani **700+ lines | Centralized Entry Point** **Key Features:** -- Single API endpoint for 18+ microservices +- Single API endpoint for 21 microservices - JWT authentication with 15-minute token cache - Rate limiting (300 req/min per client) - Server-Sent Events (SSE) for real-time alerts @@ -149,6 +166,38 @@ Bakery-IA is an **AI-powered SaaS platform** designed specifically for the Spani --- +#### 2b. **Demo Onboarding System** ([frontend/src/features/demo-onboarding/README.md](../frontend/src/features/demo-onboarding/README.md)) +**210+ lines | Interactive Demo Tour & Conversion** + +**Key Features:** +- **Interactive guided tour** - 12-step desktop, 8-step mobile (Driver.js) +- **Demo banner** with live session countdown and time remaining +- **Exit modal** with benefits reminder and conversion messaging +- **State persistence** - Auto-resume tour with sessionStorage +- **Analytics tracking** - Google Analytics & Plausible integration +- **Full localization** - Spanish and English translations +- **Mobile-responsive** - Optimized for thumb zone navigation + +**Tour Steps Coverage:** +- Welcome → Metrics Dashboard → Pending Approvals → System Actions +- Production Plan → Database Nav → Operations → Analytics → Multi-Bakery +- Demo Limitations → Final CTA + +**Tracked Events:** +- `tour_started`, `tour_step_completed`, `tour_dismissed` +- `tour_completed`, `conversion_cta_clicked` + +**Business Value:** +- Guided onboarding reduces setup friction +- Auto-resume increases completion rates +- Conversion CTAs throughout demo journey +- Session countdown creates urgency +- 3-second comprehension with progressive disclosure + +**Technology:** Driver.js, React, TypeScript, SessionStorage + +--- + #### 3. **Forecasting Service** ([services/forecasting/README.md](../services/forecasting/README.md)) **850+ lines | AI Demand Prediction Core** @@ -297,54 +346,120 @@ Data Collection → Feature Engineering → Prophet Training ### Core Business Services **7. Inventory Service** ([services/inventory/README.md](../services/inventory/README.md)) -**Enhanced | Stock Management & Receipt System** +**1,120+ lines | Stock Management & Food Safety Compliance** **Key Features:** -- Stock tracking with FIFO (First-In-First-Out) -- Expiration management and alerts -- Low stock alerts with intelligent thresholds -- Food safety compliance (HACCP) -- Barcode support -- **Stock Receipt System (NEW)**: +- Comprehensive ingredient management with FIFO consumption and batch tracking +- Automatic stock updates from delivery events with batch/expiry tracking +- HACCP-compliant food safety monitoring with temperature logging +- Expiration management with automated FIFO rotation and waste tracking +- Multi-location inventory tracking across storage locations +- Enterprise: Automatic inventory transfer processing for internal shipments +- **Stock Receipt System**: - Lot-level tracking with expiration dates (food safety requirement) - Purchase order integration with discrepancy tracking - - Draft/Confirmed receipt workflow - - Line item validation (sum of lots must equal actual quantity) - - Alert integration (DELIVERY_ARRIVING_SOON, STOCK_RECEIPT_INCOMPLETE) - - HACCP compliance enforcement (expiration dates required for perishables) - - Atomic transaction on confirmation (stock updates, lot creation, PO status update, alert resolution) + - Draft/Confirmed receipt workflow with line item validation + - Alert integration and automatic resolution on confirmation + - Atomic transactions for stock updates and PO status changes + +**Alert Types Published:** +- Low stock alerts (below reorder point) +- Expiring soon alerts (within threshold days) +- Food safety alerts (temperature violations) **Business Value:** -- 100% food safety compliance (lot traceability) -- 95% delivery discrepancy detection -- 30% faster receiving process -- Automatic alert resolution on receipt confirmation +- Waste Reduction: 20-40% through FIFO and expiry management +- Cost Savings: €200-600/month from reduced waste +- Time Savings: 8-12 hours/week on manual tracking +- Compliance: 100% HACCP compliance (avoid €5,000+ fines) +- Inventory Accuracy: 95%+ vs. 70-80% manual -**Technology:** FastAPI, PostgreSQL, Redis, RabbitMQ +**Technology:** FastAPI, PostgreSQL, Redis, RabbitMQ, SQLAlchemy -**8. Production Service** -- Production scheduling -- Batch tracking -- Quality control -- Equipment management -- Capacity planning +**8. Production Service** ([services/production/README.md](../services/production/README.md)) +**394+ lines | Manufacturing Operations Core** + +**Key Features:** +- Automated forecast-driven scheduling (7-day advance planning) +- Real-time batch tracking with FIFO stock deduction and yield monitoring +- Digital quality control with standardized templates and metrics +- Equipment management with preventive maintenance tracking +- Production analytics with OEE and cost analysis +- Multi-day scheduling with automatic equipment allocation + +**Alert Types Published (8 types):** +- Production delays, equipment failures, capacity overload +- Quality issues, missing ingredients, maintenance due +- Batch start delays, production start notifications + +**Business Value:** +- Time Savings: 10-15 hours/week on planning +- Waste Reduction: 15-25% through optimization +- Quality Improvement: 20-30% fewer defects +- Capacity Utilization: 85%+ vs 65-70% manual + +**Technology:** FastAPI, PostgreSQL, Redis, RabbitMQ, SQLAlchemy + +--- **9. Recipes Service** -- Recipe management -- Ingredient quantities -- Batch scaling -- Cost calculation +- Recipe management with versioning +- Ingredient quantities and scaling +- Batch size calculation +- Cost estimation and margin analysis +- Production instructions -**10. Orders Service** -- Customer order management -- Order lifecycle tracking -- Customer database +--- -**11. Procurement Service** -- Automated procurement planning -- Purchase order management -- Supplier integration -- Replenishment planning +**10. Orders Service** ([services/orders/README.md](../services/orders/README.md)) +**833+ lines | Customer Order Management** + +**Key Features:** +- Multi-channel order management (in-store, phone, online, wholesale) +- Comprehensive customer database with RFM analysis +- B2B wholesale management with custom pricing +- Automated invoicing with payment tracking +- Order fulfillment integration with production and inventory +- Customer analytics and segmentation + +**Alert Types Published (5 types):** +- POs pending approval, approval reminders +- Critical PO escalation, auto-approval summaries +- PO approval confirmations + +**Business Value:** +- Revenue Growth: 10-20% through improved B2B +- Time Savings: 5-8 hours/week on management +- Order Accuracy: 99%+ vs. 85-90% manual +- Payment Collection: 30% faster with reminders + +**Technology:** FastAPI, PostgreSQL, Redis, RabbitMQ, Pydantic + +--- + +**11. Procurement Service** ([services/procurement/README.md](../services/procurement/README.md)) +**1,343+ lines | Intelligent Purchasing Automation** + +**Key Features:** +- Intelligent forecast-driven replenishment (7-30 day projections) +- Automated PO generation with smart supplier selection +- Dashboard-integrated approval workflow with email notifications +- Delivery tracking with automatic stock updates +- EOQ and reorder point calculation +- Enterprise: Internal transfers with cost-based pricing + +**Alert Types Published (7 types):** +- Stock shortages, delivery overdue, supplier performance issues +- Price increases, partial deliveries, quality issues +- Low supplier ratings + +**Business Value:** +- Stockout Prevention: 85-95% reduction +- Cost Savings: 5-15% through optimized ordering +- Time Savings: 8-12 hours/week +- Inventory Reduction: 20-30% lower levels + +**Technology:** FastAPI, PostgreSQL, Redis, RabbitMQ, Pydantic **12. Suppliers Service** - Supplier database @@ -454,9 +569,61 @@ Data Collection → Feature Engineering → Prophet Training **Technology:** FastAPI, PostgreSQL, RabbitMQ, Kubernetes CronJobs -**20. Demo Session Service** -- Ephemeral demo environments -- Isolated demo accounts +**20. Demo Session Service** ([services/demo_session/README.md](../services/demo_session/README.md)) +**708+ lines | Demo Environment Management** + +**Key Features:** +- Direct database loading approach (eliminates Kubernetes Jobs) +- XOR-based deterministic ID transformation for tenant isolation +- Temporal determinism with dynamic date adjustment +- Per-service cloning progress tracking with JSONB metadata +- Session lifecycle management (PENDING → READY → EXPIRED → DESTROYED) +- Professional (~40s) and Enterprise (~75s) demo profiles +- Frontend polling mechanism for status updates +- Session extension and retry capabilities + +**Session Statuses:** +- PENDING: Data cloning in progress +- READY: All data loaded, ready to use +- PARTIAL: Some services failed, others succeeded +- FAILED: Cloning failed +- EXPIRED: Session TTL exceeded +- DESTROYED: Session terminated + +**Business Value:** +- 60-70% performance improvement (5-15s vs 30-40s) +- 100% reduction in Kubernetes Jobs (30+ → 0) +- Deterministic data loading with zero ID collisions +- Complete session isolation for demo accounts + +**Technology:** FastAPI, PostgreSQL, Redis, Async background tasks + +--- + +**21. Distribution Service** ([services/distribution/README.md](../services/distribution/README.md)) +**961+ lines | Enterprise Fleet Management & Route Optimization** + +**Key Features:** +- VRP-based route optimization using Google OR-Tools +- Real-time shipment tracking with GPS and proof of delivery +- Delivery scheduling with recurring patterns +- Haversine distance calculation for accurate routing +- Parent-child tenant hierarchy integration +- Enterprise subscription gating with tier validation + +**Event Types Published:** +- Distribution plan created +- Shipment status updated +- Delivery completed with proof + +**Business Value:** +- Route Efficiency: 20-30% distance reduction +- Fuel Savings: €200-500/month per vehicle +- Delivery Success Rate: 95-98% on-time delivery +- Time Savings: 10-15 hours/week on route planning +- ROI: 250-400% within 12 months for 5+ locations + +**Technology:** FastAPI, PostgreSQL, Google OR-Tools, RabbitMQ, NumPy --- @@ -794,8 +961,8 @@ Bakery-IA represents a **complete, production-ready AI-powered SaaS platform** s --- -**Document Version**: 2.0 -**Last Updated**: November 26, 2025 +**Document Version**: 3.0 +**Last Updated**: December 19, 2025 **Prepared For**: VUE Madrid (Ventanilla Única Empresarial) **Company**: Bakery-IA diff --git a/frontend/src/components/domain/unified-wizard/ItemTypeSelector.tsx b/frontend/src/components/domain/unified-wizard/ItemTypeSelector.tsx index f37d3752..fd734eca 100644 --- a/frontend/src/components/domain/unified-wizard/ItemTypeSelector.tsx +++ b/frontend/src/components/domain/unified-wizard/ItemTypeSelector.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Package, @@ -13,6 +13,8 @@ import { Sparkles, FileText, Factory, + Search, + X, } from 'lucide-react'; export type ItemType = @@ -36,6 +38,8 @@ export interface ItemTypeConfig { badge?: string; badgeColor?: string; isHighlighted?: boolean; + category: 'daily' | 'common' | 'setup'; + keywords?: string[]; } export const ITEM_TYPES: ItemTypeConfig[] = [ @@ -47,6 +51,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ badge: '⭐ Más Común', badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold', isHighlighted: true, + category: 'daily', + keywords: ['ventas', 'sales', 'ingresos', 'caja', 'revenue'], }, { id: 'inventory', @@ -55,6 +61,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: Package, badge: 'Configuración', badgeColor: 'bg-blue-100 text-blue-700', + category: 'setup', + keywords: ['inventario', 'inventory', 'stock', 'ingredientes', 'productos'], }, { id: 'supplier', @@ -63,6 +71,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: Building, badge: 'Configuración', badgeColor: 'bg-blue-100 text-blue-700', + category: 'setup', + keywords: ['proveedor', 'supplier', 'vendor', 'distribuidor'], }, { id: 'recipe', @@ -71,6 +81,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: ChefHat, badge: 'Común', badgeColor: 'bg-green-100 text-green-700', + category: 'common', + keywords: ['receta', 'recipe', 'formula', 'producción'], }, { id: 'equipment', @@ -79,6 +91,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: Wrench, badge: 'Configuración', badgeColor: 'bg-blue-100 text-blue-700', + category: 'setup', + keywords: ['equipo', 'equipment', 'maquinaria', 'horno', 'mixer'], }, { id: 'quality-template', @@ -87,6 +101,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: ClipboardCheck, badge: 'Configuración', badgeColor: 'bg-blue-100 text-blue-700', + category: 'setup', + keywords: ['calidad', 'quality', 'control', 'estándares', 'inspección'], }, { id: 'customer-order', @@ -95,6 +111,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: ShoppingCart, badge: 'Diario', badgeColor: 'bg-amber-100 text-amber-700', + category: 'daily', + keywords: ['pedido', 'order', 'cliente', 'customer', 'orden'], }, { id: 'customer', @@ -103,6 +121,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: Users, badge: 'Común', badgeColor: 'bg-green-100 text-green-700', + category: 'common', + keywords: ['cliente', 'customer', 'comprador', 'contacto'], }, { id: 'team-member', @@ -111,6 +131,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: UserPlus, badge: 'Configuración', badgeColor: 'bg-blue-100 text-blue-700', + category: 'setup', + keywords: ['empleado', 'employee', 'team', 'staff', 'usuario'], }, { id: 'purchase-order', @@ -119,6 +141,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: FileText, badge: 'Diario', badgeColor: 'bg-amber-100 text-amber-700', + category: 'daily', + keywords: ['compra', 'purchase', 'orden', 'proveedor', 'abastecimiento'], }, { id: 'production-batch', @@ -127,6 +151,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [ icon: Factory, badge: 'Diario', badgeColor: 'bg-amber-100 text-amber-700', + category: 'daily', + keywords: ['producción', 'production', 'lote', 'batch', 'fabricación'], }, ]; @@ -136,6 +162,36 @@ interface ItemTypeSelectorProps { export const ItemTypeSelector: React.FC = ({ onSelect }) => { const { t } = useTranslation('wizards'); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState<'all' | 'daily' | 'common' | 'setup'>('all'); + + // Filter items based on search and category + const filteredItems = useMemo(() => { + return ITEM_TYPES.filter(item => { + // Category filter + if (selectedCategory !== 'all' && item.category !== selectedCategory) { + return false; + } + + // Search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + const matchesTitle = item.title.toLowerCase().includes(query); + const matchesSubtitle = item.subtitle.toLowerCase().includes(query); + const matchesKeywords = item.keywords?.some(keyword => keyword.toLowerCase().includes(query)); + return matchesTitle || matchesSubtitle || matchesKeywords; + } + + return true; + }); + }, [searchQuery, selectedCategory]); + + const categoryLabels = { + all: 'Todos', + daily: 'Diario', + common: 'Común', + setup: 'Configuración', + }; return (
@@ -154,9 +210,60 @@ export const ItemTypeSelector: React.FC = ({ onSelect })

+ {/* Search and Filters */} +
+ {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + placeholder="Buscar por nombre o categoría..." + className="w-full pl-10 pr-10 py-2.5 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)]" + /> + {searchQuery && ( + + )} +
+ + {/* Category Filters */} +
+ {(['all', 'daily', 'common', 'setup'] as const).map((category) => ( + + ))} +
+
+ + {/* Results Count */} + {(searchQuery || selectedCategory !== 'all') && ( +
+ {filteredItems.length === 0 ? ( +

No se encontraron resultados

+ ) : ( +

{filteredItems.length} {filteredItems.length === 1 ? 'resultado' : 'resultados'}

+ )} +
+ )} + {/* Item Type Grid */}
- {ITEM_TYPES.map((itemType) => { + {filteredItems.map((itemType) => { const Icon = itemType.icon; const isHighlighted = itemType.isHighlighted; diff --git a/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx b/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx index 8fc8ebdf..81a356c7 100644 --- a/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx @@ -97,26 +97,40 @@ export const UnifiedAddWizard: React.FC = ({ // Handle Purchase Order submission if (selectedItemType === 'purchase-order') { + // Validate items have positive quantities and prices + if ((finalData.items || []).some((item: any) => + Number(item.ordered_quantity) < 0.01 || Number(item.unit_price) < 0.01 + )) { + throw new Error('Todos los productos deben tener cantidad y precio mayor a 0'); + } + const subtotal = (finalData.items || []).reduce( - (sum: number, item: any) => sum + (item.subtotal || 0), + (sum: number, item: any) => sum + (Number(item.ordered_quantity) * Number(item.unit_price)), 0 ); + // Convert date string to ISO datetime with timezone (start of day in local timezone) + const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00'); + if (isNaN(deliveryDate.getTime())) { + throw new Error('Fecha de entrega inválida'); + } + const requiredDeliveryDateTime = deliveryDate.toISOString(); + await createPurchaseOrderMutation.mutateAsync({ tenantId: currentTenant.id, data: { supplier_id: finalData.supplier_id, - required_delivery_date: finalData.required_delivery_date, + required_delivery_date: requiredDeliveryDateTime, priority: finalData.priority || 'normal', - subtotal: String(subtotal), - tax_amount: String(finalData.tax_amount || 0), - shipping_cost: String(finalData.shipping_cost || 0), - discount_amount: String(finalData.discount_amount || 0), + subtotal: subtotal, + tax_amount: Number(finalData.tax_amount) || 0, + shipping_cost: Number(finalData.shipping_cost) || 0, + discount_amount: Number(finalData.discount_amount) || 0, notes: finalData.notes || undefined, items: (finalData.items || []).map((item: any) => ({ inventory_product_id: item.inventory_product_id, - ordered_quantity: item.ordered_quantity, - unit_price: String(item.unit_price), + ordered_quantity: Number(item.ordered_quantity), + unit_price: Number(item.unit_price), unit_of_measure: item.unit_of_measure, })), }, @@ -126,17 +140,37 @@ export const UnifiedAddWizard: React.FC = ({ // Handle Production Batch submission if (selectedItemType === 'production-batch') { + // Validate quantities + if (Number(finalData.planned_quantity) < 0.01) { + throw new Error('La cantidad planificada debe ser mayor a 0'); + } + if (Number(finalData.planned_duration_minutes) < 1) { + throw new Error('La duración planificada debe ser mayor a 0'); + } + // Convert staff_assigned from string to array const staffArray = finalData.staff_assigned_string ? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0) : []; + // Convert datetime-local strings to ISO datetime with timezone + const plannedStartDate = new Date(finalData.planned_start_time); + const plannedEndDate = new Date(finalData.planned_end_time); + + if (isNaN(plannedStartDate.getTime()) || isNaN(plannedEndDate.getTime())) { + throw new Error('Fechas de inicio o fin inválidas'); + } + + if (plannedEndDate <= plannedStartDate) { + throw new Error('La fecha de fin debe ser posterior a la fecha de inicio'); + } + const batchData: ProductionBatchCreate = { product_id: finalData.product_id, product_name: finalData.product_name, recipe_id: finalData.recipe_id || undefined, - planned_start_time: finalData.planned_start_time, - planned_end_time: finalData.planned_end_time, + planned_start_time: plannedStartDate.toISOString(), + planned_end_time: plannedEndDate.toISOString(), planned_quantity: Number(finalData.planned_quantity), planned_duration_minutes: Number(finalData.planned_duration_minutes), priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum, diff --git a/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx index 75630501..dfbabb51 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/PurchaseOrderWizard.tsx @@ -702,10 +702,10 @@ export const PurchaseOrderWizardSteps = ( return 'Debes agregar al menos un producto'; } const invalidItems = data.items.some( - (item: any) => !item.inventory_product_id || item.ordered_quantity <= 0 || item.unit_price <= 0 + (item: any) => !item.inventory_product_id || item.ordered_quantity < 0.01 || item.unit_price < 0.01 ); if (invalidItems) { - return 'Todos los productos deben tener ingrediente, cantidad y precio válidos'; + return 'Todos los productos deben tener ingrediente, cantidad mayor a 0 y precio mayor a 0'; } return true; }, diff --git a/frontend/src/components/ui/WizardModal/WizardModal.tsx b/frontend/src/components/ui/WizardModal/WizardModal.tsx index 9590acc2..d500a157 100644 --- a/frontend/src/components/ui/WizardModal/WizardModal.tsx +++ b/frontend/src/components/ui/WizardModal/WizardModal.tsx @@ -1,5 +1,5 @@ -import React, { useState, useCallback } from 'react'; -import { X, ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { X, ChevronLeft, ChevronRight, AlertCircle, CheckCircle } from 'lucide-react'; export interface WizardStep { id: string; @@ -50,6 +50,8 @@ export const WizardModal: React.FC = ({ }) => { const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const [validationSuccess, setValidationSuccess] = useState(false); const currentStep = steps[currentStepIndex]; const isFirstStep = currentStepIndex === 0; @@ -65,6 +67,8 @@ export const WizardModal: React.FC = ({ const handleClose = useCallback(() => { setCurrentStepIndex(0); + setValidationError(null); + setValidationSuccess(false); onClose(); }, [onClose]); @@ -80,6 +84,8 @@ export const WizardModal: React.FC = ({ }, [steps.length]); const handleBack = useCallback(() => { + setValidationError(null); + setValidationSuccess(false); setCurrentStepIndex(prev => Math.max(prev - 1, 0)); }, []); @@ -88,17 +94,26 @@ export const WizardModal: React.FC = ({ const step = steps[currentStepIndex]; const lastStep = currentStepIndex === steps.length - 1; + // Clear previous validation messages + setValidationError(null); + setValidationSuccess(false); + // Validate current step if validator exists if (step.validate) { setIsValidating(true); try { const isValid = await step.validate(); if (!isValid) { + setValidationError('Por favor, completa todos los campos requeridos correctamente.'); setIsValidating(false); return; } + // Show brief success indicator + setValidationSuccess(true); + setTimeout(() => setValidationSuccess(false), 1000); } catch (error) { console.error('Validation error:', error); + setValidationError(error instanceof Error ? error.message : 'Error de validación. Por favor, verifica los campos.'); setIsValidating(false); return; } @@ -112,6 +127,41 @@ export const WizardModal: React.FC = ({ } }, [steps, currentStepIndex, handleComplete]); + // Keyboard navigation + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + // Don't handle keyboard events if user is typing in an input/textarea + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') { + return; + } + + switch (e.key) { + case 'Escape': + handleClose(); + break; + case 'ArrowLeft': + if (!isFirstStep && !isValidating) { + e.preventDefault(); + handleBack(); + } + break; + case 'ArrowRight': + case 'Enter': + if (!isValidating) { + e.preventDefault(); + handleNext(); + } + break; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, isFirstStep, isValidating, handleClose, handleBack, handleNext]); + if (!isOpen) return null; const StepComponent = currentStep.component; @@ -132,61 +182,113 @@ export const WizardModal: React.FC = ({ onClick={(e) => e.stopPropagation()} > {/* Header */} -
+
{/* Title Bar */} -
-
+
+
{icon && ( -
+
{icon}
)} -
-

+
+

{title}

-

- {currentStep.description || `Step ${currentStepIndex + 1} of ${steps.length}`} +

+ {currentStep.description || `Paso ${currentStepIndex + 1} de ${steps.length}`}

- {/* Progress Bar */} -
-
- {steps.map((step, index) => ( - - + + {/* Tooltip on hover */} +
+ {step.title} +
+
+
+ ); + })}
-
- {currentStep.title} - {currentStepIndex + 1} / {steps.length} +
+
+ {currentStep.title} + {currentStep.isOptional && ( + + Opcional + + )} +
+ + {currentStepIndex + 1} / {steps.length} +
{/* Step Content */} -
+
+ {/* Validation Messages */} + {validationError && ( +
+ +
+

{validationError}

+
+ +
+ )} + + {validationSuccess && ( +
+ +

¡Validación exitosa!

+
+ )} + = ({
{/* Footer Navigation */} -
-
- {/* Back Button */} +
+ {/* Keyboard Shortcuts Hint */} +
+ + ESC + Cerrar + {!isFirstStep && ( + + + Atrás + + )} + + + ENTER + {isLastStep ? 'Completar' : 'Siguiente'} + +
+ +
+ {/* Back Button */} + {!isFirstStep ? ( + ) : ( +
)} -
- {/* Skip Button (for optional steps) */} {currentStep.isOptional && !isLastStep && ( )} + {/* Spacer */} +
+ {/* Next/Complete Button */}