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,148 @@
/*
* Performance Chart Component for Enterprise Dashboard
* Shows anonymized performance ranking of child outlets
*/
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Badge } from '../ui/Badge';
import { BarChart3, TrendingUp, TrendingDown, ArrowUp, ArrowDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface PerformanceDataPoint {
rank: number;
tenant_id: string;
anonymized_name: string; // "Outlet 1", "Outlet 2", etc.
metric_value: number;
original_name?: string; // Only for internal use, not displayed
}
interface PerformanceChartProps {
data: PerformanceDataPoint[];
metric: string;
period: number;
}
const PerformanceChart: React.FC<PerformanceChartProps> = ({
data = [],
metric,
period
}) => {
const { t } = useTranslation('dashboard');
// Get metric info
const getMetricInfo = () => {
switch (metric) {
case 'sales':
return {
icon: <TrendingUp className="w-4 h-4" />,
label: t('enterprise.metrics.sales'),
unit: '€',
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'inventory_value':
return {
icon: <Package className="w-4 h-4" />,
label: t('enterprise.metrics.inventory_value'),
unit: '€',
format: (val: number) => val.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
};
case 'order_frequency':
return {
icon: <ShoppingCart className="w-4 h-4" />,
label: t('enterprise.metrics.order_frequency'),
unit: '',
format: (val: number) => Math.round(val).toString()
};
default:
return {
icon: <BarChart3 className="w-4 h-4" />,
label: metric,
unit: '',
format: (val: number) => val.toString()
};
}
};
const metricInfo = getMetricInfo();
// Calculate max value for bar scaling
const maxValue = data.length > 0 ? Math.max(...data.map(item => item.metric_value), 1) : 1;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-blue-600" />
<CardTitle>{t('enterprise.outlet_performance')}</CardTitle>
</div>
<div className="text-sm text-gray-500">
{t('enterprise.performance_based_on_period', {
metric: t(`enterprise.metrics.${metric}`) || metric,
period
})}
</div>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<div className="space-y-4">
{data.map((item, index) => {
const percentage = (item.metric_value / maxValue) * 100;
const isTopPerformer = index === 0;
return (
<div key={item.tenant_id} className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
isTopPerformer
? 'bg-yellow-100 text-yellow-800 border border-yellow-300'
: 'bg-gray-100 text-gray-700'
}`}>
{item.rank}
</div>
<span className="font-medium">{item.anonymized_name}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold">
{metricInfo.unit}{metricInfo.format(item.metric_value)}
</span>
{isTopPerformer && (
<Badge variant="secondary" className="bg-yellow-50 text-yellow-800">
{t('enterprise.top_performer')}
</Badge>
)}
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-500 ${
isTopPerformer
? 'bg-gradient-to-r from-blue-500 to-purple-500'
: 'bg-blue-400'
}`}
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<BarChart3 className="w-12 h-12 mx-auto mb-4 text-gray-300" />
<p>{t('enterprise.no_performance_data')}</p>
<p className="text-sm text-gray-400 mt-1">
{t('enterprise.performance_based_on_period', {
metric: t(`enterprise.metrics.${metric}`) || metric,
period
})}
</p>
</div>
)}
</CardContent>
</Card>
);
};
export default PerformanceChart;

View File

@@ -0,0 +1,158 @@
/*
* Delivery Routes Map Component
* Visualizes delivery routes and shipment status
*/
import React from 'react';
import { Card, CardContent } from '../ui/Card';
import { useTranslation } from 'react-i18next';
interface Route {
route_id: string;
route_number: string;
status: string;
total_distance_km: number;
stops: any[]; // Simplified for now
estimated_duration_minutes: number;
}
interface DeliveryRoutesMapProps {
routes?: Route[];
shipments?: Record<string, number>;
}
export const DeliveryRoutesMap: React.FC<DeliveryRoutesMapProps> = ({ routes, shipments }) => {
const { t } = useTranslation('dashboard');
// Calculate summary stats for display
const totalRoutes = routes?.length || 0;
const totalDistance = routes?.reduce((sum, route) => sum + (route.total_distance_km || 0), 0) || 0;
// Calculate shipment status counts
const pendingShipments = shipments?.pending || 0;
const inTransitShipments = shipments?.in_transit || 0;
const deliveredShipments = shipments?.delivered || 0;
const totalShipments = pendingShipments + inTransitShipments + deliveredShipments;
return (
<div className="space-y-4">
{/* Route Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-blue-600">{t('enterprise.total_routes')}</p>
<p className="text-xl font-bold text-blue-900">{totalRoutes}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-sm text-green-600">{t('enterprise.total_distance')}</p>
<p className="text-xl font-bold text-green-900">{totalDistance.toFixed(1)} km</p>
</div>
<div className="bg-yellow-50 p-3 rounded-lg">
<p className="text-sm text-yellow-600">{t('enterprise.total_shipments')}</p>
<p className="text-xl font-bold text-yellow-900">{totalShipments}</p>
</div>
<div className="bg-purple-50 p-3 rounded-lg">
<p className="text-sm text-purple-600">{t('enterprise.active_routes')}</p>
<p className="text-xl font-bold text-purple-900">
{routes?.filter(r => r.status === 'in_progress').length || 0}
</p>
</div>
</div>
{/* Route Status Legend */}
<div className="flex flex-wrap gap-3 mb-4">
<div className="flex items-center">
<div className="w-3 h-3 bg-gray-300 rounded-full mr-2"></div>
<span className="text-xs">{t('enterprise.planned')}</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
<span className="text-xs">{t('enterprise.pending')}</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-yellow-500 rounded-full mr-2"></div>
<span className="text-xs">{t('enterprise.in_transit')}</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
<span className="text-xs">{t('enterprise.delivered')}</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
<span className="text-xs">{t('enterprise.failed')}</span>
</div>
</div>
{/* Simplified Map Visualization */}
<div className="border rounded-lg p-4 bg-gray-50 min-h-[400px] relative">
<h3 className="text-lg font-semibold mb-4">{t('enterprise.distribution_routes')}</h3>
{routes && routes.length > 0 ? (
<div className="space-y-3">
{/* For each route, show a simplified representation */}
{routes.map((route, index) => {
let statusColor = 'bg-gray-300'; // planned
if (route.status === 'in_progress') statusColor = 'bg-yellow-500';
else if (route.status === 'completed') statusColor = 'bg-green-500';
else if (route.status === 'cancelled') statusColor = 'bg-red-500';
return (
<div key={route.route_id} className="border rounded p-3 bg-white">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium">{t('enterprise.route')} {route.route_number}</h4>
<span className={`px-2 py-1 rounded-full text-xs ${statusColor} text-white`}>
{t(`enterprise.route_status.${route.status}`) || route.status}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-500">{t('enterprise.distance')}:</span>
<span className="ml-1">{route.total_distance_km?.toFixed(1)} km</span>
</div>
<div>
<span className="text-gray-500">{t('enterprise.duration')}:</span>
<span className="ml-1">{Math.round(route.estimated_duration_minutes || 0)} min</span>
</div>
<div>
<span className="text-gray-500">{t('enterprise.stops')}:</span>
<span className="ml-1">{route.stops?.length || 0}</span>
</div>
</div>
{/* Route stops visualization */}
<div className="mt-2 pt-2 border-t">
<div className="flex items-center space-x-2">
{route.stops && route.stops.length > 0 ? (
route.stops.map((stop, stopIndex) => (
<React.Fragment key={stopIndex}>
<div className="flex flex-col items-center">
<div className="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs">
{stopIndex + 1}
</div>
<span className="text-xs mt-1 text-center max-w-[60px] truncate">
{stop.location?.name || `${t('enterprise.stop')} ${stopIndex + 1}`}
</span>
</div>
{stopIndex < route.stops.length - 1 && (
<div className="flex-1 h-0.5 bg-gray-300"></div>
)}
</React.Fragment>
))
) : (
<span className="text-gray-500 text-sm">{t('enterprise.no_stops')}</span>
)}
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="flex items-center justify-center h-64 text-gray-500">
{t('enterprise.no_routes_available')}
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,160 @@
/*
* Network Summary Cards Component for Enterprise Dashboard
*/
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card';
import { Badge } from '../../components/ui/Badge';
import {
Store as StoreIcon,
DollarSign,
Package,
ShoppingCart,
Truck,
Users
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { formatCurrency } from '../../utils/format';
interface NetworkSummaryData {
parent_tenant_id: string;
child_tenant_count: number;
network_sales_30d: number;
production_volume_30d: number;
pending_internal_transfers_count: number;
active_shipments_count: number;
last_updated: string;
}
interface NetworkSummaryCardsProps {
data?: NetworkSummaryData;
isLoading: boolean;
}
const NetworkSummaryCards: React.FC<NetworkSummaryCardsProps> = ({
data,
isLoading
}) => {
const { t } = useTranslation('dashboard');
if (isLoading) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
{[...Array(5)].map((_, index) => (
<Card key={index} className="animate-pulse">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-500 h-4 bg-gray-200 rounded w-3/4"></CardTitle>
</CardHeader>
<CardContent>
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
</CardContent>
</Card>
))}
</div>
);
}
if (!data) {
return (
<div className="text-center py-8 text-gray-500">
{t('enterprise.no_network_data')}
</div>
);
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
{/* Network Outlets Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-500">
{t('enterprise.network_outlets')}
</CardTitle>
<StoreIcon className="w-4 h-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">
{data.child_tenant_count}
</div>
<p className="text-xs text-gray-500 mt-1">
{t('enterprise.outlets_in_network')}
</p>
</CardContent>
</Card>
{/* Network Sales Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-500">
{t('enterprise.network_sales')}
</CardTitle>
<DollarSign className="w-4 h-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">
{formatCurrency(data.network_sales_30d, 'EUR')}
</div>
<p className="text-xs text-gray-500 mt-1">
{t('enterprise.last_30_days')}
</p>
</CardContent>
</Card>
{/* Production Volume Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-500">
{t('enterprise.production_volume')}
</CardTitle>
<Package className="w-4 h-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">
{new Intl.NumberFormat('es-ES').format(data.production_volume_30d)} kg
</div>
<p className="text-xs text-gray-500 mt-1">
{t('enterprise.last_30_days')}
</p>
</CardContent>
</Card>
{/* Pending Internal Transfers Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-500">
{t('enterprise.pending_orders')}
</CardTitle>
<ShoppingCart className="w-4 h-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">
{data.pending_internal_transfers_count}
</div>
<p className="text-xs text-gray-500 mt-1">
{t('enterprise.internal_transfers')}
</p>
</CardContent>
</Card>
{/* Active Shipments Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-500">
{t('enterprise.active_shipments')}
</CardTitle>
<Truck className="w-4 h-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-900">
{data.active_shipments_count}
</div>
<p className="text-xs text-gray-500 mt-1">
{t('enterprise.today')}
</p>
</CardContent>
</Card>
</div>
);
};
export default NetworkSummaryCards;

View File

@@ -0,0 +1,155 @@
/*
* Performance Chart Component
* Shows anonymized ranking of outlets based on selected metric
*/
import React from 'react';
import { Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Card, CardContent } from '../ui/Card';
import { useTranslation } from 'react-i18next';
// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
interface PerformanceData {
rank: number;
tenant_id: string;
anonymized_name: string;
metric_value: number;
}
interface PerformanceChartProps {
data?: PerformanceData[];
metric: string;
period: number;
}
export const PerformanceChart: React.FC<PerformanceChartProps> = ({ data, metric, period }) => {
const { t } = useTranslation('dashboard');
// Prepare chart data
const chartData = {
labels: data?.map(item => item.anonymized_name) || [],
datasets: [
{
label: t(`enterprise.metric_labels.${metric}`) || metric,
data: data?.map(item => item.metric_value) || [],
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: t('enterprise.outlet_performance_chart_title'),
},
tooltip: {
callbacks: {
label: function(context: any) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
if (metric === 'sales') {
label += `${context.parsed.y.toFixed(2)}`;
} else {
label += context.parsed.y;
}
}
return label;
}
}
}
},
scales: {
x: {
title: {
display: true,
text: t('enterprise.outlet'),
},
},
y: {
title: {
display: true,
text: t(`enterprise.metric_labels.${metric}`) || metric,
},
beginAtZero: true,
},
},
};
return (
<div className="space-y-4">
<div className="text-sm text-gray-600">
{t('enterprise.performance_based_on', {
metric: t(`enterprise.metrics.${metric}`) || metric,
period
})}
</div>
{data && data.length > 0 ? (
<div className="h-80">
<Bar data={chartData} options={options} />
</div>
) : (
<div className="h-80 flex items-center justify-center text-gray-500">
{t('enterprise.no_performance_data')}
</div>
)}
{/* Performance ranking table */}
<div className="mt-4">
<h4 className="font-medium mb-2">{t('enterprise.ranking')}</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left">{t('enterprise.rank')}</th>
<th className="px-3 py-2 text-left">{t('enterprise.outlet')}</th>
<th className="px-3 py-2 text-right">
{t(`enterprise.metric_labels.${metric}`) || metric}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{data?.map((item, index) => (
<tr key={item.tenant_id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-3 py-2">{item.rank}</td>
<td className="px-3 py-2 font-medium">{item.anonymized_name}</td>
<td className="px-3 py-2 text-right">
{metric === 'sales' ? `${item.metric_value.toFixed(2)}` : item.metric_value}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};

View File

@@ -255,6 +255,23 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
const allUserRoles = [...globalUserRoles, ...tenantRoles];
const tenantPermissions = currentTenantAccess?.permissions || [];
// Debug logging for analytics route
if (item.path === '/app/analytics') {
console.log('🔍 [Sidebar] Checking analytics menu item:', {
path: item.path,
requiredRoles: item.requiredRoles,
requiredPermissions: item.requiredPermissions,
globalUserRoles,
tenantRoles,
allUserRoles,
tenantPermissions,
isAuthenticated,
hasAccess,
user,
currentTenantAccess
});
}
// If no specific permissions/roles required, allow access
if (!item.requiredPermissions && !item.requiredRoles) {
return true;
@@ -272,6 +289,10 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
tenantPermissions
);
if (item.path === '/app/analytics') {
console.log('🔍 [Sidebar] Analytics canAccessRoute result:', canAccessItem);
}
return canAccessItem;
});
};

View File

@@ -0,0 +1,336 @@
/*
* Distribution Map Component for Enterprise Dashboard
* Shows delivery routes and shipment status across the network
*/
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Badge } from '../ui/Badge';
import { Button } from '../ui/Button';
import {
MapPin,
Truck,
CheckCircle,
Clock,
AlertTriangle,
Package,
Eye,
Info,
Route,
Navigation,
Map as MapIcon
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface RoutePoint {
tenant_id: string;
name: string;
address: string;
latitude: number;
longitude: number;
status: 'pending' | 'in_transit' | 'delivered' | 'failed';
estimated_arrival?: string;
actual_arrival?: string;
sequence: number;
}
interface RouteData {
id: string;
route_number: string;
total_distance_km: number;
estimated_duration_minutes: number;
status: 'planned' | 'in_progress' | 'completed' | 'cancelled';
route_points: RoutePoint[];
}
interface ShipmentStatusData {
pending: number;
in_transit: number;
delivered: number;
failed: number;
}
interface DistributionMapProps {
routes?: RouteData[];
shipments?: ShipmentStatusData;
}
const DistributionMap: React.FC<DistributionMapProps> = ({
routes = [],
shipments = { pending: 0, in_transit: 0, delivered: 0, failed: 0 }
}) => {
const { t } = useTranslation('dashboard');
const [selectedRoute, setSelectedRoute] = useState<RouteData | null>(null);
const [showAllRoutes, setShowAllRoutes] = useState(true);
const renderMapVisualization = () => {
if (!routes || routes.length === 0) {
return (
<div className="h-96 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<div className="text-center">
<MapPin className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<p className="text-lg font-medium text-gray-900">{t('enterprise.no_active_routes')}</p>
<p className="text-gray-500">{t('enterprise.no_shipments_today')}</p>
</div>
</div>
);
}
// Find active routes (in_progress or planned for today)
const activeRoutes = routes.filter(route =>
route.status === 'in_progress' || route.status === 'planned'
);
if (activeRoutes.length === 0) {
return (
<div className="h-96 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<div className="text-center">
<CheckCircle className="w-12 h-12 mx-auto mb-4 text-green-400" />
<p className="text-lg font-medium text-gray-900">{t('enterprise.all_routes_completed')}</p>
<p className="text-gray-500">{t('enterprise.no_active_deliveries')}</p>
</div>
</div>
);
}
// This would normally render an interactive map, but we'll create a visual representation
return (
<div className="h-96 bg-gradient-to-b from-blue-50 to-indigo-50 rounded-lg border border-gray-200 relative">
{/* Map visualization placeholder with route indicators */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<MapIcon className="w-16 h-16 mx-auto text-blue-400 mb-2" />
<div className="text-lg font-medium text-gray-700">{t('enterprise.distribution_map')}</div>
<div className="text-sm text-gray-500 mt-1">
{activeRoutes.length} {t('enterprise.active_routes')}
</div>
</div>
</div>
{/* Route visualization elements */}
{activeRoutes.map((route, index) => (
<div key={route.id} className="absolute top-4 left-4 bg-white p-3 rounded-lg shadow-md text-sm max-w-xs">
<div className="font-medium text-gray-900 flex items-center gap-2">
<Route className="w-4 h-4 text-blue-600" />
{t('enterprise.route')} {route.route_number}
</div>
<div className="text-gray-600 mt-1">{route.status.replace('_', ' ')}</div>
<div className="text-gray-500">{route.total_distance_km.toFixed(1)} km {Math.ceil(route.estimated_duration_minutes / 60)}h</div>
</div>
))}
{/* Shipment status indicators */}
<div className="absolute bottom-4 right-4 bg-white p-3 rounded-lg shadow-md space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
<span className="text-sm">{t('enterprise.pending')}: {shipments.pending}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-400"></div>
<span className="text-sm">{t('enterprise.in_transit')}: {shipments.in_transit}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-400"></div>
<span className="text-sm">{t('enterprise.delivered')}: {shipments.delivered}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-400"></div>
<span className="text-sm">{t('enterprise.failed')}: {shipments.failed}</span>
</div>
</div>
</div>
);
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'delivered':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'in_transit':
return <Truck className="w-4 h-4 text-blue-500" />;
case 'pending':
return <Clock className="w-4 h-4 text-yellow-500" />;
case 'failed':
return <AlertTriangle className="w-4 h-4 text-red-500" />;
default:
return <Clock className="w-4 h-4 text-gray-500" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'delivered':
return 'bg-green-100 text-green-800 border-green-200';
case 'in_transit':
return 'bg-blue-100 text-blue-800 border-blue-200';
case 'pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'failed':
return 'bg-red-100 text-red-800 border-red-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<div className="space-y-4">
{/* Shipment Status Summary */}
<div className="grid grid-cols-4 gap-4 mb-4">
<div className="bg-yellow-50 p-3 rounded-lg border border-yellow-200">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-yellow-600" />
<span className="text-sm font-medium text-yellow-800">{t('enterprise.pending')}</span>
</div>
<p className="text-2xl font-bold text-yellow-900">{shipments?.pending || 0}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg border border-blue-200">
<div className="flex items-center gap-2">
<Truck className="w-5 h-5 text-blue-600" />
<span className="text-sm font-medium text-blue-800">{t('enterprise.in_transit')}</span>
</div>
<p className="text-2xl font-bold text-blue-900">{shipments?.in_transit || 0}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg border border-green-200">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-green-800">{t('enterprise.delivered')}</span>
</div>
<p className="text-2xl font-bold text-green-900">{shipments?.delivered || 0}</p>
</div>
<div className="bg-red-50 p-3 rounded-lg border border-red-200">
<div className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-600" />
<span className="text-sm font-medium text-red-800">{t('enterprise.failed')}</span>
</div>
<p className="text-2xl font-bold text-red-900">{shipments?.failed || 0}</p>
</div>
</div>
{/* Map Visualization */}
{renderMapVisualization()}
{/* Route Details Panel */}
<div className="mt-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-sm font-medium text-gray-700">{t('enterprise.active_routes')}</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setShowAllRoutes(!showAllRoutes)}
>
{showAllRoutes ? t('enterprise.hide_routes') : t('enterprise.show_routes')}
</Button>
</div>
{showAllRoutes && routes.length > 0 ? (
<div className="space-y-3">
{routes
.filter(route => route.status === 'in_progress' || route.status === 'planned')
.map(route => (
<Card key={route.id} className="hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-sm">
{t('enterprise.route')} {route.route_number}
</CardTitle>
<p className="text-xs text-gray-500 mt-1">
{route.total_distance_km.toFixed(1)} km {Math.ceil(route.estimated_duration_minutes / 60)}h
</p>
</div>
<Badge className={getStatusColor(route.status)}>
{getStatusIcon(route.status)}
<span className="ml-1 capitalize">
{t(`enterprise.route_status.${route.status}`) || route.status}
</span>
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{route.route_points.map((point, index) => (
<div key={index} className="flex items-center gap-3 text-sm">
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
point.status === 'delivered' ? 'bg-green-500 text-white' :
point.status === 'in_transit' ? 'bg-blue-500 text-white' :
point.status === 'failed' ? 'bg-red-500 text-white' :
'bg-yellow-500 text-white'
}`}>
{point.sequence}
</div>
<span className="flex-1 truncate">{point.name}</span>
<Badge variant="outline" className={getStatusColor(point.status)}>
{getStatusIcon(point.status)}
<span className="ml-1 text-xs">
{t(`enterprise.stop_status.${point.status}`) || point.status}
</span>
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
) : (
<p className="text-sm text-gray-500 text-center py-4">
{routes.length === 0 ?
t('enterprise.no_routes_planned') :
t('enterprise.no_active_routes')}
</p>
)}
</div>
{/* Selected Route Detail Panel (would be modal in real implementation) */}
{selectedRoute && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">{t('enterprise.route_details')}</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedRoute(null)}
>
×
</Button>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span>{t('enterprise.route_number')}</span>
<span className="font-medium">{selectedRoute.route_number}</span>
</div>
<div className="flex justify-between">
<span>{t('enterprise.total_distance')}</span>
<span>{selectedRoute.total_distance_km.toFixed(1)} km</span>
</div>
<div className="flex justify-between">
<span>{t('enterprise.estimated_duration')}</span>
<span>{Math.ceil(selectedRoute.estimated_duration_minutes / 60)}h {selectedRoute.estimated_duration_minutes % 60}m</span>
</div>
<div className="flex justify-between">
<span>{t('enterprise.status')}</span>
<Badge className={getStatusColor(selectedRoute.status)}>
{getStatusIcon(selectedRoute.status)}
<span className="ml-1 capitalize">
{t(`enterprise.route_status.${selectedRoute.status}`) || selectedRoute.status}
</span>
</Badge>
</div>
</div>
<Button
className="w-full mt-4"
onClick={() => setSelectedRoute(null)}
>
{t('common.close')}
</Button>
</div>
</div>
)}
</div>
);
};
export default DistributionMap;