Update readmes and imporve UI

This commit is contained in:
Urtzi Alfaro
2025-12-19 09:28:36 +01:00
parent a6ae730ef0
commit 71ee2976a2
10 changed files with 1035 additions and 155 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}, },

View File

@@ -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>
</> </>
); );

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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