New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -25,12 +25,13 @@ import {
useOrchestrationSummary,
useUnifiedActionQueue,
useProductionTimeline,
useInsights,
useApprovePurchaseOrder,
useStartProductionBatch,
usePauseProductionBatch,
useExecutionProgress,
} from '../../api/hooks/newDashboard';
useDashboardRealtime, // PHASE 3: SSE state sync
useProgressiveDashboard, // PHASE 4: Progressive loading
} from '../../api/hooks/useProfessionalDashboard';
import { useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
import { useIngredients } from '../../api/hooks/inventory';
import { useSuppliers } from '../../api/hooks/suppliers';
@@ -54,7 +55,14 @@ import {
useOrchestrationNotifications,
} from '../../hooks';
export function NewDashboardPage() {
// Import Enterprise Dashboard
import EnterpriseDashboardPage from './EnterpriseDashboardPage';
import { useSubscription } from '../../api/hooks/subscription';
import { SUBSCRIPTION_TIERS } from '../../api/types/subscription';
// Rename the existing component to BakeryDashboard
export function BakeryDashboard() {
const navigate = useNavigate();
const { t } = useTranslation(['dashboard', 'common', 'alerts']);
const { currentTenant } = useTenant();
@@ -75,48 +83,107 @@ export function NewDashboardPage() {
const [poModalMode, setPOModalMode] = useState<'view' | 'edit'>('view');
// Setup Progress Data
const { data: ingredients = [], isLoading: loadingIngredients } = useIngredients(tenantId, {}, { enabled: !!tenantId });
const { data: suppliers = [], isLoading: loadingSuppliers } = useSuppliers(tenantId, { enabled: !!tenantId });
const { data: recipes = [], isLoading: loadingRecipes } = useRecipes(tenantId, { enabled: !!tenantId });
const { data: qualityData, isLoading: loadingQuality } = useQualityTemplates(tenantId, { enabled: !!tenantId });
// Always fetch setup data to determine true progress, but use localStorage as fallback during loading
// PHASE 1 OPTIMIZATION: Only use cached value if we're still waiting for API to respond
const setupProgressFromStorage = useMemo(() => {
try {
const cached = localStorage.getItem(`setup_progress_${tenantId}`);
return cached ? parseInt(cached, 10) : 0;
} catch {
return 0;
}
}, [tenantId]);
// Always fetch the actual data to determine true progress
const { data: ingredients = [], isLoading: loadingIngredients } = useIngredients(
tenantId,
{},
{ enabled: !!tenantId }
);
const { data: suppliers = [], isLoading: loadingSuppliers } = useSuppliers(
tenantId,
{},
{ enabled: !!tenantId }
);
const { data: recipes = [], isLoading: loadingRecipes } = useRecipes(
tenantId,
{},
{ enabled: !!tenantId }
);
const { data: qualityData, isLoading: loadingQuality } = useQualityTemplates(
tenantId,
{},
{ enabled: !!tenantId }
);
const qualityTemplates = Array.isArray(qualityData?.templates) ? qualityData.templates : [];
// Data fetching
// PHASE 4: Progressive data loading for perceived performance boost
const {
data: healthStatus,
isLoading: healthLoading,
refetch: refetchHealth,
} = useBakeryHealthStatus(tenantId);
health: {
data: healthStatus,
isLoading: healthLoading,
refetch: refetchHealth,
},
actionQueue: {
data: actionQueue,
isLoading: actionQueueLoading,
refetch: refetchActionQueue,
},
progress: {
data: executionProgress,
isLoading: executionProgressLoading,
refetch: refetchExecutionProgress,
},
overallLoading,
isReady,
} = useProgressiveDashboard(tenantId);
// Additional hooks not part of progressive loading
const {
data: orchestrationSummary,
isLoading: orchestrationLoading,
refetch: refetchOrchestration,
} = useOrchestrationSummary(tenantId);
const {
data: actionQueue,
isLoading: actionQueueLoading,
refetch: refetchActionQueue,
} = useUnifiedActionQueue(tenantId);
const {
data: executionProgress,
isLoading: executionProgressLoading,
refetch: refetchExecutionProgress,
} = useExecutionProgress(tenantId);
const {
data: productionTimeline,
isLoading: timelineLoading,
refetch: refetchTimeline,
} = useProductionTimeline(tenantId);
const {
data: insights,
isLoading: insightsLoading,
refetch: refetchInsights,
} = useInsights(tenantId);
// Insights functionality removed as it's not needed with new architecture
const insights = undefined;
const insightsLoading = false;
const refetchInsights = () => {};
// PHASE 3: Enable SSE real-time state synchronization
useDashboardRealtime(tenantId);
// PHASE 6: Performance monitoring
useEffect(() => {
const loadTime = performance.now();
console.log(`📊 [Performance] Dashboard loaded in ${loadTime.toFixed(0)}ms`);
// Calculate setup completion status based on stored progress (approximation since actual data may not be loaded yet)
const setupComplete = setupProgressFromStorage >= 100;
if (loadTime > 1000) {
console.warn('⚠️ [Performance] Dashboard load time exceeded target (>1000ms):', {
loadTime: `${loadTime.toFixed(0)}ms`,
target: '1000ms',
setupComplete,
queriesSkipped: setupComplete ? 4 : 0,
});
} else {
console.log('✅ [Performance] Dashboard load time within target:', {
loadTime: `${loadTime.toFixed(0)}ms`,
target: '<1000ms',
setupComplete,
queriesSkipped: setupComplete ? 4 : 0,
});
}
}, [setupProgressFromStorage]); // Include setupProgressFromStorage as dependency
// Real-time event subscriptions for automatic refetching
const { notifications: batchNotifications } = useBatchNotifications();
@@ -212,7 +279,7 @@ export function NewDashboardPage() {
});
if (latestBatchNotificationId &&
latestBatchNotificationId !== prevBatchNotificationsRef.current) {
latestBatchNotificationId !== prevBatchNotificationsRef.current) {
console.log('🔥 [Dashboard] NEW batch notification detected, updating ref and refetching');
prevBatchNotificationsRef.current = latestBatchNotificationId;
const latest = batchNotifications[0];
@@ -237,7 +304,7 @@ export function NewDashboardPage() {
});
if (latestDeliveryNotificationId &&
latestDeliveryNotificationId !== prevDeliveryNotificationsRef.current) {
latestDeliveryNotificationId !== prevDeliveryNotificationsRef.current) {
console.log('🔥 [Dashboard] NEW delivery notification detected, updating ref and refetching');
prevDeliveryNotificationsRef.current = latestDeliveryNotificationId;
const latest = deliveryNotifications[0];
@@ -262,7 +329,7 @@ export function NewDashboardPage() {
});
if (latestOrchestrationNotificationId &&
latestOrchestrationNotificationId !== prevOrchestrationNotificationsRef.current) {
latestOrchestrationNotificationId !== prevOrchestrationNotificationsRef.current) {
console.log('🔥 [Dashboard] NEW orchestration notification detected, updating ref and refetching');
prevOrchestrationNotificationsRef.current = latestOrchestrationNotificationId;
const latest = orchestrationNotifications[0];
@@ -401,6 +468,17 @@ export function NewDashboardPage() {
// Calculate overall progress
const { completedSections, totalSections, progressPercentage, criticalMissing, recommendedMissing } = useMemo(() => {
// If data is still loading, use stored value as fallback to prevent flickering
if (loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality) {
return {
completedSections: 0,
totalSections: 4, // 4 required sections
progressPercentage: setupProgressFromStorage, // Use stored value during loading
criticalMissing: [],
recommendedMissing: [],
};
}
// Guard against undefined or invalid setupSections
if (!setupSections || !Array.isArray(setupSections) || setupSections.length === 0) {
return {
@@ -420,6 +498,13 @@ export function NewDashboardPage() {
const critical = setupSections.filter(s => !s.isComplete && s.id !== 'quality');
const recommended = setupSections.filter(s => s.count < s.recommended);
// PHASE 1 OPTIMIZATION: Cache progress to localStorage for next page load
try {
localStorage.setItem(`setup_progress_${tenantId}`, percentage.toString());
} catch {
// Ignore storage errors
}
return {
completedSections: completed,
totalSections: total,
@@ -427,7 +512,7 @@ export function NewDashboardPage() {
criticalMissing: critical,
recommendedMissing: recommended,
};
}, [setupSections]);
}, [setupSections, tenantId, loadingIngredients, loadingSuppliers, loadingRecipes, loadingQuality, setupProgressFromStorage]);
const handleRefreshAll = () => {
refetchHealth();
@@ -547,8 +632,8 @@ export function NewDashboardPage() {
</div>
{/* Setup Flow - Three States */}
{loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality ? (
/* Loading state */
{loadingIngredients || loadingSuppliers || loadingRecipes || loadingQuality || !isReady ? (
/* Loading state - only show spinner until first priority data (health) is ready */
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div>
</div>
@@ -570,36 +655,60 @@ export function NewDashboardPage() {
{/* Main Dashboard Layout */}
<div className="space-y-6">
{/* SECTION 1: Glanceable Health Hero (Traffic Light) */}
{/* SECTION 1: Glanceable Health Hero (Traffic Light) - PRIORITY 1 */}
<div data-tour="dashboard-stats">
<GlanceableHealthHero
healthStatus={healthStatus}
loading={healthLoading}
urgentActionCount={actionQueue?.urgentCount || 0}
/>
{healthLoading ? (
<div className="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-primary)] p-6 animate-pulse">
<div className="h-24 bg-[var(--bg-secondary)] rounded"></div>
</div>
) : (
<GlanceableHealthHero
healthStatus={healthStatus!}
loading={false}
urgentActionCount={actionQueue?.urgentCount || 0}
/>
)}
</div>
{/* SECTION 2: What Needs Your Attention (Unified Action Queue) */}
{/* SECTION 2: What Needs Your Attention (Unified Action Queue) - PRIORITY 2 */}
<div data-tour="pending-po-approvals">
<UnifiedActionQueueCard
actionQueue={actionQueue}
loading={actionQueueLoading}
tenantId={tenantId}
/>
{actionQueueLoading ? (
<div className="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-primary)] p-6 animate-pulse">
<div className="space-y-4">
<div className="h-8 bg-[var(--bg-secondary)] rounded w-1/3"></div>
<div className="h-32 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
) : (
<UnifiedActionQueueCard
actionQueue={actionQueue!}
loading={false}
tenantId={tenantId}
/>
)}
</div>
{/* SECTION 3: Execution Progress Tracker (Plan vs Actual) */}
{/* SECTION 3: Execution Progress Tracker (Plan vs Actual) - PRIORITY 3 */}
<div data-tour="execution-progress">
<ExecutionProgressTracker
progress={executionProgress}
loading={executionProgressLoading}
/>
{executionProgressLoading ? (
<div className="bg-[var(--bg-primary)] rounded-lg border border-[var(--border-primary)] p-6 animate-pulse">
<div className="space-y-4">
<div className="h-8 bg-[var(--bg-secondary)] rounded w-1/2"></div>
<div className="h-24 bg-[var(--bg-secondary)] rounded"></div>
</div>
</div>
) : (
<ExecutionProgressTracker
progress={executionProgress}
loading={false}
/>
)}
</div>
{/* SECTION 4: Intelligent System Summary - Unified AI Impact & Orchestration */}
<div data-tour="intelligent-system-summary">
<IntelligentSystemSummaryCard
orchestrationSummary={orchestrationSummary}
orchestrationSummary={orchestrationSummary!}
orchestrationLoading={orchestrationLoading}
onWorkflowComplete={handleRefreshAll}
/>
@@ -679,4 +788,30 @@ export function NewDashboardPage() {
);
}
export default NewDashboardPage;
/**
* Main Dashboard Page
* Conditionally renders either the Enterprise Dashboard or the Bakery Dashboard
* based on the user's subscription tier.
*/
export function DashboardPage() {
const { subscriptionInfo } = useSubscription();
const { currentTenant } = useTenant();
const { plan, loading } = subscriptionInfo;
const tenantId = currentTenant?.id;
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--color-primary)' }}></div>
</div>
);
}
if (plan === SUBSCRIPTION_TIERS.ENTERPRISE) {
return <EnterpriseDashboardPage tenantId={tenantId} />;
}
return <BakeryDashboard />;
}
export default DashboardPage;

View File

@@ -5,57 +5,82 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery, useQueries } from '@tanstack/react-query';
import {
useNetworkSummary,
useChildrenPerformance,
useDistributionOverview,
useForecastSummary
} from '../../api/hooks/enterprise';
} from '../../api/hooks/useEnterpriseDashboard';
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card';
import { Badge } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import {
Users,
ShoppingCart,
TrendingUp,
MapPin,
Truck,
Package,
BarChart3,
Network,
Store,
Activity,
Calendar,
Clock,
CheckCircle,
AlertTriangle,
PackageCheck,
Building2,
DollarSign
ArrowLeft,
ChevronRight
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import { ErrorBoundary } from 'react-error-boundary';
import { apiClient } from '../../api/client/apiClient';
import { useEnterprise } from '../../contexts/EnterpriseContext';
import { useTenant } from '../../stores/tenant.store';
// Components for enterprise dashboard
const NetworkSummaryCards = React.lazy(() => import('../../components/dashboard/NetworkSummaryCards'));
const DistributionMap = React.lazy(() => import('../../components/maps/DistributionMap'));
const PerformanceChart = React.lazy(() => import('../../components/charts/PerformanceChart'));
const EnterpriseDashboardPage = () => {
const { tenantId } = useParams();
interface EnterpriseDashboardPageProps {
tenantId?: string;
}
const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenantId: propTenantId }) => {
const { tenantId: urlTenantId } = useParams<{ tenantId: string }>();
const tenantId = propTenantId || urlTenantId;
const navigate = useNavigate();
const { t } = useTranslation('dashboard');
const { state: enterpriseState, drillDownToOutlet, returnToNetworkView, enterNetworkView } = useEnterprise();
const { switchTenant } = useTenant();
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
// Check if tenantId is available at the start
useEffect(() => {
if (!tenantId) {
console.error('No tenant ID available for enterprise dashboard');
navigate('/unauthorized');
}
}, [tenantId, navigate]);
// Initialize enterprise mode on mount
useEffect(() => {
if (tenantId && !enterpriseState.parentTenantId) {
enterNetworkView(tenantId);
}
}, [tenantId, enterpriseState.parentTenantId, enterNetworkView]);
// Check if user has enterprise tier access
useEffect(() => {
const checkAccess = async () => {
if (!tenantId) {
console.error('No tenant ID available for enterprise dashboard');
navigate('/unauthorized');
return;
}
try {
const response = await apiClient.get<{ tenant_type: string }>(`/tenants/${tenantId}`);
@@ -78,6 +103,7 @@ const EnterpriseDashboardPage = () => {
error: networkSummaryError
} = useNetworkSummary(tenantId!, {
refetchInterval: 60000, // Refetch every minute
enabled: !!tenantId, // Only fetch if tenantId is available
});
// Fetch children performance data
@@ -85,7 +111,9 @@ const EnterpriseDashboardPage = () => {
data: childrenPerformance,
isLoading: isChildrenPerformanceLoading,
error: childrenPerformanceError
} = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod);
} = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod, {
enabled: !!tenantId, // Only fetch if tenantId is available
});
// Fetch distribution overview data
const {
@@ -94,6 +122,7 @@ const EnterpriseDashboardPage = () => {
error: distributionError
} = useDistributionOverview(tenantId!, selectedDate, {
refetchInterval: 60000, // Refetch every minute
enabled: !!tenantId, // Only fetch if tenantId is available
});
// Fetch enterprise forecast summary
@@ -101,7 +130,36 @@ const EnterpriseDashboardPage = () => {
data: forecastSummary,
isLoading: isForecastLoading,
error: forecastError
} = useForecastSummary(tenantId!);
} = useForecastSummary(tenantId!, 7, {
enabled: !!tenantId, // Only fetch if tenantId is available
});
// Handle outlet drill-down
const handleOutletClick = async (outletId: string, outletName: string) => {
// Calculate network metrics if available
const networkMetrics = childrenPerformance?.rankings ? {
totalSales: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'sales' ? r.metric_value : 0), 0),
totalProduction: 0,
totalInventoryValue: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'inventory_value' ? r.metric_value : 0), 0),
averageSales: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'sales' ? r.metric_value : 0), 0) / childrenPerformance.rankings.length,
averageProduction: 0,
averageInventoryValue: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'inventory_value' ? r.metric_value : 0), 0) / childrenPerformance.rankings.length,
childCount: childrenPerformance.rankings.length
} : undefined;
drillDownToOutlet(outletId, outletName, networkMetrics);
await switchTenant(outletId);
navigate('/app/dashboard');
};
// Handle return to network view
const handleReturnToNetwork = async () => {
if (enterpriseState.parentTenantId) {
returnToNetworkView();
await switchTenant(enterpriseState.parentTenantId);
navigate(`/app/enterprise/${enterpriseState.parentTenantId}`);
}
};
// Error boundary fallback
const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) => (
@@ -142,18 +200,77 @@ const EnterpriseDashboardPage = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<div className="p-6 min-h-screen bg-gray-50">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<Network className="w-8 h-8 text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">
{t('enterprise.network_dashboard')}
</h1>
<div className="px-4 sm:px-6 lg:px-8 py-6 min-h-screen" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{/* Breadcrumb / Return to Network Banner */}
{enterpriseState.selectedOutletId && !enterpriseState.isNetworkView && (
<div className="mb-6 rounded-lg p-4" style={{
backgroundColor: 'var(--color-info-light, #dbeafe)',
borderColor: 'var(--color-info, #3b82f6)',
borderWidth: '1px',
borderStyle: 'solid'
}}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Network className="w-5 h-5" style={{ color: 'var(--color-info)' }} />
<div className="flex items-center gap-2 text-sm">
<span className="font-medium" style={{ color: 'var(--color-info)' }}>Network Overview</span>
<ChevronRight className="w-4 h-4" style={{ color: 'var(--color-info-light, #93c5fd)' }} />
<span className="text-gray-700 font-semibold">{enterpriseState.selectedOutletName}</span>
</div>
</div>
<Button
onClick={handleReturnToNetwork}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Return to Network View
</Button>
</div>
{enterpriseState.networkMetrics && (
<div className="mt-3 pt-3 border-t grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 text-sm"
style={{ borderColor: 'var(--color-info-light, #93c5fd)' }}>
<div>
<span style={{ color: 'var(--color-info)' }}>Network Average Sales:</span>
<span className="ml-2 font-semibold">{enterpriseState.networkMetrics.averageSales.toLocaleString()}</span>
</div>
<div>
<span style={{ color: 'var(--color-info)' }}>Total Outlets:</span>
<span className="ml-2 font-semibold">{enterpriseState.networkMetrics.childCount}</span>
</div>
<div>
<span style={{ color: 'var(--color-info)' }}>Network Total:</span>
<span className="ml-2 font-semibold">{enterpriseState.networkMetrics.totalSales.toLocaleString()}</span>
</div>
</div>
)}
</div>
)}
{/* Enhanced Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
{/* Title Section with Gradient Icon */}
<div className="flex items-center gap-4">
<div
className="w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg"
style={{
background: 'linear-gradient(135deg, var(--color-info) 0%, var(--color-primary) 100%)',
}}
>
<Network className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>
{t('enterprise.network_dashboard')}
</h1>
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>
{t('enterprise.network_summary_description')}
</p>
</div>
</div>
</div>
<p className="text-gray-600">
{t('enterprise.network_summary_description')}
</p>
</div>
{/* Network Summary Cards */}
@@ -234,6 +351,7 @@ const EnterpriseDashboardPage = () => {
data={childrenPerformance.rankings}
metric={selectedMetric}
period={selectedPeriod}
onOutletClick={handleOutletClick}
/>
) : (
<div className="h-96 flex items-center justify-center text-gray-500">
@@ -254,34 +372,78 @@ const EnterpriseDashboardPage = () => {
</CardHeader>
<CardContent>
{forecastSummary && forecastSummary.aggregated_forecasts ? (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Package className="w-4 h-4 text-blue-600" />
<h3 className="font-semibold text-blue-800">{t('enterprise.total_demand')}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Demand Card */}
<div
className="p-6 rounded-xl border-2 transition-all duration-300 hover:shadow-lg"
style={{
backgroundColor: 'var(--color-info-50)',
borderColor: 'var(--color-info-200)',
}}
>
<div className="flex items-center gap-3 mb-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
style={{ backgroundColor: 'var(--color-info-100)' }}
>
<Package className="w-5 h-5" style={{ color: 'var(--color-info-600)' }} />
</div>
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-info-800)' }}>
{t('enterprise.total_demand')}
</h3>
</div>
<p className="text-2xl font-bold text-blue-900">
<p className="text-3xl font-bold" style={{ color: 'var(--color-info-900)' }}>
{Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
total + Object.values(day).reduce((dayTotal: number, product: any) =>
dayTotal + (product.predicted_demand || 0), 0), 0
).toLocaleString()}
</p>
</div>
<div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Calendar className="w-4 h-4 text-green-600" />
<h3 className="font-semibold text-green-800">{t('enterprise.days_forecast')}</h3>
{/* Days Forecast Card */}
<div
className="p-6 rounded-xl border-2 transition-all duration-300 hover:shadow-lg"
style={{
backgroundColor: 'var(--color-success-50)',
borderColor: 'var(--color-success-200)',
}}
>
<div className="flex items-center gap-3 mb-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
style={{ backgroundColor: 'var(--color-success-100)' }}
>
<Calendar className="w-5 h-5" style={{ color: 'var(--color-success-600)' }} />
</div>
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-success-800)' }}>
{t('enterprise.days_forecast')}
</h3>
</div>
<p className="text-2xl font-bold text-green-900">
<p className="text-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
{forecastSummary.days_forecast || 7}
</p>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Activity className="w-4 h-4 text-purple-600" />
<h3 className="font-semibold text-purple-800">{t('enterprise.avg_daily_demand')}</h3>
{/* Average Daily Demand Card */}
<div
className="p-6 rounded-xl border-2 transition-all duration-300 hover:shadow-lg"
style={{
backgroundColor: 'var(--color-secondary-50)',
borderColor: 'var(--color-secondary-200)',
}}
>
<div className="flex items-center gap-3 mb-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
style={{ backgroundColor: 'var(--color-secondary-100)' }}
>
<Activity className="w-5 h-5" style={{ color: 'var(--color-secondary-600)' }} />
</div>
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-secondary-800)' }}>
{t('enterprise.avg_daily_demand')}
</h3>
</div>
<p className="text-2xl font-bold text-purple-900">
<p className="text-3xl font-bold" style={{ color: 'var(--color-secondary-900)' }}>
{forecastSummary.aggregated_forecasts
? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
total + Object.values(day).reduce((dayTotal: number, product: any) =>
@@ -291,12 +453,27 @@ const EnterpriseDashboardPage = () => {
: 0}
</p>
</div>
<div className="bg-yellow-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-yellow-600" />
<h3 className="font-semibold text-yellow-800">{t('enterprise.last_updated')}</h3>
{/* Last Updated Card */}
<div
className="p-6 rounded-xl border-2 transition-all duration-300 hover:shadow-lg"
style={{
backgroundColor: 'var(--color-warning-50)',
borderColor: 'var(--color-warning-200)',
}}
>
<div className="flex items-center gap-3 mb-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
style={{ backgroundColor: 'var(--color-warning-100)' }}
>
<Clock className="w-5 h-5" style={{ color: 'var(--color-warning-600)' }} />
</div>
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-warning-800)' }}>
{t('enterprise.last_updated')}
</h3>
</div>
<p className="text-sm text-yellow-900">
<p className="text-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
{forecastSummary.last_updated ?
new Date(forecastSummary.last_updated).toLocaleTimeString() :
'N/A'}
@@ -313,7 +490,7 @@ const EnterpriseDashboardPage = () => {
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">

View File

@@ -6,14 +6,14 @@ import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useAIInsights, useAIInsightStats, useApplyInsight, useDismissInsight } from '../../../../api/hooks/aiInsights';
import { AIInsight } from '../../../../api/services/aiInsights';
import { useReasoningTranslation } from '../../../../hooks/useReasoningTranslation';
import { useTranslation } from 'react-i18next';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id;
const { t } = useReasoningTranslation();
const { t } = useTranslation('reasoning');
// Fetch real insights from API
const { data: insightsData, isLoading, refetch } = useAIInsights(

View File

@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react';
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card, EmptyState } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
@@ -40,7 +41,7 @@ interface ModelStatus {
}
const ModelsConfigPage: React.FC = () => {
const navigate = useNavigate();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
@@ -495,9 +496,9 @@ const ModelsConfigPage: React.FC = () => {
model={selectedModel}
onRetrain={handleRetrain}
onViewPredictions={(modelId) => {
// TODO: Navigate to forecast history or predictions view
// This should show historical predictions vs actual sales
console.log('View predictions for model:', modelId);
// Navigate to forecast history page filtered by this model
navigate(`/app/operations/forecasting?model_id=${modelId}&view=history`);
setShowModelDetailsModal(false);
}}
/>
)}

View File

@@ -0,0 +1,300 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Truck,
Plus,
Package,
MapPin,
Calendar,
ArrowRight,
Search,
Filter,
MoreVertical,
Clock,
CheckCircle,
AlertTriangle
} from 'lucide-react';
import {
Button,
StatsGrid,
Card,
CardContent,
CardHeader,
CardTitle,
Badge,
Input
} from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useTenant } from '../../../../stores/tenant.store';
import { useDistributionOverview } from '../../../../api/hooks/useEnterpriseDashboard';
import DistributionMap from '../../../../components/maps/DistributionMap';
const DistributionPage: React.FC = () => {
const { t } = useTranslation(['operations', 'common', 'dashboard']);
const { currentTenant: tenant } = useTenant();
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [activeTab, setActiveTab] = useState<'overview' | 'routes' | 'shipments'>('overview');
// Fetch real distribution data
const { data: distributionData, isLoading } = useDistributionOverview(
tenant?.id || '',
selectedDate,
{ enabled: !!tenant?.id }
);
// Derive stats from real data
const stats = [
{
title: t('operations:stats.active_routes', 'Rutas Activas'),
value: distributionData?.route_sequences?.filter((r: any) => r.status === 'in_progress').length || 0,
variant: 'info' as const,
icon: Truck,
},
{
title: t('operations:stats.pending_deliveries', 'Entregas Pendientes'),
value: distributionData?.status_counts?.pending || 0,
variant: 'warning' as const,
icon: Package,
},
{
title: t('operations:stats.completed_deliveries', 'Entregas Completadas'),
value: distributionData?.status_counts?.delivered || 0,
variant: 'success' as const,
icon: CheckCircle,
},
{
title: t('operations:stats.total_routes', 'Total Rutas'),
value: distributionData?.route_sequences?.length || 0,
variant: 'default' as const,
icon: MapPin,
},
];
const handleNewRoute = () => {
// Navigate to create route page or open modal
console.log('New route clicked');
};
if (!tenant) return null;
// Prepare shipment status data safely
const shipmentStatus = {
pending: distributionData?.status_counts?.pending || 0,
in_transit: distributionData?.status_counts?.in_transit || 0,
delivered: distributionData?.status_counts?.delivered || 0,
failed: distributionData?.status_counts?.failed || 0,
};
return (
<div className="space-y-6">
<PageHeader
title={t('operations:distribution.title', 'Distribución y Logística')}
description={t('operations:distribution.description', 'Gestión integral de la flota de reparto y seguimiento de entregas en tiempo real')}
actions={[
{
id: "date-select",
label: selectedDate,
variant: "outline" as const,
icon: Calendar,
onClick: () => { }, // In a real app this would trigger a date picker
size: "md"
},
{
id: "add-new-route",
label: t('operations:actions.new_route', 'Nueva Ruta'),
variant: "primary" as const,
icon: Plus,
onClick: handleNewRoute,
tooltip: t('operations:tooltips.new_route', 'Crear una nueva ruta de distribución'),
size: "md"
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={4}
/>
{/* Main Content Areas */}
<div className="flex flex-col gap-6">
{/* Tabs Navigation */}
<div className="flex border-b border-gray-200">
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'overview'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('overview')}
>
Vista General
</button>
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'routes'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('routes')}
>
Listado de Rutas
</button>
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'shipments'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('shipments')}
>
Listado de Envíos
</button>
</div>
{/* Content based on Active Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Map Section */}
<Card className="overflow-hidden border-none shadow-lg">
<CardHeader className="bg-white border-b sticky top-0 z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-2 bg-blue-100 rounded-lg">
<MapPin className="w-5 h-5 text-blue-600" />
</div>
<div>
<CardTitle>{t('operations:map.title', 'Mapa de Distribución')}</CardTitle>
<p className="text-sm text-gray-500">Visualización en tiempo real de la flota</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
En Vivo
</Badge>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="p-4 bg-slate-50">
<DistributionMap
routes={distributionData?.route_sequences || []}
shipments={shipmentStatus}
/>
</div>
</CardContent>
</Card>
{/* Recent Activity / Quick List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Rutas en Progreso</CardTitle>
</CardHeader>
<CardContent>
{distributionData?.route_sequences?.filter((r: any) => r.status === 'in_progress').length > 0 ? (
<div className="space-y-4">
{distributionData.route_sequences
.filter((r: any) => r.status === 'in_progress')
.map((route: any) => (
<div key={route.id} className="flex items-center justify-between p-3 bg-white border rounded-lg shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-50 rounded-full">
<Truck className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="font-medium text-sm text-gray-900">Ruta {route.route_number}</p>
<p className="text-xs text-gray-500">{route.formatted_driver_name || 'Sin conductor asignado'}</p>
</div>
</div>
<Badge variant="info">En Ruta</Badge>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
No hay rutas en progreso actualmente.
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{activeTab === 'routes' && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Listado de Rutas</CardTitle>
<div className="flex gap-2">
<Input
placeholder="Buscar rutas..."
leftIcon={<Search className="w-4 h-4 text-gray-400" />}
className="w-64"
/>
<Button variant="outline" size="sm" leftIcon={<Filter className="w-4 h-4" />}>Filtros</Button>
</div>
</div>
</CardHeader>
<CardContent>
{(distributionData?.route_sequences?.length || 0) > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th className="px-4 py-3">Ruta</th>
<th className="px-4 py-3">Estado</th>
<th className="px-4 py-3">Distancia</th>
<th className="px-4 py-3">Duración Est.</th>
<th className="px-4 py-3">Paradas</th>
<th className="px-4 py-3 text-right">Acciones</th>
</tr>
</thead>
<tbody>
{distributionData.route_sequences.map((route: any) => (
<tr key={route.id} className="border-b hover:bg-gray-50">
<td className="px-4 py-3 font-medium">{route.route_number}</td>
<td className="px-4 py-3">
<Badge variant={
route.status === 'completed' ? 'success' :
route.status === 'in_progress' ? 'info' :
route.status === 'pending' ? 'warning' : 'default'
}>
{route.status}
</Badge>
</td>
<td className="px-4 py-3">{route.total_distance_km?.toFixed(1) || '-'} km</td>
<td className="px-4 py-3">{route.estimated_duration_minutes || '-'} min</td>
<td className="px-4 py-3">{route.route_points?.length || 0}</td>
<td className="px-4 py-3 text-right">
<Button variant="ghost" size="sm" leftIcon={<MoreVertical className="w-4 h-4" />} />
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed">
<p className="text-gray-500">No se encontraron rutas para esta fecha.</p>
</div>
)}
</CardContent>
</Card>
)}
{/* Similar structure for Shipments tab, simplified for now */}
{activeTab === 'shipments' && (
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed">
<Package className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<h3 className="text-lg font-medium text-gray-900">Gestión de Envíos</h3>
<p className="text-gray-500">Funcionalidad de listado detallado de envíos próximamente.</p>
</div>
)}
</div>
</div>
);
};
export default DistributionPage;

View File

@@ -271,9 +271,34 @@ const TeamPage: React.FC = () => {
};
const handleSaveMember = async () => {
// TODO: Implement member update logic
console.log('Saving member:', memberFormData);
setShowMemberModal(false);
try {
// Update user profile
if (selectedMember?.user_id) {
await userService.updateUser(selectedMember.user_id, {
full_name: memberFormData.full_name,
email: memberFormData.email,
phone: memberFormData.phone,
language: memberFormData.language,
timezone: memberFormData.timezone
});
}
// Update role if changed
if (memberFormData.role !== selectedMember?.role) {
await updateRoleMutation.mutateAsync({
tenantId,
memberUserId: selectedMember.user_id,
newRole: memberFormData.role
});
}
showToast.success(t('settings:team.member_updated_success', 'Miembro actualizado exitosamente'));
setShowMemberModal(false);
setModalMode('view');
} catch (error) {
console.error('Error updating member:', error);
showToast.error(t('settings:team.member_updated_error', 'Error al actualizar miembro'));
}
};
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: string | number) => {