Files
bakery-ia/frontend/src/pages/app/DashboardPage.tsx

612 lines
24 KiB
TypeScript
Raw Normal View History

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
import { EditViewModal } from '../../components/ui';
2025-09-22 11:04:03 +02:00
import { useTenant } from '../../stores/tenant.store';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
2025-10-21 19:50:07 +02:00
import { useDashboardStats } from '../../api/hooks/dashboard';
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';
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,
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();
const { startTour } = useDemoTour();
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
// 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,
}
);
// 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');
}
};
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-30 21:08:07 +01:00
if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) {
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();
}
}, 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
};
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');
} catch (error) {
console.error('Error starting batch:', error);
2025-10-30 21:08:07 +01:00
showToast.error('Error al iniciar lote');
}
2025-08-28 10:41:04 +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');
} catch (error) {
console.error('Error pausing batch:', error);
2025-10-30 21:08:07 +01:00
showToast.error('Error al pausar lote');
}
2025-09-19 16:17:04 +02:00
};
2025-08-28 10:41:04 +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
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');
} catch (error) {
console.error('Error approving PO:', error);
2025-10-30 21:08:07 +01:00
showToast.error('Error al aprobar orden');
}
2025-10-21 19:50:07 +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');
} catch (error) {
console.error('Error rejecting PO:', error);
2025-10-30 21:08:07 +01:00
showToast.error('Error al rechazar orden');
}
2025-10-21 19:50:07 +02:00
};
const handleViewPODetails = (poId: string) => {
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]);
// 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 */}
<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"
/>
)}
</div>
2025-08-28 10:41:04 +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 */}
<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}
/>
</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}
onViewDetails={handleViewDetails}
2025-10-21 19:50:07 +02:00
onViewAllPlans={handleViewAllProduction}
maxBatches={5}
/>
</div>
2025-08-28 10:41:04 +02:00
</div>
{/* 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');
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');
}
},
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');
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');
}
},
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');
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');
}
},
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');
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');
}
},
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;