Files
bakery-ia/frontend/src/pages/app/operations/distribution/DistributionPage.tsx
2025-12-05 20:07:01 +01:00

301 lines
15 KiB
TypeScript

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Truck,
Plus,
Package,
MapPin,
Calendar,
ArrowRight,
Search,
Filter,
MoreVertical,
Clock,
CheckCircle,
AlertTriangle
} from 'lucide-react';
import {
Button,
StatsGrid,
Card,
CardContent,
CardHeader,
CardTitle,
Badge,
Input
} from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useTenant } from '../../../../stores/tenant.store';
import { useDistributionOverview } from '../../../../api/hooks/useEnterpriseDashboard';
import DistributionMap from '../../../../components/maps/DistributionMap';
const DistributionPage: React.FC = () => {
const { t } = useTranslation(['operations', 'common', 'dashboard']);
const { currentTenant: tenant } = useTenant();
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [activeTab, setActiveTab] = useState<'overview' | 'routes' | 'shipments'>('overview');
// Fetch real distribution data
const { data: distributionData, isLoading } = useDistributionOverview(
tenant?.id || '',
selectedDate,
{ enabled: !!tenant?.id }
);
// Derive stats from real data
const stats = [
{
title: t('operations:stats.active_routes', 'Rutas Activas'),
value: distributionData?.route_sequences?.filter((r: any) => r.status === 'in_progress').length || 0,
variant: 'info' as const,
icon: Truck,
},
{
title: t('operations:stats.pending_deliveries', 'Entregas Pendientes'),
value: distributionData?.status_counts?.pending || 0,
variant: 'warning' as const,
icon: Package,
},
{
title: t('operations:stats.completed_deliveries', 'Entregas Completadas'),
value: distributionData?.status_counts?.delivered || 0,
variant: 'success' as const,
icon: CheckCircle,
},
{
title: t('operations:stats.total_routes', 'Total Rutas'),
value: distributionData?.route_sequences?.length || 0,
variant: 'default' as const,
icon: MapPin,
},
];
const handleNewRoute = () => {
// Navigate to create route page or open modal
console.log('New route clicked');
};
if (!tenant) return null;
// Prepare shipment status data safely
const shipmentStatus = {
pending: distributionData?.status_counts?.pending || 0,
in_transit: distributionData?.status_counts?.in_transit || 0,
delivered: distributionData?.status_counts?.delivered || 0,
failed: distributionData?.status_counts?.failed || 0,
};
return (
<div className="space-y-6">
<PageHeader
title={t('operations:distribution.title', 'Distribución y Logística')}
description={t('operations:distribution.description', 'Gestión integral de la flota de reparto y seguimiento de entregas en tiempo real')}
actions={[
{
id: "date-select",
label: selectedDate,
variant: "outline" as const,
icon: Calendar,
onClick: () => { }, // In a real app this would trigger a date picker
size: "md"
},
{
id: "add-new-route",
label: t('operations:actions.new_route', 'Nueva Ruta'),
variant: "primary" as const,
icon: Plus,
onClick: handleNewRoute,
tooltip: t('operations:tooltips.new_route', 'Crear una nueva ruta de distribución'),
size: "md"
}
]}
/>
{/* Stats Grid */}
<StatsGrid
stats={stats}
columns={4}
/>
{/* Main Content Areas */}
<div className="flex flex-col gap-6">
{/* Tabs Navigation */}
<div className="flex border-b border-gray-200">
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'overview'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('overview')}
>
Vista General
</button>
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'routes'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('routes')}
>
Listado de Rutas
</button>
<button
className={`px-4 py-2 font-medium text-sm transition-colors border-b-2 ${activeTab === 'shipments'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('shipments')}
>
Listado de Envíos
</button>
</div>
{/* Content based on Active Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Map Section */}
<Card className="overflow-hidden border-none shadow-lg">
<CardHeader className="bg-white border-b sticky top-0 z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-2 bg-blue-100 rounded-lg">
<MapPin className="w-5 h-5 text-blue-600" />
</div>
<div>
<CardTitle>{t('operations:map.title', 'Mapa de Distribución')}</CardTitle>
<p className="text-sm text-gray-500">Visualización en tiempo real de la flota</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
En Vivo
</Badge>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="p-4 bg-slate-50">
<DistributionMap
routes={distributionData?.route_sequences || []}
shipments={shipmentStatus}
/>
</div>
</CardContent>
</Card>
{/* Recent Activity / Quick List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Rutas en Progreso</CardTitle>
</CardHeader>
<CardContent>
{distributionData?.route_sequences?.filter((r: any) => r.status === 'in_progress').length > 0 ? (
<div className="space-y-4">
{distributionData.route_sequences
.filter((r: any) => r.status === 'in_progress')
.map((route: any) => (
<div key={route.id} className="flex items-center justify-between p-3 bg-white border rounded-lg shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-50 rounded-full">
<Truck className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="font-medium text-sm text-gray-900">Ruta {route.route_number}</p>
<p className="text-xs text-gray-500">{route.formatted_driver_name || 'Sin conductor asignado'}</p>
</div>
</div>
<Badge variant="info">En Ruta</Badge>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
No hay rutas en progreso actualmente.
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{activeTab === 'routes' && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Listado de Rutas</CardTitle>
<div className="flex gap-2">
<Input
placeholder="Buscar rutas..."
leftIcon={<Search className="w-4 h-4 text-gray-400" />}
className="w-64"
/>
<Button variant="outline" size="sm" leftIcon={<Filter className="w-4 h-4" />}>Filtros</Button>
</div>
</div>
</CardHeader>
<CardContent>
{(distributionData?.route_sequences?.length || 0) > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th className="px-4 py-3">Ruta</th>
<th className="px-4 py-3">Estado</th>
<th className="px-4 py-3">Distancia</th>
<th className="px-4 py-3">Duración Est.</th>
<th className="px-4 py-3">Paradas</th>
<th className="px-4 py-3 text-right">Acciones</th>
</tr>
</thead>
<tbody>
{distributionData.route_sequences.map((route: any) => (
<tr key={route.id} className="border-b hover:bg-gray-50">
<td className="px-4 py-3 font-medium">{route.route_number}</td>
<td className="px-4 py-3">
<Badge variant={
route.status === 'completed' ? 'success' :
route.status === 'in_progress' ? 'info' :
route.status === 'pending' ? 'warning' : 'default'
}>
{route.status}
</Badge>
</td>
<td className="px-4 py-3">{route.total_distance_km?.toFixed(1) || '-'} km</td>
<td className="px-4 py-3">{route.estimated_duration_minutes || '-'} min</td>
<td className="px-4 py-3">{route.route_points?.length || 0}</td>
<td className="px-4 py-3 text-right">
<Button variant="ghost" size="sm" leftIcon={<MoreVertical className="w-4 h-4" />} />
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed">
<p className="text-gray-500">No se encontraron rutas para esta fecha.</p>
</div>
)}
</CardContent>
</Card>
)}
{/* Similar structure for Shipments tab, simplified for now */}
{activeTab === 'shipments' && (
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed">
<Package className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<h3 className="text-lg font-medium text-gray-900">Gestión de Envíos</h3>
<p className="text-gray-500">Funcionalidad de listado detallado de envíos próximamente.</p>
</div>
)}
</div>
</div>
);
};
export default DistributionPage;