Update readmes and imporve UI
This commit is contained in:
@@ -11,7 +11,7 @@ Bakery-IA is an **AI-powered SaaS platform** designed specifically for the Spani
|
|||||||
## Platform Architecture Overview
|
## Platform Architecture Overview
|
||||||
|
|
||||||
### System Design
|
### System Design
|
||||||
- **Architecture Pattern**: Microservices (18 independent services)
|
- **Architecture Pattern**: Microservices (21 independent services)
|
||||||
- **API Gateway**: Centralized routing with JWT authentication
|
- **API Gateway**: Centralized routing with JWT authentication
|
||||||
- **Frontend**: React 18 + TypeScript progressive web application
|
- **Frontend**: React 18 + TypeScript progressive web application
|
||||||
- **Database Strategy**: PostgreSQL 17 per service (database-per-service pattern)
|
- **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
|
## 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))
|
### 🎯 **New: Alert System Architecture** ([docs/ALERT-SYSTEM-ARCHITECTURE.md](./ALERT-SYSTEM-ARCHITECTURE.md))
|
||||||
**2,800+ lines | Complete Alert System Documentation**
|
**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**
|
**700+ lines | Centralized Entry Point**
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
- Single API endpoint for 18+ microservices
|
- Single API endpoint for 21 microservices
|
||||||
- JWT authentication with 15-minute token cache
|
- JWT authentication with 15-minute token cache
|
||||||
- Rate limiting (300 req/min per client)
|
- Rate limiting (300 req/min per client)
|
||||||
- Server-Sent Events (SSE) for real-time alerts
|
- 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))
|
#### 3. **Forecasting Service** ([services/forecasting/README.md](../services/forecasting/README.md))
|
||||||
**850+ lines | AI Demand Prediction Core**
|
**850+ lines | AI Demand Prediction Core**
|
||||||
|
|
||||||
@@ -297,54 +346,120 @@ Data Collection → Feature Engineering → Prophet Training
|
|||||||
### Core Business Services
|
### Core Business Services
|
||||||
|
|
||||||
**7. Inventory Service** ([services/inventory/README.md](../services/inventory/README.md))
|
**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:**
|
**Key Features:**
|
||||||
- Stock tracking with FIFO (First-In-First-Out)
|
- Comprehensive ingredient management with FIFO consumption and batch tracking
|
||||||
- Expiration management and alerts
|
- Automatic stock updates from delivery events with batch/expiry tracking
|
||||||
- Low stock alerts with intelligent thresholds
|
- HACCP-compliant food safety monitoring with temperature logging
|
||||||
- Food safety compliance (HACCP)
|
- Expiration management with automated FIFO rotation and waste tracking
|
||||||
- Barcode support
|
- Multi-location inventory tracking across storage locations
|
||||||
- **Stock Receipt System (NEW)**:
|
- Enterprise: Automatic inventory transfer processing for internal shipments
|
||||||
|
- **Stock Receipt System**:
|
||||||
- Lot-level tracking with expiration dates (food safety requirement)
|
- Lot-level tracking with expiration dates (food safety requirement)
|
||||||
- Purchase order integration with discrepancy tracking
|
- Purchase order integration with discrepancy tracking
|
||||||
- Draft/Confirmed receipt workflow
|
- Draft/Confirmed receipt workflow with line item validation
|
||||||
- Line item validation (sum of lots must equal actual quantity)
|
- Alert integration and automatic resolution on confirmation
|
||||||
- Alert integration (DELIVERY_ARRIVING_SOON, STOCK_RECEIPT_INCOMPLETE)
|
- Atomic transactions for stock updates and PO status changes
|
||||||
- HACCP compliance enforcement (expiration dates required for perishables)
|
|
||||||
- Atomic transaction on confirmation (stock updates, lot creation, PO status update, alert resolution)
|
**Alert Types Published:**
|
||||||
|
- Low stock alerts (below reorder point)
|
||||||
|
- Expiring soon alerts (within threshold days)
|
||||||
|
- Food safety alerts (temperature violations)
|
||||||
|
|
||||||
**Business Value:**
|
**Business Value:**
|
||||||
- 100% food safety compliance (lot traceability)
|
- Waste Reduction: 20-40% through FIFO and expiry management
|
||||||
- 95% delivery discrepancy detection
|
- Cost Savings: €200-600/month from reduced waste
|
||||||
- 30% faster receiving process
|
- Time Savings: 8-12 hours/week on manual tracking
|
||||||
- Automatic alert resolution on receipt confirmation
|
- 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**
|
**8. Production Service** ([services/production/README.md](../services/production/README.md))
|
||||||
- Production scheduling
|
**394+ lines | Manufacturing Operations Core**
|
||||||
- Batch tracking
|
|
||||||
- Quality control
|
**Key Features:**
|
||||||
- Equipment management
|
- Automated forecast-driven scheduling (7-day advance planning)
|
||||||
- Capacity 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**
|
**9. Recipes Service**
|
||||||
- Recipe management
|
- Recipe management with versioning
|
||||||
- Ingredient quantities
|
- Ingredient quantities and scaling
|
||||||
- Batch scaling
|
- Batch size calculation
|
||||||
- Cost calculation
|
- Cost estimation and margin analysis
|
||||||
|
- Production instructions
|
||||||
|
|
||||||
**10. Orders Service**
|
---
|
||||||
- Customer order management
|
|
||||||
- Order lifecycle tracking
|
|
||||||
- Customer database
|
|
||||||
|
|
||||||
**11. Procurement Service**
|
**10. Orders Service** ([services/orders/README.md](../services/orders/README.md))
|
||||||
- Automated procurement planning
|
**833+ lines | Customer Order Management**
|
||||||
- Purchase order management
|
|
||||||
- Supplier integration
|
**Key Features:**
|
||||||
- Replenishment planning
|
- 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**
|
**12. Suppliers Service**
|
||||||
- Supplier database
|
- Supplier database
|
||||||
@@ -454,9 +569,61 @@ Data Collection → Feature Engineering → Prophet Training
|
|||||||
|
|
||||||
**Technology:** FastAPI, PostgreSQL, RabbitMQ, Kubernetes CronJobs
|
**Technology:** FastAPI, PostgreSQL, RabbitMQ, Kubernetes CronJobs
|
||||||
|
|
||||||
**20. Demo Session Service**
|
**20. Demo Session Service** ([services/demo_session/README.md](../services/demo_session/README.md))
|
||||||
- Ephemeral demo environments
|
**708+ lines | Demo Environment Management**
|
||||||
- Isolated demo accounts
|
|
||||||
|
**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
|
**Document Version**: 3.0
|
||||||
**Last Updated**: November 26, 2025
|
**Last Updated**: December 19, 2025
|
||||||
**Prepared For**: VUE Madrid (Ventanilla Única Empresarial)
|
**Prepared For**: VUE Madrid (Ventanilla Única Empresarial)
|
||||||
**Company**: Bakery-IA
|
**Company**: Bakery-IA
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
FileText,
|
FileText,
|
||||||
Factory,
|
Factory,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export type ItemType =
|
export type ItemType =
|
||||||
@@ -36,6 +38,8 @@ export interface ItemTypeConfig {
|
|||||||
badge?: string;
|
badge?: string;
|
||||||
badgeColor?: string;
|
badgeColor?: string;
|
||||||
isHighlighted?: boolean;
|
isHighlighted?: boolean;
|
||||||
|
category: 'daily' | 'common' | 'setup';
|
||||||
|
keywords?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ITEM_TYPES: ItemTypeConfig[] = [
|
export const ITEM_TYPES: ItemTypeConfig[] = [
|
||||||
@@ -47,6 +51,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
badge: '⭐ Más Común',
|
badge: '⭐ Más Común',
|
||||||
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
|
||||||
isHighlighted: true,
|
isHighlighted: true,
|
||||||
|
category: 'daily',
|
||||||
|
keywords: ['ventas', 'sales', 'ingresos', 'caja', 'revenue'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'inventory',
|
id: 'inventory',
|
||||||
@@ -55,6 +61,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: Package,
|
icon: Package,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
|
category: 'setup',
|
||||||
|
keywords: ['inventario', 'inventory', 'stock', 'ingredientes', 'productos'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'supplier',
|
id: 'supplier',
|
||||||
@@ -63,6 +71,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: Building,
|
icon: Building,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
|
category: 'setup',
|
||||||
|
keywords: ['proveedor', 'supplier', 'vendor', 'distribuidor'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recipe',
|
id: 'recipe',
|
||||||
@@ -71,6 +81,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: ChefHat,
|
icon: ChefHat,
|
||||||
badge: 'Común',
|
badge: 'Común',
|
||||||
badgeColor: 'bg-green-100 text-green-700',
|
badgeColor: 'bg-green-100 text-green-700',
|
||||||
|
category: 'common',
|
||||||
|
keywords: ['receta', 'recipe', 'formula', 'producción'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'equipment',
|
id: 'equipment',
|
||||||
@@ -79,6 +91,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: Wrench,
|
icon: Wrench,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
|
category: 'setup',
|
||||||
|
keywords: ['equipo', 'equipment', 'maquinaria', 'horno', 'mixer'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quality-template',
|
id: 'quality-template',
|
||||||
@@ -87,6 +101,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: ClipboardCheck,
|
icon: ClipboardCheck,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
|
category: 'setup',
|
||||||
|
keywords: ['calidad', 'quality', 'control', 'estándares', 'inspección'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'customer-order',
|
id: 'customer-order',
|
||||||
@@ -95,6 +111,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
badge: 'Diario',
|
badge: 'Diario',
|
||||||
badgeColor: 'bg-amber-100 text-amber-700',
|
badgeColor: 'bg-amber-100 text-amber-700',
|
||||||
|
category: 'daily',
|
||||||
|
keywords: ['pedido', 'order', 'cliente', 'customer', 'orden'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'customer',
|
id: 'customer',
|
||||||
@@ -103,6 +121,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
badge: 'Común',
|
badge: 'Común',
|
||||||
badgeColor: 'bg-green-100 text-green-700',
|
badgeColor: 'bg-green-100 text-green-700',
|
||||||
|
category: 'common',
|
||||||
|
keywords: ['cliente', 'customer', 'comprador', 'contacto'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'team-member',
|
id: 'team-member',
|
||||||
@@ -111,6 +131,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: UserPlus,
|
icon: UserPlus,
|
||||||
badge: 'Configuración',
|
badge: 'Configuración',
|
||||||
badgeColor: 'bg-blue-100 text-blue-700',
|
badgeColor: 'bg-blue-100 text-blue-700',
|
||||||
|
category: 'setup',
|
||||||
|
keywords: ['empleado', 'employee', 'team', 'staff', 'usuario'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'purchase-order',
|
id: 'purchase-order',
|
||||||
@@ -119,6 +141,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: FileText,
|
icon: FileText,
|
||||||
badge: 'Diario',
|
badge: 'Diario',
|
||||||
badgeColor: 'bg-amber-100 text-amber-700',
|
badgeColor: 'bg-amber-100 text-amber-700',
|
||||||
|
category: 'daily',
|
||||||
|
keywords: ['compra', 'purchase', 'orden', 'proveedor', 'abastecimiento'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'production-batch',
|
id: 'production-batch',
|
||||||
@@ -127,6 +151,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
|
|||||||
icon: Factory,
|
icon: Factory,
|
||||||
badge: 'Diario',
|
badge: 'Diario',
|
||||||
badgeColor: 'bg-amber-100 text-amber-700',
|
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<ItemTypeSelectorProps> = ({ onSelect }) => {
|
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
|
||||||
const { t } = useTranslation('wizards');
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -154,9 +210,60 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-[var(--bg-secondary)] rounded transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filters */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(['all', 'daily', 'common', 'setup'] as const).map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'bg-[var(--color-primary)] text-white shadow-md'
|
||||||
|
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{categoryLabels[category]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results Count */}
|
||||||
|
{(searchQuery || selectedCategory !== 'all') && (
|
||||||
|
<div className="text-sm text-[var(--text-secondary)]">
|
||||||
|
{filteredItems.length === 0 ? (
|
||||||
|
<p className="text-center py-4">No se encontraron resultados</p>
|
||||||
|
) : (
|
||||||
|
<p>{filteredItems.length} {filteredItems.length === 1 ? 'resultado' : 'resultados'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Item Type Grid */}
|
{/* Item Type Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
|
||||||
{ITEM_TYPES.map((itemType) => {
|
{filteredItems.map((itemType) => {
|
||||||
const Icon = itemType.icon;
|
const Icon = itemType.icon;
|
||||||
const isHighlighted = itemType.isHighlighted;
|
const isHighlighted = itemType.isHighlighted;
|
||||||
|
|
||||||
|
|||||||
@@ -97,26 +97,40 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
|
|
||||||
// Handle Purchase Order submission
|
// Handle Purchase Order submission
|
||||||
if (selectedItemType === 'purchase-order') {
|
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(
|
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
|
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({
|
await createPurchaseOrderMutation.mutateAsync({
|
||||||
tenantId: currentTenant.id,
|
tenantId: currentTenant.id,
|
||||||
data: {
|
data: {
|
||||||
supplier_id: finalData.supplier_id,
|
supplier_id: finalData.supplier_id,
|
||||||
required_delivery_date: finalData.required_delivery_date,
|
required_delivery_date: requiredDeliveryDateTime,
|
||||||
priority: finalData.priority || 'normal',
|
priority: finalData.priority || 'normal',
|
||||||
subtotal: String(subtotal),
|
subtotal: subtotal,
|
||||||
tax_amount: String(finalData.tax_amount || 0),
|
tax_amount: Number(finalData.tax_amount) || 0,
|
||||||
shipping_cost: String(finalData.shipping_cost || 0),
|
shipping_cost: Number(finalData.shipping_cost) || 0,
|
||||||
discount_amount: String(finalData.discount_amount || 0),
|
discount_amount: Number(finalData.discount_amount) || 0,
|
||||||
notes: finalData.notes || undefined,
|
notes: finalData.notes || undefined,
|
||||||
items: (finalData.items || []).map((item: any) => ({
|
items: (finalData.items || []).map((item: any) => ({
|
||||||
inventory_product_id: item.inventory_product_id,
|
inventory_product_id: item.inventory_product_id,
|
||||||
ordered_quantity: item.ordered_quantity,
|
ordered_quantity: Number(item.ordered_quantity),
|
||||||
unit_price: String(item.unit_price),
|
unit_price: Number(item.unit_price),
|
||||||
unit_of_measure: item.unit_of_measure,
|
unit_of_measure: item.unit_of_measure,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@@ -126,17 +140,37 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
|
|||||||
|
|
||||||
// Handle Production Batch submission
|
// Handle Production Batch submission
|
||||||
if (selectedItemType === 'production-batch') {
|
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
|
// Convert staff_assigned from string to array
|
||||||
const staffArray = finalData.staff_assigned_string
|
const staffArray = finalData.staff_assigned_string
|
||||||
? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
|
? 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 = {
|
const batchData: ProductionBatchCreate = {
|
||||||
product_id: finalData.product_id,
|
product_id: finalData.product_id,
|
||||||
product_name: finalData.product_name,
|
product_name: finalData.product_name,
|
||||||
recipe_id: finalData.recipe_id || undefined,
|
recipe_id: finalData.recipe_id || undefined,
|
||||||
planned_start_time: finalData.planned_start_time,
|
planned_start_time: plannedStartDate.toISOString(),
|
||||||
planned_end_time: finalData.planned_end_time,
|
planned_end_time: plannedEndDate.toISOString(),
|
||||||
planned_quantity: Number(finalData.planned_quantity),
|
planned_quantity: Number(finalData.planned_quantity),
|
||||||
planned_duration_minutes: Number(finalData.planned_duration_minutes),
|
planned_duration_minutes: Number(finalData.planned_duration_minutes),
|
||||||
priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum,
|
priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum,
|
||||||
|
|||||||
@@ -702,10 +702,10 @@ export const PurchaseOrderWizardSteps = (
|
|||||||
return 'Debes agregar al menos un producto';
|
return 'Debes agregar al menos un producto';
|
||||||
}
|
}
|
||||||
const invalidItems = data.items.some(
|
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) {
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { X, ChevronLeft, ChevronRight, AlertCircle, CheckCircle } from 'lucide-react';
|
||||||
|
|
||||||
export interface WizardStep {
|
export interface WizardStep {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -50,6 +50,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
const [isValidating, setIsValidating] = useState(false);
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const [validationSuccess, setValidationSuccess] = useState(false);
|
||||||
|
|
||||||
const currentStep = steps[currentStepIndex];
|
const currentStep = steps[currentStepIndex];
|
||||||
const isFirstStep = currentStepIndex === 0;
|
const isFirstStep = currentStepIndex === 0;
|
||||||
@@ -65,6 +67,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setCurrentStepIndex(0);
|
setCurrentStepIndex(0);
|
||||||
|
setValidationError(null);
|
||||||
|
setValidationSuccess(false);
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
@@ -80,6 +84,8 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
}, [steps.length]);
|
}, [steps.length]);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
|
setValidationError(null);
|
||||||
|
setValidationSuccess(false);
|
||||||
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
|
setCurrentStepIndex(prev => Math.max(prev - 1, 0));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -88,17 +94,26 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
const step = steps[currentStepIndex];
|
const step = steps[currentStepIndex];
|
||||||
const lastStep = currentStepIndex === steps.length - 1;
|
const lastStep = currentStepIndex === steps.length - 1;
|
||||||
|
|
||||||
|
// Clear previous validation messages
|
||||||
|
setValidationError(null);
|
||||||
|
setValidationSuccess(false);
|
||||||
|
|
||||||
// Validate current step if validator exists
|
// Validate current step if validator exists
|
||||||
if (step.validate) {
|
if (step.validate) {
|
||||||
setIsValidating(true);
|
setIsValidating(true);
|
||||||
try {
|
try {
|
||||||
const isValid = await step.validate();
|
const isValid = await step.validate();
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
setValidationError('Por favor, completa todos los campos requeridos correctamente.');
|
||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Show brief success indicator
|
||||||
|
setValidationSuccess(true);
|
||||||
|
setTimeout(() => setValidationSuccess(false), 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Validation error:', error);
|
console.error('Validation error:', error);
|
||||||
|
setValidationError(error instanceof Error ? error.message : 'Error de validación. Por favor, verifica los campos.');
|
||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,6 +127,41 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
}
|
}
|
||||||
}, [steps, currentStepIndex, handleComplete]);
|
}, [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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const StepComponent = currentStep.component;
|
const StepComponent = currentStep.component;
|
||||||
@@ -132,61 +182,113 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky top-0 z-10 bg-[var(--bg-primary)] border-b border-[var(--border-secondary)]">
|
<div className="sticky top-0 z-10 bg-[var(--bg-primary)] border-b border-[var(--border-secondary)] shadow-sm">
|
||||||
{/* Title Bar */}
|
{/* Title Bar */}
|
||||||
<div className="flex items-center justify-between p-6 pb-4">
|
<div className="flex items-center justify-between p-4 sm:p-6 pb-3 sm:pb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="w-10 h-10 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center text-[var(--color-primary)]">
|
<div className="w-10 h-10 sm:w-12 sm:h-12 flex-shrink-0 rounded-xl bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 flex items-center justify-center text-[var(--color-primary)] shadow-sm">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
<h2 className="text-lg sm:text-xl font-bold text-[var(--text-primary)] truncate">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-[var(--text-secondary)] mt-0.5">
|
<p className="text-xs sm:text-sm text-[var(--text-secondary)] mt-0.5 truncate">
|
||||||
{currentStep.description || `Step ${currentStepIndex + 1} of ${steps.length}`}
|
{currentStep.description || `Paso ${currentStepIndex + 1} de ${steps.length}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="p-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors"
|
className="p-2 flex-shrink-0 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-all hover:scale-110 active:scale-95"
|
||||||
|
aria-label="Cerrar"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Enhanced Progress Bar */}
|
||||||
<div className="px-6 pb-4">
|
<div className="px-4 sm:px-6 pb-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-1.5 sm:gap-2 mb-2.5">
|
||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => {
|
||||||
<React.Fragment key={step.id}>
|
const isCompleted = index < currentStepIndex;
|
||||||
<button
|
const isCurrent = index === currentStepIndex;
|
||||||
onClick={() => index < currentStepIndex && goToStep(index)}
|
const isUpcoming = index > currentStepIndex;
|
||||||
disabled={index > currentStepIndex}
|
|
||||||
className={`flex-1 h-2 rounded-full transition-all duration-300 ${
|
return (
|
||||||
index < currentStepIndex
|
<div
|
||||||
? 'bg-[var(--color-success)] cursor-pointer hover:bg-[var(--color-success)]/80'
|
key={step.id}
|
||||||
: index === currentStepIndex
|
className="flex-1 group relative"
|
||||||
? 'bg-[var(--color-primary)]'
|
>
|
||||||
: 'bg-[var(--bg-tertiary)] cursor-not-allowed'
|
<button
|
||||||
}`}
|
onClick={() => isCompleted && goToStep(index)}
|
||||||
title={step.title}
|
disabled={!isCompleted}
|
||||||
/>
|
className={`w-full h-2.5 rounded-full transition-all duration-300 relative overflow-hidden ${
|
||||||
</React.Fragment>
|
isCompleted
|
||||||
))}
|
? 'bg-[var(--color-success)] cursor-pointer hover:bg-[var(--color-success)]/80 hover:h-3'
|
||||||
|
: isCurrent
|
||||||
|
? 'bg-[var(--color-primary)] shadow-md'
|
||||||
|
: 'bg-[var(--bg-tertiary)] cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
aria-label={`${step.title} - ${isCompleted ? 'Completado' : isCurrent ? 'En progreso' : 'Pendiente'}`}
|
||||||
|
>
|
||||||
|
{isCurrent && (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Tooltip on hover */}
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded shadow-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-20">
|
||||||
|
{step.title}
|
||||||
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-[var(--border-secondary)]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs text-[var(--text-secondary)]">
|
<div className="flex items-center justify-between text-xs sm:text-sm">
|
||||||
<span className="font-medium">{currentStep.title}</span>
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<span>{currentStepIndex + 1} / {steps.length}</span>
|
<span className="font-semibold text-[var(--text-primary)] truncate">{currentStep.title}</span>
|
||||||
|
{currentStep.isOptional && (
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-[var(--bg-secondary)] text-[var(--text-tertiary)] rounded-full flex-shrink-0">
|
||||||
|
Opcional
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[var(--text-tertiary)] font-medium ml-2 flex-shrink-0">
|
||||||
|
{currentStepIndex + 1} / {steps.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step Content */}
|
{/* Step Content */}
|
||||||
<div className="p-6 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 220px)' }}>
|
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 220px)' }}>
|
||||||
|
{/* Validation Messages */}
|
||||||
|
{validationError && (
|
||||||
|
<div className="mb-4 p-3 sm:p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3 animate-slideDown">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-red-800">{validationError}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setValidationError(null)}
|
||||||
|
className="flex-shrink-0 text-red-400 hover:text-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{validationSuccess && (
|
||||||
|
<div className="mb-4 p-3 sm:p-4 bg-green-50 border border-green-200 rounded-lg flex items-center gap-3 animate-slideDown">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0" />
|
||||||
|
<p className="text-sm font-medium text-green-800">¡Validación exitosa!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<StepComponent
|
<StepComponent
|
||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
@@ -202,52 +304,75 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Navigation */}
|
{/* Footer Navigation */}
|
||||||
<div className="sticky bottom-0 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)]/50 backdrop-blur-sm px-6 py-4">
|
<div className="sticky bottom-0 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)]/80 backdrop-blur-md px-4 sm:px-6 py-3 sm:py-4 shadow-lg">
|
||||||
<div className="flex items-center justify-between gap-3">
|
{/* Keyboard Shortcuts Hint */}
|
||||||
{/* Back Button */}
|
<div className="hidden md:flex items-center justify-center gap-4 text-xs text-[var(--text-tertiary)] mb-2 pb-2 border-b border-[var(--border-secondary)]/50">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<kbd className="px-1.5 py-0.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-[10px] font-mono">ESC</kbd>
|
||||||
|
Cerrar
|
||||||
|
</span>
|
||||||
{!isFirstStep && (
|
{!isFirstStep && (
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<kbd className="px-1.5 py-0.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-[10px] font-mono">←</kbd>
|
||||||
|
Atrás
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<kbd className="px-1.5 py-0.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-[10px] font-mono">→</kbd>
|
||||||
|
<kbd className="px-1.5 py-0.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-[10px] font-mono">ENTER</kbd>
|
||||||
|
{isLastStep ? 'Completar' : 'Siguiente'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2 sm:gap-3">
|
||||||
|
{/* Back Button */}
|
||||||
|
{!isFirstStep ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
className="px-4 py-2.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
|
className="px-3 sm:px-4 py-2.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 sm:gap-2 active:scale-95"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
Back
|
<span className="hidden sm:inline">Atrás</span>
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1" />
|
|
||||||
|
|
||||||
{/* Skip Button (for optional steps) */}
|
{/* Skip Button (for optional steps) */}
|
||||||
{currentStep.isOptional && !isLastStep && (
|
{currentStep.isOptional && !isLastStep && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentStepIndex(prev => prev + 1)}
|
onClick={() => setCurrentStepIndex(prev => prev + 1)}
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
className="px-4 py-2.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-colors font-medium disabled:opacity-50"
|
className="px-3 sm:px-4 py-2.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] rounded-lg transition-all font-medium disabled:opacity-50 text-sm active:scale-95"
|
||||||
>
|
>
|
||||||
Skip This Step
|
Saltar
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* Next/Complete Button */}
|
{/* Next/Complete Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium inline-flex items-center gap-2 min-w-[140px] justify-center"
|
className="px-4 sm:px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-all font-semibold inline-flex items-center gap-2 min-w-[100px] sm:min-w-[140px] justify-center shadow-md hover:shadow-lg active:scale-95"
|
||||||
>
|
>
|
||||||
{isValidating ? (
|
{isValidating ? (
|
||||||
<>
|
<>
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
Validating...
|
<span className="hidden sm:inline">Validando...</span>
|
||||||
</>
|
</>
|
||||||
) : isLastStep ? (
|
) : isLastStep ? (
|
||||||
<>
|
<>
|
||||||
Complete
|
Completar
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Next
|
<span className="hidden sm:inline">Siguiente</span>
|
||||||
|
<span className="sm:hidden">Sig.</span>
|
||||||
<ChevronRight className="w-4 h-4" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -273,12 +398,36 @@ export const WizardModal: React.FC<WizardModalProps> = ({
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
.animate-fadeIn {
|
.animate-fadeIn {
|
||||||
animation: fadeIn 0.2s ease-out;
|
animation: fadeIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
.animate-slideUp {
|
.animate-slideUp {
|
||||||
animation: slideUp 0.3s ease-out;
|
animation: slideUp 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
.animate-slideDown {
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.animate-shimmer {
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ function MyComponent() {
|
|||||||
- ✅ **Conversion CTAs** throughout experience
|
- ✅ **Conversion CTAs** throughout experience
|
||||||
- ✅ **Responsive design** across all devices
|
- ✅ **Responsive design** across all devices
|
||||||
- ✅ **Accessibility** (ARIA, keyboard navigation)
|
- ✅ **Accessibility** (ARIA, keyboard navigation)
|
||||||
|
- ✅ **Demo banner** with session status and time remaining countdown
|
||||||
|
- ✅ **Exit modal** with benefits reminder and conversion messaging
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -91,9 +93,9 @@ Track tour analytics event.
|
|||||||
### Desktop (12 steps)
|
### Desktop (12 steps)
|
||||||
1. Welcome to Demo Session
|
1. Welcome to Demo Session
|
||||||
2. Real-time Metrics Dashboard
|
2. Real-time Metrics Dashboard
|
||||||
3. Intelligent Alerts
|
3. Pending Approvals
|
||||||
4. Procurement Plans
|
4. System Actions Log
|
||||||
5. Production Management
|
5. Daily Production Plan
|
||||||
6. Database Navigation (Sidebar)
|
6. Database Navigation (Sidebar)
|
||||||
7. Daily Operations (Sidebar)
|
7. Daily Operations (Sidebar)
|
||||||
8. Analytics & AI (Sidebar)
|
8. Analytics & AI (Sidebar)
|
||||||
@@ -151,9 +153,9 @@ The tour targets elements with `data-tour` attributes:
|
|||||||
- `demo-banner` - Demo banner
|
- `demo-banner` - Demo banner
|
||||||
- `demo-banner-actions` - Banner action buttons
|
- `demo-banner-actions` - Banner action buttons
|
||||||
- `dashboard-stats` - Metrics grid
|
- `dashboard-stats` - Metrics grid
|
||||||
- `real-time-alerts` - Alerts section
|
- `pending-po-approvals` - Approval requests
|
||||||
- `procurement-plans` - Procurement section
|
- `real-time-alerts` - System actions log
|
||||||
- `production-plans` - Production section
|
- `today-production` - Daily production plan
|
||||||
- `sidebar-database` - Database navigation
|
- `sidebar-database` - Database navigation
|
||||||
- `sidebar-operations` - Operations navigation
|
- `sidebar-operations` - Operations navigation
|
||||||
- `sidebar-analytics` - Analytics navigation
|
- `sidebar-analytics` - Analytics navigation
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ Services → RabbitMQ → [Alert Processor] → PostgreSQL
|
|||||||
|
|
||||||
### Enrichment Pipeline
|
### Enrichment Pipeline
|
||||||
|
|
||||||
1. **Message Generator**: Creates i18n keys and parameters from metadata
|
1. **Duplicate Detection**: Checks for duplicate alerts within 24-hour window
|
||||||
2. **Orchestrator Client**: Queries AI orchestrator for context
|
2. **Message Generator**: Creates i18n keys and parameters from metadata
|
||||||
3. **Business Impact Analyzer**: Calculates financial and operational impact
|
3. **Orchestrator Client**: Queries AI orchestrator for context
|
||||||
4. **Urgency Analyzer**: Assesses time sensitivity and deadlines
|
4. **AI Reasoning Extractor**: Extracts AI reasoning details and confidence scores
|
||||||
5. **User Agency Analyzer**: Determines user's ability to act
|
5. **Business Impact Analyzer**: Calculates financial and operational impact
|
||||||
6. **Priority Scorer**: Calculates weighted priority score (0-100)
|
6. **Urgency Analyzer**: Assesses time sensitivity and deadlines
|
||||||
7. **Smart Action Generator**: Creates contextual action buttons
|
7. **User Agency Analyzer**: Determines user's ability to act
|
||||||
8. **Entity Link Extractor**: Maps metadata to entity references
|
8. **Priority Scorer**: Calculates weighted priority score (0-100)
|
||||||
|
9. **Type Classifier**: Determines if action needed or issue prevented
|
||||||
|
10. **Smart Action Generator**: Creates contextual action buttons
|
||||||
|
11. **Entity Link Extractor**: Maps metadata to entity references
|
||||||
|
|
||||||
## Service Structure
|
## Service Structure
|
||||||
|
|
||||||
@@ -177,12 +180,15 @@ await publisher.publish_alert(
|
|||||||
|
|
||||||
### 2. Alert Processor Enriches
|
### 2. Alert Processor Enriches
|
||||||
|
|
||||||
|
- **Checks for duplicates**: Searches 24-hour window for similar alerts
|
||||||
- Generates i18n: `alerts.critical_stock_shortage.title` with params
|
- Generates i18n: `alerts.critical_stock_shortage.title` with params
|
||||||
- Queries orchestrator for AI context
|
- Queries orchestrator for AI context
|
||||||
|
- Extracts AI reasoning and confidence scores (if available)
|
||||||
- Analyzes business impact: €197.50 financial impact
|
- Analyzes business impact: €197.50 financial impact
|
||||||
- Assesses urgency: 12 hours until consequence
|
- Assesses urgency: 12 hours until consequence
|
||||||
- Determines user agency: Can create PO, requires supplier
|
- Determines user agency: Can create PO, requires supplier
|
||||||
- Calculates priority: Score 78 → "important"
|
- Calculates priority: Score 78 → "important"
|
||||||
|
- Classifies type: `action_needed` or `prevented_issue`
|
||||||
- Generates smart actions: [Create PO, Call Supplier, Dismiss]
|
- Generates smart actions: [Create PO, Call Supplier, Dismiss]
|
||||||
- Extracts entity links: `{ingredient: "..."}`
|
- Extracts entity links: `{ingredient: "..."}`
|
||||||
|
|
||||||
@@ -192,8 +198,12 @@ await publisher.publish_alert(
|
|||||||
{
|
{
|
||||||
"id": "...",
|
"id": "...",
|
||||||
"event_type": "critical_stock_shortage",
|
"event_type": "critical_stock_shortage",
|
||||||
|
"event_domain": "inventory",
|
||||||
|
"severity": "urgent",
|
||||||
|
"type_class": "action_needed",
|
||||||
"priority_score": 78,
|
"priority_score": 78,
|
||||||
"priority_level": "important",
|
"priority_level": "important",
|
||||||
|
"confidence_score": 95,
|
||||||
"i18n": {
|
"i18n": {
|
||||||
"title_key": "alerts.critical_stock_shortage.title",
|
"title_key": "alerts.critical_stock_shortage.title",
|
||||||
"title_params": {"ingredient_name": "Flour"},
|
"title_params": {"ingredient_name": "Flour"},
|
||||||
@@ -206,7 +216,10 @@ await publisher.publish_alert(
|
|||||||
"business_impact": {...},
|
"business_impact": {...},
|
||||||
"urgency": {...},
|
"urgency": {...},
|
||||||
"user_agency": {...},
|
"user_agency": {...},
|
||||||
"smart_actions": [...]
|
"ai_reasoning_details": {...},
|
||||||
|
"orchestrator_context": {...},
|
||||||
|
"smart_actions": [...],
|
||||||
|
"entity_links": {"ingredient": "..."}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -249,30 +262,83 @@ Publishes to Redis channel `alerts:{tenant_id}` for real-time frontend updates.
|
|||||||
|
|
||||||
See [app/utils/message_templates.py](app/utils/message_templates.py) for complete list.
|
See [app/utils/message_templates.py](app/utils/message_templates.py) for complete list.
|
||||||
|
|
||||||
Key alert types:
|
### Standard Alerts
|
||||||
- `critical_stock_shortage`
|
- `critical_stock_shortage` - Urgent stock shortages
|
||||||
- `low_stock_warning`
|
- `low_stock_warning` - Stock running low
|
||||||
- `production_delay`
|
- `production_delay` - Production behind schedule
|
||||||
- `equipment_failure`
|
- `equipment_failure` - Equipment issues
|
||||||
- `po_approval_needed`
|
- `po_approval_needed` - Purchase order approval required
|
||||||
- `temperature_breach`
|
- `temperature_breach` - Temperature control violations
|
||||||
- `delivery_overdue`
|
- `delivery_overdue` - Late deliveries
|
||||||
- `expired_products`
|
- `expired_products` - Product expiration warnings
|
||||||
|
|
||||||
|
### AI Recommendations
|
||||||
|
- `ai_yield_prediction` - AI-predicted production yields
|
||||||
|
- `ai_safety_stock_optimization` - AI stock level recommendations
|
||||||
|
- `ai_supplier_recommendation` - AI supplier suggestions
|
||||||
|
- `ai_price_forecast` - AI price predictions
|
||||||
|
- `ai_demand_forecast` - AI demand forecasts
|
||||||
|
- `ai_business_rule` - AI-suggested business rules
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
**events table** with JSONB enrichment:
|
**events table** with JSONB enrichment:
|
||||||
- Core: `id`, `tenant_id`, `created_at`, `event_type`
|
- Core: `id`, `tenant_id`, `created_at`, `event_type`, `event_domain`, `severity`
|
||||||
- i18n: `i18n_title_key`, `i18n_title_params`, `i18n_message_key`, `i18n_message_params`
|
- i18n: `i18n_title_key`, `i18n_title_params`, `i18n_message_key`, `i18n_message_params`
|
||||||
- Priority: `priority_score` (0-100), `priority_level` (critical/important/standard/info)
|
- Priority: `priority_score` (0-100), `priority_level` (critical/important/standard/info)
|
||||||
- Enrichment: `orchestrator_context`, `business_impact`, `urgency`, `user_agency` (JSONB)
|
- Enrichment: `orchestrator_context`, `business_impact`, `urgency`, `user_agency` (JSONB)
|
||||||
|
- AI Fields: `ai_reasoning_details`, `confidence_score`, `ai_reasoning_summary_key`, `ai_reasoning_summary_params`
|
||||||
|
- Classification: `type_class` (action_needed/prevented_issue)
|
||||||
- Actions: `smart_actions` (JSONB array)
|
- Actions: `smart_actions` (JSONB array)
|
||||||
|
- Entities: `entity_links` (JSONB)
|
||||||
- Status: `status` (active/acknowledged/resolved/dismissed)
|
- Status: `status` (active/acknowledged/resolved/dismissed)
|
||||||
|
- Metadata: `raw_metadata` (JSONB)
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Duplicate Alert Detection
|
||||||
|
|
||||||
|
The service automatically detects and prevents duplicate alerts:
|
||||||
|
- **24-hour window**: Checks for similar alerts in the past 24 hours
|
||||||
|
- **Smart matching**: Compares `tenant_id`, `event_type`, and key metadata fields
|
||||||
|
- **Update strategy**: Updates existing alert instead of creating duplicates
|
||||||
|
- **Metadata preservation**: Keeps enriched data while preventing alert fatigue
|
||||||
|
|
||||||
|
### Type Classification
|
||||||
|
|
||||||
|
Events are classified into two types:
|
||||||
|
- **action_needed**: User action required (default for alerts)
|
||||||
|
- **prevented_issue**: AI already handled the situation (for AI recommendations)
|
||||||
|
|
||||||
|
This helps the frontend display appropriate UI and messaging.
|
||||||
|
|
||||||
|
### AI Reasoning Integration
|
||||||
|
|
||||||
|
When AI orchestrator has acted on an event:
|
||||||
|
- Extracts complete reasoning data structure
|
||||||
|
- Stores confidence scores (0-100)
|
||||||
|
- Generates i18n-friendly reasoning summaries
|
||||||
|
- Links to orchestrator context for full details
|
||||||
|
|
||||||
|
### Notification Service Integration
|
||||||
|
|
||||||
|
Enriched events are automatically sent to the notification service for delivery via:
|
||||||
|
- WhatsApp
|
||||||
|
- Email
|
||||||
|
- Push notifications
|
||||||
|
- SMS
|
||||||
|
|
||||||
|
Priority mapping:
|
||||||
|
- `critical` → urgent priority
|
||||||
|
- `important` → high priority
|
||||||
|
- `standard` → medium priority
|
||||||
|
- `info` → low priority
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
Structured JSON logs with:
|
Structured JSON logs with:
|
||||||
- `enrichment_started` - Event received
|
- `enrichment_started` - Event received
|
||||||
|
- `duplicate_detected` - Duplicate alert found and updated
|
||||||
- `enrichment_completed` - Enrichment pipeline finished
|
- `enrichment_completed` - Enrichment pipeline finished
|
||||||
- `event_stored` - Saved to database
|
- `event_stored` - Saved to database
|
||||||
- `notification_sent` - Notification queued
|
- `notification_sent` - Notification queued
|
||||||
|
|||||||
@@ -172,30 +172,98 @@ graph TD
|
|||||||
- **Use Case**: Central obrador + 3 retail outlets
|
- **Use Case**: Central obrador + 3 retail outlets
|
||||||
- **Features**: VRP-optimized routes, multi-location inventory
|
- **Features**: VRP-optimized routes, multi-location inventory
|
||||||
|
|
||||||
|
## 🎯 API Endpoints
|
||||||
|
|
||||||
|
### Atomic Operations
|
||||||
|
- `GET /api/v1/demo/accounts` - List available demo account types
|
||||||
|
- `POST /api/v1/demo/sessions` - Create new demo session
|
||||||
|
- `GET /api/v1/demo/sessions/{session_id}` - Get session details
|
||||||
|
- `GET /api/v1/demo/sessions/{session_id}/status` - Poll cloning status
|
||||||
|
- `GET /api/v1/demo/sessions/{session_id}/errors` - Get detailed errors
|
||||||
|
- `DELETE /api/v1/demo/sessions/{session_id}` - Destroy session
|
||||||
|
|
||||||
|
### Business Operations
|
||||||
|
- `POST /api/v1/demo/sessions/{session_id}/extend` - Extend session TTL
|
||||||
|
- `POST /api/v1/demo/sessions/{session_id}/retry` - Retry failed cloning
|
||||||
|
- `GET /api/v1/demo/stats` - Session statistics
|
||||||
|
- `POST /api/v1/demo/operations/cleanup` - Cleanup expired sessions
|
||||||
|
- `POST /api/v1/demo/sessions/{session_id}/seed-alerts` - Seed demo alerts
|
||||||
|
|
||||||
|
### Session Lifecycle
|
||||||
|
|
||||||
|
**Statuses:**
|
||||||
|
- `PENDING` - Data cloning in progress
|
||||||
|
- `READY` - All data loaded, ready to use
|
||||||
|
- `PARTIAL` - Some services failed, others succeeded
|
||||||
|
- `FAILED` - One or more services failed completely
|
||||||
|
- `EXPIRED` - Session TTL exceeded
|
||||||
|
- `DESTROYED` - Session terminated
|
||||||
|
|
||||||
|
**Session Duration:**
|
||||||
|
- Default: 2 hours
|
||||||
|
- Extendable via `/extend` endpoint
|
||||||
|
- Extension limit: Configurable per environment
|
||||||
|
|
||||||
|
**Estimated Load Times:**
|
||||||
|
- Professional: ~40 seconds
|
||||||
|
- Enterprise: ~75 seconds (includes child tenants)
|
||||||
|
|
||||||
## 🔧 Usage
|
## 🔧 Usage
|
||||||
|
|
||||||
### Create Demo Session via API
|
### Create Demo Session via API
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Professional demo
|
# Professional demo
|
||||||
curl -X POST http://localhost:8000/api/v1/demo-sessions \
|
curl -X POST http://localhost:8000/api/v1/demo/sessions \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"demo_account_type": "professional",
|
"demo_account_type": "professional",
|
||||||
"email": "test@example.com",
|
"email": "test@example.com"
|
||||||
"subscription_tier": "professional"
|
|
||||||
}'
|
}'
|
||||||
|
|
||||||
# Enterprise demo
|
# Enterprise demo
|
||||||
curl -X POST http://localhost:8000/api/v1/demo-sessions \
|
curl -X POST http://localhost:8000/api/v1/demo/sessions \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"demo_account_type": "enterprise",
|
"demo_account_type": "enterprise",
|
||||||
"email": "test@example.com",
|
"email": "test@example.com"
|
||||||
"subscription_tier": "enterprise"
|
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Poll Session Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if session is ready
|
||||||
|
curl http://localhost:8000/api/v1/demo/sessions/{session_id}/status
|
||||||
|
|
||||||
|
# Response includes per-service progress
|
||||||
|
{
|
||||||
|
"session_id": "demo_xxx",
|
||||||
|
"status": "ready|pending|failed|partial",
|
||||||
|
"progress": {
|
||||||
|
"orders": {"status": "completed", "records": 42},
|
||||||
|
"production": {"status": "in_progress", "records": 15}
|
||||||
|
},
|
||||||
|
"estimated_remaining_seconds": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extend session (add more time)
|
||||||
|
curl -X POST http://localhost:8000/api/v1/demo/sessions/{session_id}/extend
|
||||||
|
|
||||||
|
# Retry failed services
|
||||||
|
curl -X POST http://localhost:8000/api/v1/demo/sessions/{session_id}/retry
|
||||||
|
|
||||||
|
# Get session details
|
||||||
|
curl http://localhost:8000/api/v1/demo/sessions/{session_id}
|
||||||
|
|
||||||
|
# Destroy session (cleanup)
|
||||||
|
curl -X DELETE http://localhost:8000/api/v1/demo/sessions/{session_id}
|
||||||
|
```
|
||||||
|
|
||||||
### Implementation Example
|
### Implementation Example
|
||||||
|
|
||||||
Here's how the Orders service implements direct loading:
|
Here's how the Orders service implements direct loading:
|
||||||
@@ -392,11 +460,12 @@ def adjust_date_for_demo(original_date: datetime, session_time: datetime) -> dat
|
|||||||
|
|
||||||
### Step-by-Step Demo Session Creation
|
### Step-by-Step Demo Session Creation
|
||||||
|
|
||||||
1. **User Request**: Frontend calls `/api/v1/demo-sessions` with demo type
|
1. **User Request**: Frontend calls `/api/v1/demo/sessions` with demo type
|
||||||
2. **Session Setup**: Demo Session Service:
|
2. **Session Setup**: Demo Session Service:
|
||||||
- Generates virtual tenant UUID
|
- Generates virtual tenant UUID
|
||||||
- Records session metadata
|
- Records session metadata (session_id, ip_address, user_agent)
|
||||||
- Calculates session creation timestamp
|
- Calculates session creation timestamp and expiration
|
||||||
|
- For enterprise: Generates child tenant IDs
|
||||||
3. **Parallel Service Calls**: Demo Session Service calls each service's `/internal/demo/clone` endpoint with:
|
3. **Parallel Service Calls**: Demo Session Service calls each service's `/internal/demo/clone` endpoint with:
|
||||||
- `virtual_tenant_id` - Virtual tenant UUID
|
- `virtual_tenant_id` - Virtual tenant UUID
|
||||||
- `demo_account_type` - Profile (professional/enterprise)
|
- `demo_account_type` - Profile (professional/enterprise)
|
||||||
@@ -406,8 +475,10 @@ def adjust_date_for_demo(original_date: datetime, session_time: datetime) -> dat
|
|||||||
- Transforms all IDs using XOR with virtual tenant ID
|
- Transforms all IDs using XOR with virtual tenant ID
|
||||||
- Adjusts all dates relative to session creation time
|
- Adjusts all dates relative to session creation time
|
||||||
- Inserts data into its database within a transaction
|
- Inserts data into its database within a transaction
|
||||||
- Returns success/failure status
|
- Returns success/failure status with record count
|
||||||
5. **Response**: Demo Session Service returns credentials and session info
|
5. **Status Tracking**: Per-service progress stored in JSONB field with timestamps and error details
|
||||||
|
6. **Response**: Demo Session Service returns credentials and session info
|
||||||
|
7. **Frontend Polling**: Frontend polls `/api/v1/demo/sessions/{session_id}/status` until status is READY or FAILED
|
||||||
|
|
||||||
### Example: Orders Service Clone Endpoint
|
### Example: Orders Service Clone Endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -648,6 +648,133 @@ async def calculate_customer_rfm(customer_id: UUID) -> dict:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Alert Events
|
||||||
|
|
||||||
|
The Orders service also publishes procurement-related alerts through the alert processor.
|
||||||
|
|
||||||
|
**Exchange**: `events.exchange`
|
||||||
|
**Domain**: `procurement`
|
||||||
|
|
||||||
|
#### 1. POs Pending Approval Alert
|
||||||
|
**Event Type**: `procurement.pos_pending_approval`
|
||||||
|
**Severity**: urgent (>€10,000), high (>€5,000 or critical POs), medium (otherwise)
|
||||||
|
**Trigger**: New purchase orders created and awaiting approval
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "procurement.pos_pending_approval",
|
||||||
|
"severity": "high",
|
||||||
|
"metadata": {
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"pos_count": 3,
|
||||||
|
"total_amount": 6500.00,
|
||||||
|
"critical_count": 1,
|
||||||
|
"pos": [
|
||||||
|
{
|
||||||
|
"po_id": "uuid",
|
||||||
|
"po_number": "PO-001",
|
||||||
|
"supplier_id": "uuid",
|
||||||
|
"total_amount": 3000.00,
|
||||||
|
"auto_approved": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action_required": true,
|
||||||
|
"action_url": "/app/comprar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Approval Reminder Alert
|
||||||
|
**Event Type**: `procurement.approval_reminder`
|
||||||
|
**Severity**: high (>36 hours pending), medium (otherwise)
|
||||||
|
**Trigger**: PO not approved within threshold time
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "procurement.approval_reminder",
|
||||||
|
"severity": "high",
|
||||||
|
"metadata": {
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"po_id": "uuid",
|
||||||
|
"po_number": "PO-001",
|
||||||
|
"supplier_name": "Supplier ABC",
|
||||||
|
"total_amount": 3000.00,
|
||||||
|
"hours_pending": 40,
|
||||||
|
"created_at": "2025-12-18T10:00:00Z",
|
||||||
|
"action_required": true,
|
||||||
|
"action_url": "/app/comprar?po=uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Critical PO Escalation Alert
|
||||||
|
**Event Type**: `procurement.critical_po_escalation`
|
||||||
|
**Severity**: urgent
|
||||||
|
**Trigger**: Critical/urgent PO not approved in time
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "procurement.critical_po_escalation",
|
||||||
|
"severity": "urgent",
|
||||||
|
"metadata": {
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"po_id": "uuid",
|
||||||
|
"po_number": "PO-001",
|
||||||
|
"supplier_name": "Supplier ABC",
|
||||||
|
"total_amount": 5000.00,
|
||||||
|
"priority": "urgent",
|
||||||
|
"required_delivery_date": "2025-12-22",
|
||||||
|
"hours_pending": 48,
|
||||||
|
"escalated": true,
|
||||||
|
"action_required": true,
|
||||||
|
"action_url": "/app/comprar?po=uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Auto-Approval Summary (Notification)
|
||||||
|
**Event Type**: `procurement.auto_approval_summary`
|
||||||
|
**Type**: Notification (not alert)
|
||||||
|
**Trigger**: Daily summary of auto-approved POs
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "procurement.auto_approval_summary",
|
||||||
|
"metadata": {
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"auto_approved_count": 5,
|
||||||
|
"total_auto_approved_amount": 8500.00,
|
||||||
|
"manual_approval_count": 2,
|
||||||
|
"summary_date": "2025-12-19",
|
||||||
|
"auto_approved_pos": [...],
|
||||||
|
"pending_approval_pos": [...],
|
||||||
|
"action_url": "/app/comprar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. PO Approved Confirmation (Notification)
|
||||||
|
**Event Type**: `procurement.po_approved_confirmation`
|
||||||
|
**Type**: Notification (not alert)
|
||||||
|
**Trigger**: Purchase order approved
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "procurement.po_approved_confirmation",
|
||||||
|
"metadata": {
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"po_id": "uuid",
|
||||||
|
"po_number": "PO-001",
|
||||||
|
"supplier_name": "Supplier ABC",
|
||||||
|
"total_amount": 3000.00,
|
||||||
|
"approved_by": "user@example.com",
|
||||||
|
"auto_approved": false,
|
||||||
|
"approved_at": "2025-12-19T14:30:00Z",
|
||||||
|
"action_url": "/app/comprar?po=uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Consumed Events
|
### Consumed Events
|
||||||
- **From Production**: Batch completion updates order fulfillment status
|
- **From Production**: Batch completion updates order fulfillment status
|
||||||
- **From Inventory**: Stock availability affects order confirmation
|
- **From Inventory**: Stock availability affects order confirmation
|
||||||
|
|||||||
@@ -224,8 +224,10 @@ CREATE TABLE production_capacity (
|
|||||||
|
|
||||||
### Published Events (RabbitMQ)
|
### Published Events (RabbitMQ)
|
||||||
|
|
||||||
**Exchange**: `production`
|
**Exchange**: `events.exchange`
|
||||||
**Routing Keys**: `production.batch.completed`, `production.quality.issue`, `production.equipment.maintenance`
|
**Domain**: `production`
|
||||||
|
|
||||||
|
### Business Events
|
||||||
|
|
||||||
**Batch Completed Event**
|
**Batch Completed Event**
|
||||||
```json
|
```json
|
||||||
@@ -247,22 +249,177 @@ CREATE TABLE production_capacity (
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Quality Issue Alert**
|
### Alert Events
|
||||||
|
|
||||||
|
All alerts are published to the alert processor for enrichment and notification.
|
||||||
|
|
||||||
|
#### 1. Production Delay Alert
|
||||||
|
**Event Type**: `production.production_delay`
|
||||||
|
**Severity**: urgent (>120 min), high (>60 min), medium (otherwise)
|
||||||
|
**Trigger**: Batch delayed past scheduled start time
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event_type": "quality_issue",
|
"event_type": "production.production_delay",
|
||||||
"tenant_id": "uuid",
|
"severity": "urgent",
|
||||||
"batch_id": "uuid",
|
"metadata": {
|
||||||
"product_name": "Croissant",
|
"batch_id": "uuid",
|
||||||
"quality_score": 65,
|
"product_name": "Baguette",
|
||||||
"passing_score": 80,
|
"batch_number": "BATCH-001",
|
||||||
"issues_found": "Color too dark, texture inconsistent",
|
"delay_minutes": 135,
|
||||||
"severity": "high",
|
"affected_orders": 5,
|
||||||
"corrective_actions": "Adjust oven temperature, check proofing time",
|
"customer_names": ["Café A", "Café B"]
|
||||||
"timestamp": "2025-11-06T10:30:00Z"
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 2. Equipment Failure Alert
|
||||||
|
**Event Type**: `production.equipment_failure`
|
||||||
|
**Severity**: urgent
|
||||||
|
**Trigger**: Equipment malfunction detected
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.equipment_failure",
|
||||||
|
"severity": "urgent",
|
||||||
|
"metadata": {
|
||||||
|
"equipment_id": "uuid",
|
||||||
|
"equipment_name": "Oven #1",
|
||||||
|
"equipment_type": "oven",
|
||||||
|
"affected_batches": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Capacity Overload Alert
|
||||||
|
**Event Type**: `production.capacity_overload`
|
||||||
|
**Severity**: urgent (>120%), high (>100%), medium (otherwise)
|
||||||
|
**Trigger**: Production capacity exceeded
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.capacity_overload",
|
||||||
|
"severity": "urgent",
|
||||||
|
"metadata": {
|
||||||
|
"current_load_percent": 125,
|
||||||
|
"planned_batches": 15,
|
||||||
|
"available_capacity": 12,
|
||||||
|
"affected_date": "2025-12-20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Quality Issue Alert
|
||||||
|
**Event Type**: `production.quality_issue`
|
||||||
|
**Severity**: high
|
||||||
|
**Trigger**: Batch quality below threshold
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.quality_issue",
|
||||||
|
"severity": "high",
|
||||||
|
"metadata": {
|
||||||
|
"batch_id": "uuid",
|
||||||
|
"product_name": "Croissant",
|
||||||
|
"issue_type": "quality_below_threshold",
|
||||||
|
"issue_description": "Color too dark, texture inconsistent",
|
||||||
|
"affected_quantity": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Production Start Alert
|
||||||
|
**Event Type**: `production.start_production`
|
||||||
|
**Severity**: medium
|
||||||
|
**Trigger**: Production batch created
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.start_production",
|
||||||
|
"severity": "medium",
|
||||||
|
"metadata": {
|
||||||
|
"batch_id": "uuid",
|
||||||
|
"product_name": "Baguette",
|
||||||
|
"batch_number": "BATCH-001",
|
||||||
|
"planned_start_time": "2025-12-20T06:00:00Z",
|
||||||
|
"reasoning_data": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Batch Start Delayed Alert
|
||||||
|
**Event Type**: `production.batch_start_delayed`
|
||||||
|
**Severity**: high
|
||||||
|
**Trigger**: Batch start delayed for specific reason
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.batch_start_delayed",
|
||||||
|
"severity": "high",
|
||||||
|
"metadata": {
|
||||||
|
"batch_id": "uuid",
|
||||||
|
"product_name": "Pain au Chocolat",
|
||||||
|
"batch_number": "BATCH-002",
|
||||||
|
"scheduled_start": "2025-12-20T07:00:00Z",
|
||||||
|
"delay_reason": "Missing ingredients"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Missing Ingredients Alert
|
||||||
|
**Event Type**: `production.missing_ingredients`
|
||||||
|
**Severity**: urgent
|
||||||
|
**Trigger**: Required ingredients unavailable
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.missing_ingredients",
|
||||||
|
"severity": "urgent",
|
||||||
|
"metadata": {
|
||||||
|
"batch_id": "uuid",
|
||||||
|
"product_name": "Baguette",
|
||||||
|
"batch_number": "BATCH-001",
|
||||||
|
"missing_ingredients": [
|
||||||
|
{"name": "Flour", "required": 50, "available": 10},
|
||||||
|
{"name": "Yeast", "required": 2, "available": 0}
|
||||||
|
],
|
||||||
|
"missing_count": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. Equipment Maintenance Due Alert
|
||||||
|
**Event Type**: `production.equipment_maintenance_due`
|
||||||
|
**Severity**: high (>30 days overdue), medium (otherwise)
|
||||||
|
**Trigger**: Equipment maintenance overdue
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "production.equipment_maintenance_due",
|
||||||
|
"severity": "high",
|
||||||
|
"metadata": {
|
||||||
|
"equipment_id": "uuid",
|
||||||
|
"equipment_name": "Mixer #2",
|
||||||
|
"last_maintenance_date": "2024-10-15",
|
||||||
|
"days_overdue": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Recommendations
|
||||||
|
|
||||||
|
#### Efficiency Recommendation
|
||||||
|
**Event Type**: `production.efficiency_recommendation`
|
||||||
|
**Severity**: medium
|
||||||
|
|
||||||
|
#### Energy Optimization
|
||||||
|
**Event Type**: `production.energy_optimization`
|
||||||
|
**Severity**: medium
|
||||||
|
|
||||||
|
#### Batch Sequence Optimization
|
||||||
|
**Event Type**: `production.batch_sequence_optimization`
|
||||||
|
**Severity**: medium
|
||||||
|
|
||||||
### Consumed Events
|
### Consumed Events
|
||||||
- **From Forecasting**: Daily forecasts for production planning
|
- **From Forecasting**: Daily forecasts for production planning
|
||||||
- **From Orchestrator**: Scheduled production triggers
|
- **From Orchestrator**: Scheduled production triggers
|
||||||
|
|||||||
Reference in New Issue
Block a user