New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -0,0 +1,372 @@
/*
* 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 { useQuery, useQueries } from '@tanstack/react-query';
import {
useNetworkSummary,
useChildrenPerformance,
useDistributionOverview,
useForecastSummary
} from '../../api/hooks/enterprise';
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
} 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';
// 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();
const navigate = useNavigate();
const { t } = useTranslation('dashboard');
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
// Check if user has enterprise tier access
useEffect(() => {
const checkAccess = async () => {
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
});
// Fetch children performance data
const {
data: childrenPerformance,
isLoading: isChildrenPerformanceLoading,
error: childrenPerformanceError
} = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod);
// Fetch distribution overview data
const {
data: distributionOverview,
isLoading: isDistributionLoading,
error: distributionError
} = useDistributionOverview(tenantId!, selectedDate, {
refetchInterval: 60000, // Refetch every minute
});
// Fetch enterprise forecast summary
const {
data: forecastSummary,
isLoading: isForecastLoading,
error: forecastError
} = useForecastSummary(tenantId!);
// 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}>
<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>
<p className="text-gray-600">
{t('enterprise.network_summary_description')}
</p>
</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}
/>
) : (
<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 ? (
<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>
<p className="text-2xl font-bold text-blue-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>
</div>
<p className="text-2xl font-bold text-green-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>
</div>
<p className="text-2xl font-bold text-purple-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>
</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>
</div>
<p className="text-sm text-yellow-900">
{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 */}
<div className="grid grid-cols-1 md: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-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;