2025-10-23 07:44:54 +02:00
import React , { useEffect , useState } from 'react' ;
2025-09-22 11:04:03 +02:00
import { useNavigate } from 'react-router-dom' ;
import { useTranslation } from 'react-i18next' ;
2025-08-28 10:41:04 +02:00
import { PageHeader } from '../../components/layout' ;
2025-09-19 16:17:04 +02:00
import StatsGrid from '../../components/ui/Stats/StatsGrid' ;
import RealTimeAlerts from '../../components/domain/dashboard/RealTimeAlerts' ;
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
import { IncompleteIngredientsAlert } from '../../components/domain/dashboard/IncompleteIngredientsAlert' ;
Implement Phase 1: Post-onboarding configuration system
This commit implements the first phase of the post-onboarding configuration
system based on JTBD analysis:
**1. Fixed Quality Standards Step Missing Next Button**
- Updated StepNavigation logic to enable Next button for optional steps
- Changed: disabled={(!canContinue && !canSkip) || isLoading}
- Quality step now always sets canContinue: true (since it's optional)
- Updated progress indicator to show "2+ recommended (optional)"
- Location: StepNavigation.tsx, QualitySetupStep.tsx
**2. Implemented Configuration Progress Widget**
A comprehensive dashboard widget that guides post-onboarding configuration:
Features:
- Real-time progress tracking (% complete calculation)
- Section-by-section status (Inventory, Suppliers, Recipes, Quality)
- Visual indicators: checkmarks for complete, circles for incomplete
- Minimum requirements vs recommended amounts
- Next action prompts ("Add at least 3 ingredients")
- Feature unlock notifications ("Purchase Orders unlocked!")
- Clickable sections that navigate to configuration pages
- Auto-hides when 100% configured
Location: ConfigurationProgressWidget.tsx (340 lines)
Integration: DashboardPage.tsx
**Configuration Logic:**
- Inventory: 3 minimum, 10 recommended
- Suppliers: 1 minimum, 3 recommended
- Recipes: 1 minimum, 3 recommended
- Quality: 0 minimum (optional), 2 recommended
**UX Improvements:**
- Clear orientation ("Complete Your Bakery Setup")
- Progress bar with percentage
- Next step call-to-action
- Visual hierarchy (gradient borders, icons, colors)
- Responsive design
- Loading states
**Technical Implementation:**
- React hooks: useMemo for calculations
- Real-time data fetching from inventory, suppliers, recipes, quality APIs
- Automatic progress recalculation on data changes
- Navigation integration with react-router
- i18n support for all text
**Files Created:**
- ConfigurationProgressWidget.tsx
**Files Modified:**
- StepNavigation.tsx - Fixed optional step button logic
- QualitySetupStep.tsx - Always allow continuing (optional step)
- DashboardPage.tsx - Added configuration widget
**Pending (Next Phases):**
- Phase 2: Recipe & Supplier Wizard Modals (multi-step forms)
- Phase 3: Recipe templates, bulk operations, configuration recovery
Build: ✅ Success (21.17s)
All TypeScript validations passed.
2025-11-06 17:49:06 +00:00
import { ConfigurationProgressWidget } from '../../components/domain/dashboard/ConfigurationProgressWidget' ;
2025-10-21 19:50:07 +02:00
import PendingPOApprovals from '../../components/domain/dashboard/PendingPOApprovals' ;
import TodayProduction from '../../components/domain/dashboard/TodayProduction' ;
2025-10-27 16:33:26 +01:00
// Sustainability widget removed - now using stats in StatsGrid
2025-10-23 07:44:54 +02:00
import { EditViewModal } from '../../components/ui' ;
2025-09-22 11:04:03 +02:00
import { useTenant } from '../../stores/tenant.store' ;
2025-10-12 18:47:33 +02:00
import { useDemoTour , shouldStartTour , clearTourStartPending } from '../../features/demo-onboarding' ;
2025-10-21 19:50:07 +02:00
import { useDashboardStats } from '../../api/hooks/dashboard' ;
2025-10-23 07:44:54 +02:00
import { usePurchaseOrder , useApprovePurchaseOrder , useRejectPurchaseOrder } from '../../api/hooks/purchase-orders' ;
import { useBatchDetails , useUpdateBatchStatus } from '../../api/hooks/production' ;
2025-10-30 21:08:07 +01:00
import { useRunDailyWorkflow } from '../../api' ;
2025-10-23 07:44:54 +02:00
import { ProductionStatusEnum } from '../../api' ;
2025-09-19 16:17:04 +02:00
import {
AlertTriangle ,
2025-10-30 21:08:07 +01:00
Clock ,
2025-09-22 16:10:08 +02:00
Euro ,
2025-10-23 07:44:54 +02:00
Package ,
FileText ,
Building2 ,
Calendar ,
CheckCircle ,
X ,
ShoppingCart ,
Factory ,
2025-10-27 16:33:26 +01:00
Timer ,
TrendingDown ,
2025-10-30 21:08:07 +01:00
Leaf ,
Play
2025-09-19 16:17:04 +02:00
} from 'lucide-react' ;
2025-10-30 21:08:07 +01:00
import { showToast } from '../../utils/toast' ;
2025-08-28 10:41:04 +02:00
const DashboardPage : React.FC = ( ) = > {
2025-09-22 11:04:03 +02:00
const { t } = useTranslation ( ) ;
const navigate = useNavigate ( ) ;
2025-10-21 19:50:07 +02:00
const { availableTenants , currentTenant } = useTenant ( ) ;
2025-10-12 18:47:33 +02:00
const { startTour } = useDemoTour ( ) ;
const isDemoMode = localStorage . getItem ( 'demo_mode' ) === 'true' ;
2025-10-23 07:44:54 +02:00
// Modal state management
const [ selectedPOId , setSelectedPOId ] = useState < string | null > ( null ) ;
const [ selectedBatchId , setSelectedBatchId ] = useState < string | null > ( null ) ;
const [ showPOModal , setShowPOModal ] = useState ( false ) ;
const [ showBatchModal , setShowBatchModal ] = useState ( false ) ;
const [ approvalNotes , setApprovalNotes ] = useState ( '' ) ;
2025-10-21 19:50:07 +02:00
// Fetch real dashboard statistics
const { data : dashboardStats , isLoading : isLoadingStats , error : statsError } = useDashboardStats (
currentTenant ? . id || '' ,
{
enabled : ! ! currentTenant ? . id ,
}
) ;
2025-10-23 07:44:54 +02:00
// Fetch PO details when modal is open
const { data : poDetails , isLoading : isLoadingPO } = usePurchaseOrder (
currentTenant ? . id || '' ,
selectedPOId || '' ,
{
enabled : ! ! currentTenant ? . id && ! ! selectedPOId && showPOModal
}
) ;
// Fetch Production batch details when modal is open
const { data : batchDetails , isLoading : isLoadingBatch } = useBatchDetails (
currentTenant ? . id || '' ,
selectedBatchId || '' ,
{
enabled : ! ! currentTenant ? . id && ! ! selectedBatchId && showBatchModal
}
) ;
// Mutations
const approvePOMutation = useApprovePurchaseOrder ( ) ;
const rejectPOMutation = useRejectPurchaseOrder ( ) ;
const updateBatchStatusMutation = useUpdateBatchStatus ( ) ;
2025-10-30 21:08:07 +01:00
const orchestratorMutation = useRunDailyWorkflow ( ) ;
const handleRunOrchestrator = async ( ) = > {
try {
await orchestratorMutation . mutateAsync ( currentTenant ? . id || '' ) ;
showToast . success ( 'Flujo de planificación ejecutado exitosamente' ) ;
} catch ( error ) {
console . error ( 'Error running orchestrator:' , error ) ;
showToast . error ( 'Error al ejecutar flujo de planificación' ) ;
}
} ;
2025-10-23 07:44:54 +02:00
2025-10-12 18:47:33 +02:00
useEffect ( ( ) = > {
console . log ( '[Dashboard] Demo mode:' , isDemoMode ) ;
console . log ( '[Dashboard] Should start tour:' , shouldStartTour ( ) ) ;
console . log ( '[Dashboard] SessionStorage demo_tour_should_start:' , sessionStorage . getItem ( 'demo_tour_should_start' ) ) ;
2025-10-30 21:08:07 +01:00
console . log ( '[Dashboard] SessionStorage demo_tour_start_step:' , sessionStorage . getItem ( 'demo_tour_start_step' ) ) ;
// Check if there's a tour intent from redirection (higher priority)
const shouldStartFromRedirect = sessionStorage . getItem ( 'demo_tour_should_start' ) === 'true' ;
const redirectStartStep = parseInt ( sessionStorage . getItem ( 'demo_tour_start_step' ) || '0' , 10 ) ;
2025-10-12 18:47:33 +02:00
2025-10-30 21:08:07 +01:00
if ( isDemoMode && ( shouldStartTour ( ) || shouldStartFromRedirect ) ) {
2025-10-12 18:47:33 +02:00
console . log ( '[Dashboard] Starting tour in 1.5s...' ) ;
const timer = setTimeout ( ( ) = > {
console . log ( '[Dashboard] Executing startTour()' ) ;
2025-10-30 21:08:07 +01:00
if ( shouldStartFromRedirect ) {
// Start tour from the specific step that was intended
startTour ( redirectStartStep ) ;
// Clear the redirect intent
sessionStorage . removeItem ( 'demo_tour_should_start' ) ;
sessionStorage . removeItem ( 'demo_tour_start_step' ) ;
} else {
// Start tour normally (from beginning or resume)
startTour ( ) ;
clearTourStartPending ( ) ;
}
2025-10-12 18:47:33 +02:00
} , 1500 ) ;
return ( ) = > clearTimeout ( timer ) ;
}
} , [ isDemoMode , startTour ] ) ;
2025-09-22 11:04:03 +02:00
2025-10-21 19:50:07 +02:00
const handleViewAllProcurement = ( ) = > {
navigate ( '/app/operations/procurement' ) ;
2025-09-22 11:04:03 +02:00
} ;
2025-10-21 19:50:07 +02:00
const handleViewAllProduction = ( ) = > {
navigate ( '/app/operations/production' ) ;
} ;
2025-08-28 10:41:04 +02:00
2025-09-19 16:17:04 +02:00
const handleOrderItem = ( itemId : string ) = > {
console . log ( 'Ordering item:' , itemId ) ;
2025-10-21 19:50:07 +02:00
navigate ( '/app/operations/procurement' ) ;
2025-08-28 10:41:04 +02:00
} ;
2025-10-23 07:44:54 +02:00
const handleStartBatch = async ( batchId : string ) = > {
try {
await updateBatchStatusMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
batchId ,
statusUpdate : { status : ProductionStatusEnum.IN_PROGRESS }
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Lote iniciado' ) ;
2025-10-23 07:44:54 +02:00
} catch ( error ) {
console . error ( 'Error starting batch:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al iniciar lote' ) ;
2025-10-23 07:44:54 +02:00
}
2025-08-28 10:41:04 +02:00
} ;
2025-10-23 07:44:54 +02:00
const handlePauseBatch = async ( batchId : string ) = > {
try {
await updateBatchStatusMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
batchId ,
statusUpdate : { status : ProductionStatusEnum.ON_HOLD }
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Lote pausado' ) ;
2025-10-23 07:44:54 +02:00
} catch ( error ) {
console . error ( 'Error pausing batch:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al pausar lote' ) ;
2025-10-23 07:44:54 +02:00
}
2025-09-19 16:17:04 +02:00
} ;
2025-08-28 10:41:04 +02:00
2025-10-23 07:44:54 +02:00
const handleViewDetails = ( batchId : string ) = > {
setSelectedBatchId ( batchId ) ;
setShowBatchModal ( true ) ;
2025-09-19 16:17:04 +02:00
} ;
2025-08-28 10:41:04 +02:00
2025-10-23 07:44:54 +02:00
const handleApprovePO = async ( poId : string ) = > {
try {
await approvePOMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
poId ,
notes : 'Aprobado desde el dashboard'
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Orden aprobada' ) ;
2025-10-23 07:44:54 +02:00
} catch ( error ) {
console . error ( 'Error approving PO:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al aprobar orden' ) ;
2025-10-23 07:44:54 +02:00
}
2025-10-21 19:50:07 +02:00
} ;
2025-10-23 07:44:54 +02:00
const handleRejectPO = async ( poId : string ) = > {
try {
await rejectPOMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
poId ,
reason : 'Rechazado desde el dashboard'
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Orden rechazada' ) ;
2025-10-23 07:44:54 +02:00
} catch ( error ) {
console . error ( 'Error rejecting PO:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al rechazar orden' ) ;
2025-10-23 07:44:54 +02:00
}
2025-10-21 19:50:07 +02:00
} ;
const handleViewPODetails = ( poId : string ) = > {
2025-10-23 07:44:54 +02:00
setSelectedPOId ( poId ) ;
setShowPOModal ( true ) ;
2025-10-21 19:50:07 +02:00
} ;
const handleViewAllPOs = ( ) = > {
navigate ( '/app/operations/procurement' ) ;
2025-09-19 16:17:04 +02:00
} ;
2025-08-28 10:41:04 +02:00
2025-10-29 06:58:05 +01:00
// Build stats from real API data (Sales analytics removed - Professional/Enterprise tier only)
2025-10-21 19:50:07 +02:00
const criticalStats = React . useMemo ( ( ) = > {
if ( ! dashboardStats ) {
// Return loading/empty state
return [ ] ;
}
// Determine trend direction
const getTrendDirection = ( value : number ) : 'up' | 'down' | 'neutral' = > {
if ( value > 0 ) return 'up' ;
if ( value < 0 ) return 'down' ;
return 'neutral' ;
} ;
return [
{
title : t ( 'dashboard:stats.pending_orders' , 'Pending Orders' ) ,
value : dashboardStats.pendingOrders.toString ( ) ,
icon : Clock ,
variant : dashboardStats.pendingOrders > 10 ? ( 'warning' as const ) : ( 'info' as const ) ,
trend : dashboardStats.ordersTrend !== 0 ? {
value : Math.abs ( dashboardStats . ordersTrend ) ,
direction : getTrendDirection ( dashboardStats . ordersTrend ) ,
label : t ( 'dashboard:trends.vs_yesterday' , '% vs yesterday' )
} : undefined ,
subtitle : dashboardStats.pendingOrders > 0
? t ( 'dashboard:messages.require_attention' , 'Require attention' )
: t ( 'dashboard:messages.all_caught_up' , 'All caught up!' )
} ,
{
title : t ( 'dashboard:stats.stock_alerts' , 'Critical Stock' ) ,
value : dashboardStats.criticalStock.toString ( ) ,
icon : AlertTriangle ,
variant : dashboardStats.criticalStock > 0 ? ( 'error' as const ) : ( 'success' as const ) ,
trend : undefined , // Stock alerts don't have historical trends
subtitle : dashboardStats.criticalStock > 0
? t ( 'dashboard:messages.action_required' , 'Action required' )
: t ( 'dashboard:messages.stock_healthy' , 'Stock levels healthy' )
2025-10-27 16:33:26 +01:00
} ,
{
title : t ( 'dashboard:stats.waste_reduction' , 'Waste Reduction' ) ,
value : dashboardStats.wasteReductionPercentage
? ` ${ Math . abs ( dashboardStats . wasteReductionPercentage ) . toFixed ( 1 ) } % `
: '0%' ,
icon : TrendingDown ,
variant : ( dashboardStats . wasteReductionPercentage || 0 ) >= 15 ? ( 'success' as const ) : ( 'info' as const ) ,
trend : undefined ,
subtitle : ( dashboardStats . wasteReductionPercentage || 0 ) >= 15
? t ( 'dashboard:messages.excellent_progress' , 'Excellent progress!' )
: t ( 'dashboard:messages.keep_improving' , 'Keep improving' )
} ,
{
title : t ( 'dashboard:stats.monthly_savings' , 'Monthly Savings' ) ,
value : dashboardStats.monthlySavingsEur
? ` € ${ dashboardStats . monthlySavingsEur . toFixed ( 0 ) } `
: '€0' ,
icon : Leaf ,
variant : 'success' as const ,
trend : undefined ,
subtitle : t ( 'dashboard:messages.from_sustainability' , 'From sustainability' )
2025-10-21 19:50:07 +02:00
}
] ;
} , [ dashboardStats , t ] ) ;
2025-10-23 07:44:54 +02:00
// Helper function to build PO detail sections (reused from ProcurementPage)
const buildPODetailsSections = ( po : any ) = > {
if ( ! po ) return [ ] ;
const getPOStatusConfig = ( status : string ) = > {
const normalizedStatus = status ? . toUpperCase ( ) . replace ( /_/g , '_' ) ;
const configs : Record < string , any > = {
PENDING_APPROVAL : { text : 'Pendiente de Aprobación' , color : 'var(--color-warning)' } ,
APPROVED : { text : 'Aprobado' , color : 'var(--color-success)' } ,
SENT_TO_SUPPLIER : { text : 'Enviado al Proveedor' , color : 'var(--color-info)' } ,
CONFIRMED : { text : 'Confirmado' , color : 'var(--color-success)' } ,
RECEIVED : { text : 'Recibido' , color : 'var(--color-success)' } ,
COMPLETED : { text : 'Completado' , color : 'var(--color-success)' } ,
CANCELLED : { text : 'Cancelado' , color : 'var(--color-error)' } ,
} ;
return configs [ normalizedStatus ] || { text : status , color : 'var(--color-info)' } ;
} ;
const statusConfig = getPOStatusConfig ( po . status ) ;
return [
{
title : 'Información General' ,
icon : FileText ,
fields : [
{ label : 'Número de Orden' , value : po.po_number , type : 'text' as const } ,
{ label : 'Estado' , value : statusConfig.text , type : 'status' as const } ,
{ label : 'Prioridad' , value : po.priority === 'urgent' ? 'Urgente' : po . priority === 'high' ? 'Alta' : po . priority === 'low' ? 'Baja' : 'Normal' , type : 'text' as const } ,
{ label : 'Fecha de Creación' , value : new Date ( po . created_at ) . toLocaleDateString ( 'es-ES' , { year : 'numeric' , month : 'long' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) , type : 'text' as const }
]
} ,
{
title : 'Información del Proveedor' ,
icon : Building2 ,
fields : [
{ label : 'Proveedor' , value : po.supplier?.name || po . supplier_name || 'N/A' , type : 'text' as const } ,
{ label : 'Email' , value : po.supplier?.contact_email || 'N/A' , type : 'text' as const } ,
{ label : 'Teléfono' , value : po.supplier?.contact_phone || 'N/A' , type : 'text' as const }
]
} ,
{
title : 'Resumen Financiero' ,
icon : Euro ,
fields : [
{ label : 'Subtotal' , value : ` € ${ ( typeof po . subtotal === 'string' ? parseFloat ( po . subtotal ) : po . subtotal || 0 ) . toFixed ( 2 ) } ` , type : 'text' as const } ,
{ label : 'Impuestos' , value : ` € ${ ( typeof po . tax_amount === 'string' ? parseFloat ( po . tax_amount ) : po . tax_amount || 0 ) . toFixed ( 2 ) } ` , type : 'text' as const } ,
{ label : 'TOTAL' , value : ` € ${ ( typeof po . total_amount === 'string' ? parseFloat ( po . total_amount ) : po . total_amount || 0 ) . toFixed ( 2 ) } ` , type : 'text' as const , highlight : true }
]
} ,
{
title : 'Entrega' ,
icon : Calendar ,
fields : [
{ label : 'Fecha Requerida' , value : po.required_delivery_date ? new Date ( po . required_delivery_date ) . toLocaleDateString ( 'es-ES' , { year : 'numeric' , month : 'long' , day : 'numeric' } ) : 'No especificada' , type : 'text' as const } ,
{ label : 'Fecha Esperada' , value : po.expected_delivery_date ? new Date ( po . expected_delivery_date ) . toLocaleDateString ( 'es-ES' , { year : 'numeric' , month : 'long' , day : 'numeric' } ) : 'No especificada' , type : 'text' as const }
]
}
] ;
} ;
// Helper function to build Production batch detail sections
const buildBatchDetailsSections = ( batch : any ) = > {
if ( ! batch ) return [ ] ;
return [
{
title : 'Información General' ,
icon : Package ,
fields : [
{ label : 'Producto' , value : batch.product_name , type : 'text' as const , highlight : true } ,
{ label : 'Número de Lote' , value : batch.batch_number , type : 'text' as const } ,
{ label : 'Cantidad Planificada' , value : ` ${ batch . planned_quantity } unidades ` , type : 'text' as const } ,
{ label : 'Cantidad Real' , value : batch.actual_quantity ? ` ${ batch . actual_quantity } unidades ` : 'Pendiente' , type : 'text' as const } ,
{ label : 'Estado' , value : batch.status , type : 'text' as const } ,
{ label : 'Prioridad' , value : batch.priority , type : 'text' as const }
]
} ,
{
title : 'Cronograma' ,
icon : Clock ,
fields : [
{ label : 'Inicio Planificado' , value : batch.planned_start_time ? new Date ( batch . planned_start_time ) . toLocaleString ( 'es-ES' ) : 'No especificado' , type : 'text' as const } ,
{ label : 'Fin Planificado' , value : batch.planned_end_time ? new Date ( batch . planned_end_time ) . toLocaleString ( 'es-ES' ) : 'No especificado' , type : 'text' as const } ,
{ label : 'Inicio Real' , value : batch.actual_start_time ? new Date ( batch . actual_start_time ) . toLocaleString ( 'es-ES' ) : 'Pendiente' , type : 'text' as const } ,
{ label : 'Fin Real' , value : batch.actual_end_time ? new Date ( batch . actual_end_time ) . toLocaleString ( 'es-ES' ) : 'Pendiente' , type : 'text' as const }
]
} ,
{
title : 'Producción' ,
icon : Factory ,
fields : [
{ label : 'Personal Asignado' , value : batch.staff_assigned?.join ( ', ' ) || 'No asignado' , type : 'text' as const } ,
{ label : 'Estación' , value : batch.station_id || 'No asignada' , type : 'text' as const } ,
{ label : 'Duración Planificada' , value : batch.planned_duration_minutes ? ` ${ batch . planned_duration_minutes } minutos ` : 'No especificada' , type : 'text' as const }
]
} ,
{
title : 'Calidad y Costos' ,
icon : CheckCircle ,
fields : [
{ label : 'Puntuación de Calidad' , value : batch.quality_score ? ` ${ batch . quality_score } /10 ` : 'Pendiente' , type : 'text' as const } ,
{ label : 'Rendimiento' , value : batch.yield_percentage ? ` ${ batch . yield_percentage } % ` : 'Calculando...' , type : 'text' as const } ,
{ label : 'Costo Estimado' , value : batch.estimated_cost ? ` € ${ batch . estimated_cost } ` : '€0.00' , type : 'text' as const } ,
{ label : 'Costo Real' , value : batch.actual_cost ? ` € ${ batch . actual_cost } ` : '€0.00' , type : 'text' as const }
]
}
] ;
} ;
2025-10-21 19:50:07 +02:00
2025-08-28 10:41:04 +02:00
return (
2025-09-19 16:17:04 +02:00
< div className = "space-y-6 p-4 sm:p-6" >
2025-08-28 10:41:04 +02:00
< PageHeader
2025-09-22 11:04:03 +02:00
title = { t ( 'dashboard:title' , 'Dashboard' ) }
description = { t ( 'dashboard:subtitle' , 'Overview of your bakery operations' ) }
2025-10-30 21:08:07 +01:00
actions = { [
{
id : 'run-orchestrator' ,
label : orchestratorMutation.isPending ? 'Ejecutando...' : 'Ejecutar Planificación Diaria' ,
icon : Play ,
onClick : handleRunOrchestrator ,
variant : 'primary' , // Primary button for visibility
size : 'sm' ,
disabled : orchestratorMutation.isPending ,
loading : orchestratorMutation.isPending
}
] }
2025-08-28 10:41:04 +02:00
/ >
2025-09-19 16:17:04 +02:00
{ /* Critical Metrics using StatsGrid */ }
2025-10-12 18:47:33 +02:00
< div data-tour = "dashboard-stats" >
2025-10-21 19:50:07 +02:00
{ isLoadingStats ? (
2025-10-29 06:58:05 +01:00
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6" >
{ [ 1 , 2 , 3 , 4 ] . map ( ( i ) = > (
2025-10-21 19:50:07 +02:00
< div
key = { i }
className = "h-32 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg animate-pulse"
/ >
) ) }
< / div >
) : statsError ? (
< div className = "mb-6 p-4 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg" >
< p className = "text-[var(--color-error)] text-sm" >
{ t ( 'dashboard:errors.failed_to_load_stats' , 'Failed to load dashboard statistics. Please try again.' ) }
< / p >
< / div >
) : (
< StatsGrid
stats = { criticalStats }
2025-10-29 06:58:05 +01:00
columns = { 4 }
2025-10-21 19:50:07 +02:00
gap = "lg"
className = "mb-6"
/ >
) }
2025-10-12 18:47:33 +02:00
< / div >
2025-08-28 10:41:04 +02:00
2025-10-23 07:44:54 +02:00
{ /* Dashboard Content - Main Sections */ }
2025-09-19 16:17:04 +02:00
< div className = "space-y-6" >
Implement Phase 1: Post-onboarding configuration system
This commit implements the first phase of the post-onboarding configuration
system based on JTBD analysis:
**1. Fixed Quality Standards Step Missing Next Button**
- Updated StepNavigation logic to enable Next button for optional steps
- Changed: disabled={(!canContinue && !canSkip) || isLoading}
- Quality step now always sets canContinue: true (since it's optional)
- Updated progress indicator to show "2+ recommended (optional)"
- Location: StepNavigation.tsx, QualitySetupStep.tsx
**2. Implemented Configuration Progress Widget**
A comprehensive dashboard widget that guides post-onboarding configuration:
Features:
- Real-time progress tracking (% complete calculation)
- Section-by-section status (Inventory, Suppliers, Recipes, Quality)
- Visual indicators: checkmarks for complete, circles for incomplete
- Minimum requirements vs recommended amounts
- Next action prompts ("Add at least 3 ingredients")
- Feature unlock notifications ("Purchase Orders unlocked!")
- Clickable sections that navigate to configuration pages
- Auto-hides when 100% configured
Location: ConfigurationProgressWidget.tsx (340 lines)
Integration: DashboardPage.tsx
**Configuration Logic:**
- Inventory: 3 minimum, 10 recommended
- Suppliers: 1 minimum, 3 recommended
- Recipes: 1 minimum, 3 recommended
- Quality: 0 minimum (optional), 2 recommended
**UX Improvements:**
- Clear orientation ("Complete Your Bakery Setup")
- Progress bar with percentage
- Next step call-to-action
- Visual hierarchy (gradient borders, icons, colors)
- Responsive design
- Loading states
**Technical Implementation:**
- React hooks: useMemo for calculations
- Real-time data fetching from inventory, suppliers, recipes, quality APIs
- Automatic progress recalculation on data changes
- Navigation integration with react-router
- i18n support for all text
**Files Created:**
- ConfigurationProgressWidget.tsx
**Files Modified:**
- StepNavigation.tsx - Fixed optional step button logic
- QualitySetupStep.tsx - Always allow continuing (optional step)
- DashboardPage.tsx - Added configuration widget
**Pending (Next Phases):**
- Phase 2: Recipe & Supplier Wizard Modals (multi-step forms)
- Phase 3: Recipe templates, bulk operations, configuration recovery
Build: ✅ Success (21.17s)
All TypeScript validations passed.
2025-11-06 17:49:06 +00:00
{ /* 0. Configuration Progress Widget */ }
< ConfigurationProgressWidget / >
2025-10-21 19:50:07 +02:00
{ /* 1. Real-time Alerts */ }
2025-10-12 18:47:33 +02:00
< div data-tour = "real-time-alerts" >
< RealTimeAlerts / >
< / div >
2025-09-19 16:17:04 +02:00
Implement 5 UX enhancements for ingredient management
This commit implements the requested enhancements for the ingredient
quick-add system and batch management:
**1. Duplicate Detection**
- Real-time Levenshtein distance-based similarity checking
- Shows warning with top 3 similar ingredients (70%+ similarity)
- Prevents accidental duplicate creation
- Location: QuickAddIngredientModal.tsx
**2. Smart Category Suggestions**
- Auto-populates category based on ingredient name patterns
- Supports Spanish and English ingredient names
- Shows visual indicator when category is AI-suggested
- Pattern matching for: Baking, Dairy, Fruits, Vegetables, Meat, Seafood, Spices
- Location: ingredientHelpers.ts
**3. Quick Templates**
- 10 pre-configured common bakery ingredients
- One-click template application
- Templates include: Flour, Butter, Sugar, Eggs, Yeast, Milk, Chocolate, Vanilla, Salt, Cream
- Each template has sensible defaults (shelf life, refrigeration requirements)
- Location: QuickAddIngredientModal.tsx
**4. Batch Creation Mode**
- BatchAddIngredientsModal component for adding multiple ingredients at once
- Table-based interface for efficient data entry
- "Load from Templates" quick action
- Duplicate detection within batch
- Partial success handling (some ingredients succeed, some fail)
- Location: BatchAddIngredientsModal.tsx
- Integration: UploadSalesDataStep.tsx (2 buttons: "Add One" / "Add Multiple")
**5. Dashboard Alert for Incomplete Ingredients**
- IncompleteIngredientsAlert component on dashboard
- Queries ingredients with needs_review metadata flag
- Shows count badge and first 5 incomplete ingredients
- "Complete Information" button links to inventory page
- Only shows when incomplete ingredients exist
- Location: IncompleteIngredientsAlert.tsx
- Integration: DashboardPage.tsx
**New Files Created:**
- ingredientHelpers.ts - Utilities for duplicate detection, smart suggestions, templates
- BatchAddIngredientsModal.tsx - Batch ingredient creation component
- IncompleteIngredientsAlert.tsx - Dashboard alert component
**Files Modified:**
- QuickAddIngredientModal.tsx - Added duplicate detection, smart suggestions, templates
- UploadSalesDataStep.tsx - Integrated batch creation modal
- DashboardPage.tsx - Added incomplete ingredients alert
**Technical Highlights:**
- Levenshtein distance algorithm for fuzzy name matching
- Pattern-based category suggestions (supports 100+ ingredient patterns)
- Metadata tracking (needs_review, created_context)
- Real-time validation and error handling
- Responsive UI with animations
- Consistent with existing design system
All features built and tested successfully.
Build time: 21.29s
2025-11-06 15:39:30 +00:00
{ /* 1.5. Incomplete Ingredients Alert */ }
< IncompleteIngredientsAlert / >
2025-10-27 16:33:26 +01:00
{ /* 2. Pending PO Approvals - What purchase orders need approval? */ }
2025-10-21 19:50:07 +02:00
< div data-tour = "pending-po-approvals" >
< PendingPOApprovals
onApprovePO = { handleApprovePO }
onRejectPO = { handleRejectPO }
onViewDetails = { handleViewPODetails }
onViewAllPOs = { handleViewAllPOs }
maxPOs = { 5 }
2025-10-12 18:47:33 +02:00
/ >
< / div >
2025-09-19 16:17:04 +02:00
2025-10-27 16:33:26 +01:00
{ /* 3. Today's Production - What needs to be produced today? */ }
2025-10-21 19:50:07 +02:00
< div data-tour = "today-production" >
< TodayProduction
onStartBatch = { handleStartBatch }
onPauseBatch = { handlePauseBatch }
2025-10-12 18:47:33 +02:00
onViewDetails = { handleViewDetails }
2025-10-21 19:50:07 +02:00
onViewAllPlans = { handleViewAllProduction }
maxBatches = { 5 }
2025-10-12 18:47:33 +02:00
/ >
< / div >
2025-08-28 10:41:04 +02:00
< / div >
2025-10-23 07:44:54 +02:00
{ /* Purchase Order Details Modal */ }
{ showPOModal && poDetails && (
< EditViewModal
isOpen = { showPOModal }
onClose = { ( ) = > {
setShowPOModal ( false ) ;
setSelectedPOId ( null ) ;
} }
title = { ` Orden de Compra: ${ poDetails . po_number } ` }
subtitle = { ` Proveedor: ${ poDetails . supplier ? . name || poDetails . supplier_name || 'N/A' } ` }
mode = "view"
sections = { buildPODetailsSections ( poDetails ) }
loading = { isLoadingPO }
statusIndicator = { {
color : poDetails.status === 'PENDING_APPROVAL' ? 'var(--color-warning)' :
poDetails . status === 'APPROVED' ? 'var(--color-success)' :
'var(--color-info)' ,
text : poDetails.status === 'PENDING_APPROVAL' ? 'Pendiente de Aprobación' :
poDetails . status === 'APPROVED' ? 'Aprobado' :
poDetails . status || 'N/A' ,
icon : ShoppingCart
} }
actions = {
poDetails . status === 'PENDING_APPROVAL' ? [
{
label : 'Aprobar' ,
onClick : async ( ) = > {
try {
await approvePOMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
poId : poDetails.id ,
notes : 'Aprobado desde el dashboard'
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Orden aprobada' ) ;
2025-10-23 07:44:54 +02:00
setShowPOModal ( false ) ;
setSelectedPOId ( null ) ;
} catch ( error ) {
console . error ( 'Error approving PO:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al aprobar orden' ) ;
2025-10-23 07:44:54 +02:00
}
} ,
variant : 'primary' as const ,
icon : CheckCircle
} ,
{
label : 'Rechazar' ,
onClick : async ( ) = > {
try {
await rejectPOMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
poId : poDetails.id ,
reason : 'Rechazado desde el dashboard'
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Orden rechazada' ) ;
2025-10-23 07:44:54 +02:00
setShowPOModal ( false ) ;
setSelectedPOId ( null ) ;
} catch ( error ) {
console . error ( 'Error rejecting PO:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al rechazar orden' ) ;
2025-10-23 07:44:54 +02:00
}
} ,
variant : 'outline' as const ,
icon : X
}
] : undefined
}
/ >
) }
{ /* Production Batch Details Modal */ }
{ showBatchModal && batchDetails && (
< EditViewModal
isOpen = { showBatchModal }
onClose = { ( ) = > {
setShowBatchModal ( false ) ;
setSelectedBatchId ( null ) ;
} }
title = { batchDetails . product_name }
subtitle = { ` Lote # ${ batchDetails . batch_number } ` }
mode = "view"
sections = { buildBatchDetailsSections ( batchDetails ) }
loading = { isLoadingBatch }
statusIndicator = { {
color : batchDetails.status === 'PENDING' ? 'var(--color-warning)' :
batchDetails . status === 'IN_PROGRESS' ? 'var(--color-info)' :
batchDetails . status === 'COMPLETED' ? 'var(--color-success)' :
batchDetails . status === 'FAILED' ? 'var(--color-error)' :
'var(--color-info)' ,
text : batchDetails.status === 'PENDING' ? 'Pendiente' :
batchDetails . status === 'IN_PROGRESS' ? 'En Progreso' :
batchDetails . status === 'COMPLETED' ? 'Completado' :
batchDetails . status === 'FAILED' ? 'Fallido' :
batchDetails . status === 'ON_HOLD' ? 'Pausado' :
batchDetails . status || 'N/A' ,
icon : Factory
} }
actions = {
batchDetails . status === 'PENDING' ? [
{
label : 'Iniciar Lote' ,
onClick : async ( ) = > {
try {
await updateBatchStatusMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
batchId : batchDetails.id ,
statusUpdate : { status : ProductionStatusEnum.IN_PROGRESS }
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Lote iniciado' ) ;
2025-10-23 07:44:54 +02:00
setShowBatchModal ( false ) ;
setSelectedBatchId ( null ) ;
} catch ( error ) {
console . error ( 'Error starting batch:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al iniciar lote' ) ;
2025-10-23 07:44:54 +02:00
}
} ,
variant : 'primary' as const ,
icon : CheckCircle
}
] : batchDetails . status === 'IN_PROGRESS' ? [
{
label : 'Pausar Lote' ,
onClick : async ( ) = > {
try {
await updateBatchStatusMutation . mutateAsync ( {
tenantId : currentTenant?.id || '' ,
batchId : batchDetails.id ,
statusUpdate : { status : ProductionStatusEnum.ON_HOLD }
} ) ;
2025-10-30 21:08:07 +01:00
showToast . success ( 'Lote pausado' ) ;
2025-10-23 07:44:54 +02:00
setShowBatchModal ( false ) ;
setSelectedBatchId ( null ) ;
} catch ( error ) {
console . error ( 'Error pausing batch:' , error ) ;
2025-10-30 21:08:07 +01:00
showToast . error ( 'Error al pausar lote' ) ;
2025-10-23 07:44:54 +02:00
}
} ,
variant : 'outline' as const ,
icon : X
}
] : undefined
}
/ >
) }
2025-08-28 10:41:04 +02:00
< / div >
) ;
} ;
2025-10-30 21:08:07 +01:00
export default DashboardPage ;