2025-11-30 09:12:40 +01:00
|
|
|
/*
|
|
|
|
|
* Enterprise Dashboard Page
|
|
|
|
|
* Main dashboard for enterprise parent tenants showing network-wide metrics
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
|
|
|
import {
|
|
|
|
|
useNetworkSummary,
|
|
|
|
|
useChildrenPerformance,
|
|
|
|
|
useDistributionOverview,
|
|
|
|
|
useForecastSummary
|
2025-12-05 20:07:01 +01:00
|
|
|
} from '../../api/hooks/useEnterpriseDashboard';
|
2025-11-30 09:12:40 +01:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card';
|
|
|
|
|
import { Button } from '../../components/ui/Button';
|
|
|
|
|
import {
|
|
|
|
|
TrendingUp,
|
|
|
|
|
MapPin,
|
|
|
|
|
Truck,
|
|
|
|
|
Package,
|
|
|
|
|
BarChart3,
|
|
|
|
|
Network,
|
|
|
|
|
Activity,
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
PackageCheck,
|
|
|
|
|
Building2,
|
2025-12-05 20:07:01 +01:00
|
|
|
ArrowLeft,
|
|
|
|
|
ChevronRight
|
2025-11-30 09:12:40 +01:00
|
|
|
} 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';
|
2025-12-05 20:07:01 +01:00
|
|
|
import { useEnterprise } from '../../contexts/EnterpriseContext';
|
|
|
|
|
import { useTenant } from '../../stores/tenant.store';
|
2025-11-30 09:12:40 +01:00
|
|
|
|
|
|
|
|
// 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'));
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
interface EnterpriseDashboardPageProps {
|
|
|
|
|
tenantId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenantId: propTenantId }) => {
|
|
|
|
|
const { tenantId: urlTenantId } = useParams<{ tenantId: string }>();
|
|
|
|
|
const tenantId = propTenantId || urlTenantId;
|
2025-11-30 09:12:40 +01:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { t } = useTranslation('dashboard');
|
2025-12-05 20:07:01 +01:00
|
|
|
const { state: enterpriseState, drillDownToOutlet, returnToNetworkView, enterNetworkView } = useEnterprise();
|
|
|
|
|
const { switchTenant } = useTenant();
|
2025-11-30 09:12:40 +01:00
|
|
|
|
|
|
|
|
const [selectedMetric, setSelectedMetric] = useState('sales');
|
|
|
|
|
const [selectedPeriod, setSelectedPeriod] = useState(30);
|
|
|
|
|
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-11-30 09:12:40 +01:00
|
|
|
// Check if user has enterprise tier access
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const checkAccess = async () => {
|
2025-12-05 20:07:01 +01:00
|
|
|
if (!tenantId) {
|
|
|
|
|
console.error('No tenant ID available for enterprise dashboard');
|
|
|
|
|
navigate('/unauthorized');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 09:12:40 +01:00
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get<{ tenant_type: string }>(`/tenants/${tenantId}`);
|
|
|
|
|
|
|
|
|
|
if (response.tenant_type !== 'parent') {
|
|
|
|
|
navigate('/unauthorized');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Access check failed:', error);
|
|
|
|
|
navigate('/unauthorized');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
checkAccess();
|
|
|
|
|
}, [tenantId, navigate]);
|
|
|
|
|
|
|
|
|
|
// Fetch network summary data
|
|
|
|
|
const {
|
|
|
|
|
data: networkSummary,
|
|
|
|
|
isLoading: isNetworkSummaryLoading,
|
|
|
|
|
error: networkSummaryError
|
|
|
|
|
} = useNetworkSummary(tenantId!, {
|
|
|
|
|
refetchInterval: 60000, // Refetch every minute
|
2025-12-05 20:07:01 +01:00
|
|
|
enabled: !!tenantId, // Only fetch if tenantId is available
|
2025-11-30 09:12:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Fetch children performance data
|
|
|
|
|
const {
|
|
|
|
|
data: childrenPerformance,
|
|
|
|
|
isLoading: isChildrenPerformanceLoading,
|
|
|
|
|
error: childrenPerformanceError
|
2025-12-05 20:07:01 +01:00
|
|
|
} = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod, {
|
|
|
|
|
enabled: !!tenantId, // Only fetch if tenantId is available
|
|
|
|
|
});
|
2025-11-30 09:12:40 +01:00
|
|
|
|
|
|
|
|
// Fetch distribution overview data
|
|
|
|
|
const {
|
|
|
|
|
data: distributionOverview,
|
|
|
|
|
isLoading: isDistributionLoading,
|
|
|
|
|
error: distributionError
|
|
|
|
|
} = useDistributionOverview(tenantId!, selectedDate, {
|
|
|
|
|
refetchInterval: 60000, // Refetch every minute
|
2025-12-05 20:07:01 +01:00
|
|
|
enabled: !!tenantId, // Only fetch if tenantId is available
|
2025-11-30 09:12:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Fetch enterprise forecast summary
|
|
|
|
|
const {
|
|
|
|
|
data: forecastSummary,
|
|
|
|
|
isLoading: isForecastLoading,
|
|
|
|
|
error: forecastError
|
2025-12-05 20:07:01 +01:00
|
|
|
} = 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}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-30 09:12:40 +01:00
|
|
|
|
|
|
|
|
// Error boundary fallback
|
|
|
|
|
const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) => (
|
|
|
|
|
<div className="p-6 text-center">
|
|
|
|
|
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Something went wrong</h3>
|
|
|
|
|
<p className="text-gray-500 mb-4">{error.message}</p>
|
|
|
|
|
<Button onClick={resetErrorBoundary}>Try again</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isNetworkSummaryLoading || isChildrenPerformanceLoading || isDistributionLoading || isForecastLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 min-h-screen">
|
|
|
|
|
<div className="flex items-center justify-center h-96">
|
|
|
|
|
<LoadingSpinner text={t('enterprise.loading')} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (networkSummaryError || childrenPerformanceError || distributionError || forecastError) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 min-h-screen">
|
|
|
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
|
|
|
|
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-red-800 mb-2">Error Loading Dashboard</h3>
|
|
|
|
|
<p className="text-red-600">
|
|
|
|
|
{networkSummaryError?.message ||
|
|
|
|
|
childrenPerformanceError?.message ||
|
|
|
|
|
distributionError?.message ||
|
|
|
|
|
forecastError?.message}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
2025-12-05 20:07:01 +01:00
|
|
|
<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 */}
|
2025-11-30 09:12:40 +01:00
|
|
|
<div className="mb-8">
|
2025-12-05 20:07:01 +01:00
|
|
|
<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>
|
2025-11-30 09:12:40 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Network Summary Cards */}
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<NetworkSummaryCards
|
|
|
|
|
data={networkSummary}
|
|
|
|
|
isLoading={isNetworkSummaryLoading}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Distribution Map and Performance Chart Row */}
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
|
|
|
{/* Distribution Map */}
|
|
|
|
|
<div>
|
|
|
|
|
<Card className="h-full">
|
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Truck className="w-5 h-5 text-blue-600" />
|
|
|
|
|
<CardTitle>{t('enterprise.distribution_map')}</CardTitle>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Calendar className="w-4 h-4 text-gray-500" />
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={selectedDate}
|
|
|
|
|
onChange={(e) => setSelectedDate(e.target.value)}
|
|
|
|
|
className="border rounded px-2 py-1 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{distributionOverview ? (
|
|
|
|
|
<DistributionMap
|
|
|
|
|
routes={distributionOverview.route_sequences}
|
|
|
|
|
shipments={distributionOverview.status_counts}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="h-96 flex items-center justify-center text-gray-500">
|
|
|
|
|
{t('enterprise.no_distribution_data')}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Performance Chart */}
|
|
|
|
|
<div>
|
|
|
|
|
<Card className="h-full">
|
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<BarChart3 className="w-5 h-5 text-green-600" />
|
|
|
|
|
<CardTitle>{t('enterprise.outlet_performance')}</CardTitle>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<select
|
|
|
|
|
value={selectedMetric}
|
|
|
|
|
onChange={(e) => setSelectedMetric(e.target.value)}
|
|
|
|
|
className="border rounded px-2 py-1 text-sm"
|
|
|
|
|
>
|
|
|
|
|
<option value="sales">{t('enterprise.metrics.sales')}</option>
|
|
|
|
|
<option value="inventory_value">{t('enterprise.metrics.inventory_value')}</option>
|
|
|
|
|
<option value="order_frequency">{t('enterprise.metrics.order_frequency')}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedPeriod}
|
|
|
|
|
onChange={(e) => setSelectedPeriod(Number(e.target.value))}
|
|
|
|
|
className="border rounded px-2 py-1 text-sm"
|
|
|
|
|
>
|
|
|
|
|
<option value={7}>{t('enterprise.last_7_days')}</option>
|
|
|
|
|
<option value={30}>{t('enterprise.last_30_days')}</option>
|
|
|
|
|
<option value={90}>{t('enterprise.last_90_days')}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{childrenPerformance ? (
|
|
|
|
|
<PerformanceChart
|
|
|
|
|
data={childrenPerformance.rankings}
|
|
|
|
|
metric={selectedMetric}
|
|
|
|
|
period={selectedPeriod}
|
2025-12-05 20:07:01 +01:00
|
|
|
onOutletClick={handleOutletClick}
|
2025-11-30 09:12:40 +01:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="h-96 flex items-center justify-center text-gray-500">
|
|
|
|
|
{t('enterprise.no_performance_data')}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Forecast Summary */}
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="flex flex-row items-center gap-2">
|
|
|
|
|
<TrendingUp className="w-5 h-5 text-purple-600" />
|
|
|
|
|
<CardTitle>{t('enterprise.network_forecast')}</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{forecastSummary && forecastSummary.aggregated_forecasts ? (
|
2025-12-05 20:07:01 +01:00
|
|
|
<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>
|
2025-11-30 09:12:40 +01:00
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
<p className="text-3xl font-bold" style={{ color: 'var(--color-info-900)' }}>
|
2025-11-30 09:12:40 +01:00
|
|
|
{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>
|
2025-12-05 20:07:01 +01:00
|
|
|
|
|
|
|
|
{/* 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>
|
2025-11-30 09:12:40 +01:00
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
<p className="text-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
|
2025-11-30 09:12:40 +01:00
|
|
|
{forecastSummary.days_forecast || 7}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
|
|
|
|
|
{/* 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>
|
2025-11-30 09:12:40 +01:00
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
<p className="text-3xl font-bold" style={{ color: 'var(--color-secondary-900)' }}>
|
2025-11-30 09:12:40 +01:00
|
|
|
{forecastSummary.aggregated_forecasts
|
|
|
|
|
? Math.round(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) /
|
|
|
|
|
Object.keys(forecastSummary.aggregated_forecasts).length
|
|
|
|
|
).toLocaleString()
|
|
|
|
|
: 0}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
|
|
|
|
|
{/* 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>
|
2025-11-30 09:12:40 +01:00
|
|
|
</div>
|
2025-12-05 20:07:01 +01:00
|
|
|
<p className="text-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
|
2025-11-30 09:12:40 +01:00
|
|
|
{forecastSummary.last_updated ?
|
|
|
|
|
new Date(forecastSummary.last_updated).toLocaleTimeString() :
|
|
|
|
|
'N/A'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center justify-center h-48 text-gray-500">
|
|
|
|
|
{t('enterprise.no_forecast_data')}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Quick Actions */}
|
2025-12-05 20:07:01 +01:00
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
2025-11-30 09:12:40 +01:00
|
|
|
<Card>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="flex items-center gap-3 mb-4">
|
|
|
|
|
<Building2 className="w-6 h-6 text-blue-600" />
|
|
|
|
|
<h3 className="text-lg font-semibold">Agregar Punto de Venta</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-gray-600 mb-4">Añadir un nuevo outlet a la red enterprise</p>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => navigate(`/app/tenants/${tenantId}/settings/organization`)}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
Crear Outlet
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="flex items-center gap-3 mb-4">
|
|
|
|
|
<PackageCheck className="w-6 h-6 text-green-600" />
|
|
|
|
|
<h3 className="text-lg font-semibold">Transferencias Internas</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-gray-600 mb-4">Gestionar pedidos entre obrador central y outlets</p>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => navigate(`/app/tenants/${tenantId}/procurement/internal-transfers`)}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
Ver Transferencias
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="flex items-center gap-3 mb-4">
|
|
|
|
|
<MapPin className="w-6 h-6 text-red-600" />
|
|
|
|
|
<h3 className="text-lg font-semibold">Rutas de Distribución</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-gray-600 mb-4">Optimizar rutas de entrega entre ubicaciones</p>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => navigate(`/app/tenants/${tenantId}/distribution/routes`)}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
Ver Rutas
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default EnterpriseDashboardPage;
|