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,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>
);
};