Imporve enterprise

This commit is contained in:
Urtzi Alfaro
2025-12-17 20:50:22 +01:00
parent e3ef47b879
commit f8591639a7
28 changed files with 6802 additions and 258 deletions

View File

@@ -13,6 +13,7 @@ import {
} from '../../api/hooks/useEnterpriseDashboard';
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card';
import { Button } from '../../components/ui/Button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/Tabs';
import {
TrendingUp,
MapPin,
@@ -27,7 +28,11 @@ import {
PackageCheck,
Building2,
ArrowLeft,
ChevronRight
ChevronRight,
Target,
Warehouse,
ShoppingCart,
ShieldCheck
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
@@ -35,11 +40,18 @@ import { ErrorBoundary } from 'react-error-boundary';
import { apiClient } from '../../api/client/apiClient';
import { useEnterprise } from '../../contexts/EnterpriseContext';
import { useTenant } from '../../stores/tenant.store';
import { useSSEEvents } from '../../hooks/useSSE';
import { useQueryClient } from '@tanstack/react-query';
// 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 NetworkOverviewTab = React.lazy(() => import('../../components/dashboard/NetworkOverviewTab'));
const NetworkPerformanceTab = React.lazy(() => import('../../components/dashboard/NetworkPerformanceTab'));
const OutletFulfillmentTab = React.lazy(() => import('../../components/dashboard/OutletFulfillmentTab'));
const ProductionTab = React.lazy(() => import('../../components/dashboard/ProductionTab'));
const DistributionTab = React.lazy(() => import('../../components/dashboard/DistributionTab'));
interface EnterpriseDashboardPageProps {
tenantId?: string;
@@ -56,6 +68,51 @@ const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenan
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [activeTab, setActiveTab] = useState('overview');
const queryClient = useQueryClient();
// SSE Integration for real-time updates
const { events: sseEvents } = useSSEEvents({
channels: ['*.alerts', '*.notifications', 'recommendations']
});
// Invalidate enterprise data on relevant SSE events
useEffect(() => {
if (sseEvents.length === 0 || !tenantId) return;
const latest = sseEvents[0];
const relevantEventTypes = [
'batch_completed', 'batch_started', 'batch_state_changed',
'delivery_received', 'delivery_overdue', 'delivery_arriving_soon',
'stock_receipt_incomplete', 'orchestration_run_completed',
'production_delay', 'batch_start_delayed', 'equipment_maintenance',
'network_alert', 'outlet_performance_update', 'distribution_route_update'
];
if (relevantEventTypes.includes(latest.event_type)) {
// Invalidate all enterprise dashboard queries
queryClient.invalidateQueries({
queryKey: ['enterprise', 'network-summary', tenantId],
refetchType: 'active',
});
queryClient.invalidateQueries({
queryKey: ['enterprise', 'children-performance', tenantId],
refetchType: 'active',
});
queryClient.invalidateQueries({
queryKey: ['enterprise', 'distribution-overview', tenantId],
refetchType: 'active',
});
queryClient.invalidateQueries({
queryKey: ['enterprise', 'forecast-summary', tenantId],
refetchType: 'active',
});
queryClient.invalidateQueries({
queryKey: ['control-panel-data', tenantId],
refetchType: 'active',
});
}
}, [sseEvents, tenantId, queryClient]);
// Check if tenantId is available at the start
useEffect(() => {
@@ -273,258 +330,187 @@ const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenan
</div>
</div>
{/* Network Summary Cards */}
<div className="mb-8">
<NetworkSummaryCards
data={networkSummary}
isLoading={isNetworkSummaryLoading}
/>
</div>
{/* Main Tabs Structure */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList className="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-8 gap-2 mb-8">
<TabsTrigger value="overview">
<Network className="w-4 h-4 mr-2" />
{t('enterprise.network_status')}
</TabsTrigger>
{/* 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-[var(--color-info)]" />
<CardTitle>{t('enterprise.distribution_map')}</CardTitle>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-[var(--text-secondary)]" />
<input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
className="border border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
/>
</div>
</CardHeader>
<CardContent>
{distributionOverview ? (
<DistributionMap
routes={distributionOverview.route_sequences}
shipments={distributionOverview.status_counts}
/>
) : (
<div className="h-96 flex items-center justify-center text-[var(--text-secondary)]">
{t('enterprise.no_distribution_data')}
</div>
)}
</CardContent>
</Card>
</div>
<TabsTrigger value="network-performance">
<Target className="w-4 h-4 mr-2" />
{t('enterprise.network_performance')}
</TabsTrigger>
<TabsTrigger value="fulfillment">
<Warehouse className="w-4 h-4 mr-2" />
{t('enterprise.outlet_fulfillment')}
</TabsTrigger>
<TabsTrigger value="distribution">
<Truck className="w-4 h-4 mr-2" />
{t('enterprise.distribution_map')}
</TabsTrigger>
<TabsTrigger value="forecast">
<TrendingUp className="w-4 h-4 mr-2" />
{t('enterprise.network_forecast')}
</TabsTrigger>
<TabsTrigger value="production">
<Package className="w-4 h-4 mr-2" />
{t('enterprise.production')}
</TabsTrigger>
{/* 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-[var(--color-success)]" />
<CardTitle>{t('enterprise.outlet_performance')}</CardTitle>
</div>
<div className="flex gap-2">
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="border border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
>
<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 border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
>
<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}
onOutletClick={handleOutletClick}
/>
) : (
<div className="h-96 flex items-center justify-center text-[var(--text-secondary)]">
{t('enterprise.no_performance_data')}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</TabsList>
{/* Forecast Summary */}
<div className="mb-8">
<Card>
<CardHeader className="flex flex-row items-center gap-2">
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
<CardTitle>{t('enterprise.network_forecast')}</CardTitle>
</CardHeader>
<CardContent>
{forecastSummary && forecastSummary.aggregated_forecasts ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Demand Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-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>
</CardContent>
</Card>
{/* Tab Content */}
<TabsContent value="overview">
<NetworkOverviewTab
tenantId={tenantId!}
onOutletClick={handleOutletClick}
/>
</TabsContent>
{/* Days Forecast Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
{forecastSummary.days_forecast || 7}
</p>
</CardContent>
</Card>
{/* Average Daily Demand Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-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) =>
dayTotal + (product.predicted_demand || 0), 0), 0) /
Object.keys(forecastSummary.aggregated_forecasts).length
).toLocaleString()
: 0}
</p>
</CardContent>
</Card>
{/* Last Updated Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
{forecastSummary.last_updated ?
new Date(forecastSummary.last_updated).toLocaleTimeString() :
'N/A'}
</p>
</CardContent>
</Card>
</div>
) : (
<div className="flex items-center justify-center h-48 text-[var(--text-secondary)]">
{t('enterprise.no_forecast_data')}
</div>
)}
</CardContent>
</Card>
</div>
<TabsContent value="network-performance">
<NetworkPerformanceTab
tenantId={tenantId!}
onOutletClick={handleOutletClick}
/>
</TabsContent>
{/* Quick Actions */}
<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">
<Building2 className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Agregar Punto de Venta</h3>
</div>
<p className="text-[var(--text-secondary)] 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>
<TabsContent value="distribution">
<DistributionTab
tenantId={tenantId!}
selectedDate={selectedDate}
onDateChange={setSelectedDate}
/>
</TabsContent>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<PackageCheck className="w-6 h-6 text-[var(--color-success)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Transferencias Internas</h3>
</div>
<p className="text-[var(--text-secondary)] 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>
<TabsContent value="forecast">
{/* Forecast Summary */}
<div className="mb-8">
<Card>
<CardHeader className="flex flex-row items-center gap-2">
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
<CardTitle>{t('enterprise.network_forecast')}</CardTitle>
</CardHeader>
<CardContent>
{forecastSummary && forecastSummary.aggregated_forecasts ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Demand Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-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>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<MapPin className="w-6 h-6 text-[var(--color-info)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Rutas de Distribución</h3>
</div>
<p className="text-[var(--text-secondary)] 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>
{/* Days Forecast Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
{forecastSummary.days_forecast || 7}
</p>
</CardContent>
</Card>
{/* Average Daily Demand Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-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) =>
dayTotal + (product.predicted_demand || 0), 0), 0) /
Object.keys(forecastSummary.aggregated_forecasts).length
).toLocaleString()
: 0}
</p>
</CardContent>
</Card>
{/* Last Updated Card */}
<Card className="hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-6">
<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-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
{forecastSummary.last_updated ?
new Date(forecastSummary.last_updated).toLocaleTimeString() :
'N/A'}
</p>
</CardContent>
</Card>
</div>
) : (
<div className="flex items-center justify-center h-48 text-[var(--text-secondary)]">
{t('enterprise.no_forecast_data')}
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="fulfillment">
<OutletFulfillmentTab
tenantId={tenantId!}
onOutletClick={handleOutletClick}
/>
</TabsContent>
<TabsContent value="production">
<ProductionTab tenantId={tenantId!} />
</TabsContent>
</Tabs>
</div>
</ErrorBoundary>
);