169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
/**
|
|
* ETA Progress Bar
|
|
* ERP Transportistas
|
|
* Sprint S7 - TASK-007
|
|
*
|
|
* Visual progress indicator for trip ETA.
|
|
*/
|
|
|
|
import { TipoEventoTracking } from '../types';
|
|
|
|
interface ETAProgressBarProps {
|
|
progreso: number;
|
|
etaOriginal: string;
|
|
etaActual: string;
|
|
ultimoEvento?: TipoEventoTracking;
|
|
}
|
|
|
|
const etapaLabels: Record<TipoEventoTracking, string> = {
|
|
[TipoEventoTracking.POSICION]: 'En ruta',
|
|
[TipoEventoTracking.SALIDA]: 'Salida',
|
|
[TipoEventoTracking.ARRIBO_ORIGEN]: 'En origen',
|
|
[TipoEventoTracking.INICIO_CARGA]: 'Cargando',
|
|
[TipoEventoTracking.FIN_CARGA]: 'Carga lista',
|
|
[TipoEventoTracking.ARRIBO_DESTINO]: 'En destino',
|
|
[TipoEventoTracking.INICIO_DESCARGA]: 'Descargando',
|
|
[TipoEventoTracking.FIN_DESCARGA]: 'Descarga lista',
|
|
[TipoEventoTracking.ENTREGA_POD]: 'Entregado',
|
|
[TipoEventoTracking.DESVIO]: 'Desvío',
|
|
[TipoEventoTracking.PARADA]: 'Detenido',
|
|
[TipoEventoTracking.INCIDENTE]: 'Incidente',
|
|
[TipoEventoTracking.GPS_POSICION]: 'En ruta',
|
|
[TipoEventoTracking.GEOCERCA_ENTRADA]: 'Entrando zona',
|
|
[TipoEventoTracking.GEOCERCA_SALIDA]: 'Saliendo zona',
|
|
};
|
|
|
|
export function ETAProgressBar({
|
|
progreso,
|
|
etaOriginal,
|
|
etaActual,
|
|
ultimoEvento,
|
|
}: ETAProgressBarProps) {
|
|
const etaOriginalDate = new Date(etaOriginal);
|
|
const etaActualDate = new Date(etaActual);
|
|
const diferenciaMinutos = Math.round(
|
|
(etaActualDate.getTime() - etaOriginalDate.getTime()) / 60000
|
|
);
|
|
|
|
const getStatusColor = () => {
|
|
if (diferenciaMinutos <= 0) return 'bg-green-500';
|
|
if (diferenciaMinutos <= 30) return 'bg-yellow-500';
|
|
return 'bg-red-500';
|
|
};
|
|
|
|
const getStatusText = () => {
|
|
if (diferenciaMinutos <= 0) return 'A tiempo';
|
|
if (diferenciaMinutos <= 30) return `+${diferenciaMinutos} min`;
|
|
const horas = Math.floor(diferenciaMinutos / 60);
|
|
const mins = diferenciaMinutos % 60;
|
|
return horas > 0 ? `+${horas}h ${mins}m` : `+${mins} min`;
|
|
};
|
|
|
|
const milestones = [
|
|
{ label: 'Salida', position: 0 },
|
|
{ label: 'Carga', position: 25 },
|
|
{ label: 'En ruta', position: 50 },
|
|
{ label: 'Descarga', position: 75 },
|
|
{ label: 'POD', position: 100 },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{/* Status row */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-gray-700">Progreso:</span>
|
|
{ultimoEvento && (
|
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
|
{etapaLabels[ultimoEvento] || ultimoEvento}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
|
diferenciaMinutos <= 0
|
|
? 'bg-green-100 text-green-700'
|
|
: diferenciaMinutos <= 30
|
|
? 'bg-yellow-100 text-yellow-700'
|
|
: 'bg-red-100 text-red-700'
|
|
}`}
|
|
>
|
|
{getStatusText()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="relative">
|
|
<div className="h-3 overflow-hidden rounded-full bg-gray-200">
|
|
<div
|
|
className={`h-full transition-all duration-500 ${getStatusColor()}`}
|
|
style={{ width: `${Math.min(progreso, 100)}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Milestone markers */}
|
|
<div className="absolute inset-0 flex items-center">
|
|
{milestones.map((milestone, index) => (
|
|
<div
|
|
key={milestone.label}
|
|
className="absolute flex flex-col items-center"
|
|
style={{
|
|
left: `${milestone.position}%`,
|
|
transform: 'translateX(-50%)',
|
|
}}
|
|
>
|
|
<div
|
|
className={`h-3 w-3 rounded-full border-2 ${
|
|
progreso >= milestone.position
|
|
? 'border-white bg-white'
|
|
: 'border-gray-400 bg-gray-200'
|
|
}`}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Milestone labels */}
|
|
<div className="relative h-4">
|
|
{milestones.map((milestone) => (
|
|
<div
|
|
key={`label-${milestone.label}`}
|
|
className="absolute text-xs text-gray-500"
|
|
style={{
|
|
left: `${milestone.position}%`,
|
|
transform: 'translateX(-50%)',
|
|
}}
|
|
>
|
|
{milestone.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* ETA comparison */}
|
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
|
<div>
|
|
<span className="text-gray-400">ETA Original: </span>
|
|
<span className="font-medium text-gray-600">
|
|
{etaOriginalDate.toLocaleString('es-MX', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
})}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-400">ETA Actual: </span>
|
|
<span className={`font-medium ${diferenciaMinutos > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{etaActualDate.toLocaleString('es-MX', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|