Added stops departure and arrival datetime

This commit is contained in:
Roba Boru
2026-07-18 21:51:54 +03:00
parent d7a66cfac5
commit c1858e76d4
16 changed files with 1026 additions and 94 deletions

View File

@@ -10,6 +10,14 @@ import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import { routesApi } from '@/lib/api/routes';
import { stationsApi, fleetApi, routeCoachTemplatesApi } from '@/lib/api';
import DateTimePicker from '@/components/ui/DateTimePicker';
// EAT ↔ UTC helpers (same as schedules page)
const EAT_MS = 3 * 60 * 60 * 1000;
const isoToEAT = (iso: string): string =>
new Date(new Date(iso).getTime() + EAT_MS).toISOString().slice(0, 16);
const eatToISO = (local: string): string =>
new Date(new Date(local + ':00Z').getTime() - EAT_MS).toISOString();
interface RouteStop {
stationId: string;
@@ -17,6 +25,8 @@ interface RouteStop {
distanceKm?: number;
distanceFromOrigin?: number;
checkinMinutesBefore?: number;
plannedArrivalTime?: string;
plannedDepartureTime?: string;
}
type Tab = 'routes' | 'coaches';
@@ -175,6 +185,8 @@ export default function RoutesPage() {
const [destinationDistance, setDestinationDistance] = useState<number | undefined>(undefined);
const [originCheckinMinutes, setOriginCheckinMinutes] = useState<number | undefined>(undefined);
const [destinationCheckinMinutes, setDestinationCheckinMinutes] = useState<number | undefined>(undefined);
const [originDepartureTime, setOriginDepartureTime] = useState<string>('');
const [destinationArrivalTime, setDestinationArrivalTime] = useState<string>('');
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; route: any | null; error?: string; cascade?: boolean; cascadeChecked?: boolean }>({ isOpen: false, route: null });
const [search, setSearch] = useState('');
const queryClient = useQueryClient();
@@ -245,18 +257,27 @@ export default function RoutesPage() {
// distanceKm = cumulative distance from origin (fare engine uses destStop.distanceKm - originStop.distanceKm)
const stopsArray = [
{ stationId: originStationId, sequence: 1, distanceKm: 0, checkinMinutesBefore: originCheckinMinutes ?? undefined },
{
stationId: originStationId,
sequence: 1,
distanceKm: 0,
checkinMinutesBefore: originCheckinMinutes ?? undefined,
plannedDepartureTime: originDepartureTime ? eatToISO(originDepartureTime) : undefined,
},
...sortedMiddleStops.map((stop, idx) => ({
stationId: stop.stationId,
sequence: idx + 2,
distanceKm: stop.distanceFromOrigin || 0,
checkinMinutesBefore: stop.checkinMinutesBefore ?? undefined,
plannedArrivalTime: stop.plannedArrivalTime ? eatToISO(stop.plannedArrivalTime) : undefined,
plannedDepartureTime: stop.plannedDepartureTime ? eatToISO(stop.plannedDepartureTime) : undefined,
})),
{
stationId: destinationStationId,
sequence: sortedMiddleStops.length + 2,
distanceKm: destinationDistance || 0,
checkinMinutesBefore: destinationCheckinMinutes ?? undefined,
plannedArrivalTime: destinationArrivalTime ? eatToISO(destinationArrivalTime) : undefined,
},
];
@@ -387,8 +408,10 @@ export default function RoutesPage() {
const destStop = routeStops[routeStops.length - 1];
setOriginStationId(originStop.stationId);
setOriginCheckinMinutes(originStop.checkinMinutesBefore ?? undefined);
setOriginDepartureTime(originStop.plannedDepartureTime ? isoToEAT(originStop.plannedDepartureTime) : '');
setDestinationStationId(destStop.stationId);
setDestinationCheckinMinutes(destStop.checkinMinutesBefore ?? undefined);
setDestinationArrivalTime(destStop.plannedArrivalTime ? isoToEAT(destStop.plannedArrivalTime) : '');
setDestinationDistance(destStop.distanceKm || 0);
setStops(routeStops.slice(1, -1).map((s: any) => ({
stationId: s.stationId,
@@ -396,6 +419,8 @@ export default function RoutesPage() {
distanceKm: s.distanceKm,
distanceFromOrigin: s.distanceKm || 0,
checkinMinutesBefore: s.checkinMinutesBefore ?? undefined,
plannedArrivalTime: s.plannedArrivalTime ? isoToEAT(s.plannedArrivalTime) : '',
plannedDepartureTime: s.plannedDepartureTime ? isoToEAT(s.plannedDepartureTime) : '',
})));
}
setShowModal(true);
@@ -433,8 +458,10 @@ export default function RoutesPage() {
setEditingRoute(null);
setOriginStationId('');
setOriginCheckinMinutes(undefined);
setOriginDepartureTime('');
setDestinationStationId('');
setDestinationCheckinMinutes(undefined);
setDestinationArrivalTime('');
setDestinationDistance(undefined);
setStops([]);
setShowModal(true);
@@ -519,7 +546,7 @@ export default function RoutesPage() {
setSearch('');
}}
title={`${editingRoute ? 'Edit' : 'Add'} Route`}
size="lg"
size="xl"
>
<form onSubmit={handleSubmit} className="space-y-4 max-h-[calc(100vh-200px)] overflow-y-auto">
{editingRoute && (
@@ -665,7 +692,7 @@ export default function RoutesPage() {
<div className="border-t pt-4">
<div className="flex items-center justify-between mb-3">
<label className="label mb-0">Route Stops</label>
<span className="text-xs text-muted-foreground">Drag to rearrange · <span className="font-medium">Cutoff min</span> overrides route check-in window per stop (leave blank to inherit)</span>
<span className="text-xs text-muted-foreground">Drag to rearrange · <span className="font-medium">Cutoff</span> overrides check-in · <span className="font-medium">Arr/Dep time</span> sets default times (auto-filled on schedule creation)</span>
</div>
<div className="space-y-2">
@@ -683,7 +710,7 @@ export default function RoutesPage() {
<span className="text-muted-foreground">Select origin station above</span>
)}
</div>
<div className="w-28 flex-shrink-0">
<div className="w-24 flex-shrink-0">
<input
type="number"
className="input input-sm"
@@ -694,7 +721,15 @@ export default function RoutesPage() {
title="Check-in cutoff override (minutes) for this stop"
/>
</div>
<div className="text-sm text-muted-foreground w-12 text-right flex-shrink-0">0 km</div>
<div className="w-44 flex-shrink-0">
<DateTimePicker
value={originDepartureTime}
onChange={setOriginDepartureTime}
placeholder="Dep time"
label="Planned Departure"
/>
</div>
<div className="text-sm text-muted-foreground w-10 text-right flex-shrink-0">0 km</div>
</div>
{stops.map((stop, index) => (
@@ -729,7 +764,7 @@ export default function RoutesPage() {
))}
</select>
</div>
<div className="w-28">
<div className="w-20">
<input
type="number"
className="input input-sm"
@@ -741,17 +776,33 @@ export default function RoutesPage() {
required
/>
</div>
<div className="w-28">
<div className="w-20">
<input
type="number"
className="input input-sm"
placeholder="cutoff min"
placeholder="cutoff"
value={stop.checkinMinutesBefore ?? ''}
onChange={(e) => updateStop(index, 'checkinMinutesBefore', e.target.value ? parseInt(e.target.value) : undefined)}
min={1}
title="Check-in cutoff override (minutes) for this stop"
/>
</div>
<div className="w-44">
<DateTimePicker
value={stop.plannedDepartureTime ?? ''}
onChange={(v) => updateStop(index, 'plannedDepartureTime', v)}
placeholder="Dep time"
label="Planned Departure"
/>
</div>
<div className="w-44">
<DateTimePicker
value={stop.plannedArrivalTime ?? ''}
onChange={(v) => updateStop(index, 'plannedArrivalTime', v)}
placeholder="Arr time"
label="Planned Arrival"
/>
</div>
<button
type="button"
onClick={() => removeStop(index)}
@@ -790,7 +841,7 @@ export default function RoutesPage() {
<span className="text-muted-foreground">Select destination station above</span>
)}
</div>
<div className="w-28">
<div className="w-24">
{destinationStationId && (
<input
type="number"
@@ -803,7 +854,18 @@ export default function RoutesPage() {
/>
)}
</div>
<div className="w-28">
<div className="w-44" />
<div className="w-44">
{destinationStationId && (
<DateTimePicker
value={destinationArrivalTime}
onChange={setDestinationArrivalTime}
placeholder="Arr time"
label="Planned Arrival"
/>
)}
</div>
<div className="w-24">
{destinationStationId && (
<input
type="number"

View File

@@ -7,10 +7,29 @@ import DataTable from '@/components/ui/DataTable';
import ActionButton from '@/components/ui/ActionButton';
import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import DateTimePicker from '@/components/ui/DateTimePicker';
import { apiClient } from '@/lib/api-client';
import { routeCoachTemplatesApi } from '@/lib/api';
import { formatDateTime } from '@/lib/utils';
// EAT is UTC+3. Convert without depending on the browser's own timezone.
const EAT_MS = 3 * 60 * 60 * 1000;
// UTC ISO string → EAT "YYYY-MM-DDTHH:mm" for DateTimePicker display
const isoToEAT = (iso: string): string =>
new Date(new Date(iso).getTime() + EAT_MS).toISOString().slice(0, 16);
// EAT "YYYY-MM-DDTHH:mm" → UTC ISO string for API submission
const eatToISO = (local: string): string =>
new Date(new Date(local + ':00Z').getTime() - EAT_MS).toISOString();
// Extract HH:mm in EAT from a UTC ISO datetime (e.g. route stop planned time)
const isoToEATTimePart = (iso: string): string | null => {
if (!iso) return null;
const eatMs = new Date(iso).getTime() + EAT_MS;
const msIntoDay = eatMs % (24 * 60 * 60 * 1000);
const h = Math.floor(msIntoDay / 3600000);
const m = Math.floor((msIntoDay % 3600000) / 60000);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
};
interface Schedule {
id: string;
trainId: string;
@@ -24,6 +43,13 @@ interface Schedule {
destinationStation?: { id: string; name: string };
coachAssignments?: Array<{ coachId: string; positionNumber: number; coach?: { id: string; number: string } }>;
isPackageOnly?: boolean;
stopTimes?: Array<{
sequence: number;
stationId: string;
plannedDepartureAt: string | null;
plannedArrivalAt: string | null;
station?: { name: string };
}>;
}
interface Train {
@@ -72,6 +98,8 @@ export default function SchedulesPage() {
const [addForm, setAddForm] = useState({ trainId: '', routeId: '', departureAt: '', arrivalAt: '' });
const [addCoachRows, setAddCoachRows] = useState<{ coachId: string; positionNumber: number }[]>([]);
const [addStopTimes, setAddStopTimes] = useState<{ sequence: number; stationName: string; plannedArrivalAt: string; plannedDepartureAt: string }[]>([]);
const [editStopTimes, setEditStopTimes] = useState<{ sequence: number; stationName: string; plannedArrivalAt: string; plannedDepartureAt: string }[]>([]);
const { data: singleRouteTemplate, isLoading: singleTemplateLoading } = useQuery({
queryKey: ['route-coaches', addForm.routeId],
@@ -79,12 +107,42 @@ export default function SchedulesPage() {
enabled: !!addForm.routeId,
});
const { data: addRouteDetail } = useQuery({
queryKey: ['route-detail', addForm.routeId],
queryFn: () => apiClient.get<any>(`/routes/${addForm.routeId}`),
enabled: !!addForm.routeId,
});
const { data: editRouteDetail } = useQuery({
queryKey: ['route-detail', editingSchedule?.routeId],
queryFn: () => apiClient.get<any>(`/routes/${editingSchedule!.routeId}`),
enabled: !!editingSchedule?.routeId,
});
useEffect(() => {
if (!addForm.routeId) { setAddCoachRows([]); return; }
const rows: any[] = Array.isArray(singleRouteTemplate) ? singleRouteTemplate : (singleRouteTemplate as any)?.coaches ?? [];
setAddCoachRows(rows.length ? rows.map((r: any) => ({ coachId: r.coachId ?? r.coach?.id, positionNumber: r.positionNumber })) : []);
}, [singleRouteTemplate, addForm.routeId]);
useEffect(() => {
const stops: any[] = (addRouteDetail as any)?.stops ?? [];
if (!stops.length) { setAddStopTimes([]); return; }
const eatDateStr = addForm.departureAt ? addForm.departureAt.slice(0, 10) : null;
setAddStopTimes(stops.map((s: any) => {
const arrTimePart = s.plannedArrivalTime ? isoToEATTimePart(s.plannedArrivalTime) : null;
const depTimePart = s.plannedDepartureTime ? isoToEATTimePart(s.plannedDepartureTime) : null;
return {
sequence: s.sequence,
stationName: s.station?.name ?? `Stop ${s.sequence}`,
plannedArrivalAt: eatDateStr && arrTimePart ? `${eatDateStr}T${arrTimePart}` : '',
plannedDepartureAt: eatDateStr && depTimePart ? `${eatDateStr}T${depTimePart}` : '',
};
}));
}, [addRouteDetail, addForm.departureAt]);
// Fetch route coach template when route changes
const { data: routeTemplate, isLoading: templateLoading } = useQuery({
queryKey: ['route-coaches', bulkForm.routeId],
@@ -110,6 +168,24 @@ export default function SchedulesPage() {
isPackageOnly: false,
});
useEffect(() => {
const stops: any[] = (editRouteDetail as any)?.stops ?? [];
if (!stops.length || !editingSchedule) return;
const hasRouteTimes = stops.some((s: any) => s.plannedArrivalTime || s.plannedDepartureTime);
if (!hasRouteTimes) return;
const eatDateStr = editForm.departureAt ? editForm.departureAt.slice(0, 10) : null;
setEditStopTimes(stops.map((s: any) => {
const arrTimePart = s.plannedArrivalTime ? isoToEATTimePart(s.plannedArrivalTime) : null;
const depTimePart = s.plannedDepartureTime ? isoToEATTimePart(s.plannedDepartureTime) : null;
return {
sequence: s.sequence,
stationName: s.station?.name ?? `Stop ${s.sequence}`,
plannedArrivalAt: eatDateStr && arrTimePart ? `${eatDateStr}T${arrTimePart}` : '',
plannedDepartureAt: eatDateStr && depTimePart ? `${eatDateStr}T${depTimePart}` : '',
};
}));
}, [editRouteDetail, editingSchedule?.id, editForm.departureAt]);
const [filters, setFilters] = useState({
search: '',
trainId: '',
@@ -175,6 +251,7 @@ export default function SchedulesPage() {
setShowAddModal(false);
setAddForm({ trainId: '', routeId: '', departureAt: '', arrivalAt: '' });
setAddCoachRows([]);
setAddStopTimes([]);
setError(null);
},
onError: (err: any) => {
@@ -189,6 +266,7 @@ export default function SchedulesPage() {
queryClient.invalidateQueries({ queryKey: ['schedules'] });
setShowEditModal(false);
setEditingSchedule(null);
setEditStopTimes([]);
setError(null);
},
onError: (err: any) => {
@@ -255,15 +333,30 @@ export default function SchedulesPage() {
const handleAddSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
const dep = new Date(addForm.departureAt);
const arr = new Date(addForm.arrivalAt);
if (arr <= dep) { setError('Arrival must be after departure'); return; }
if (!addForm.departureAt || !addForm.arrivalAt) {
setError('Please select departure and arrival date & time');
return;
}
if (new Date(addForm.arrivalAt + ':00Z') <= new Date(addForm.departureAt + ':00Z')) {
setError('Arrival must be after departure'); return;
}
const filledStops = addStopTimes.filter(s => s.plannedDepartureAt || s.plannedArrivalAt);
const plannedTimes = filledStops.length === addStopTimes.length && addStopTimes.length > 0
? addStopTimes.map(s => ({
sequence: s.sequence,
...(s.plannedArrivalAt ? { plannedArrivalAt: eatToISO(s.plannedArrivalAt) } : {}),
...(s.plannedDepartureAt ? { plannedDepartureAt: eatToISO(s.plannedDepartureAt) } : {}),
}))
: undefined;
const validCoaches = addCoachRows.filter((r) => r.coachId);
await createScheduleMutation.mutateAsync({
trainId: addForm.trainId,
routeId: addForm.routeId,
departureAt: dep.toISOString(),
arrivalAt: arr.toISOString(),
departureAt: eatToISO(addForm.departureAt),
arrivalAt: eatToISO(addForm.arrivalAt),
...(plannedTimes ? { plannedTimes } : {}),
...(validCoaches.length > 0 && { coachIds: validCoaches.map((r) => r.coachId) }),
});
};
@@ -274,24 +367,35 @@ export default function SchedulesPage() {
if (!editingSchedule) return;
// Convert local datetime-local values to UTC for API
const depLocal = new Date(editForm.departureAt);
const arrLocal = new Date(editForm.arrivalAt);
if (arrLocal <= depLocal) {
if (!editForm.departureAt || !editForm.arrivalAt) {
setError('Please select departure and arrival date & time');
return;
}
if (new Date(editForm.arrivalAt + ':00Z') <= new Date(editForm.departureAt + ':00Z')) {
setError('Arrival time must be after departure time');
return;
}
const filledEditStops = editStopTimes.filter(s => s.plannedDepartureAt || s.plannedArrivalAt);
const editPlannedTimes = filledEditStops.length === editStopTimes.length && editStopTimes.length > 0
? editStopTimes.map(s => ({
sequence: s.sequence,
...(s.plannedArrivalAt ? { plannedArrivalAt: eatToISO(s.plannedArrivalAt) } : {}),
...(s.plannedDepartureAt ? { plannedDepartureAt: eatToISO(s.plannedDepartureAt) } : {}),
}))
: undefined;
const payload: any = {
departureAt: depLocal.toISOString(),
arrivalAt: arrLocal.toISOString(),
departureAt: eatToISO(editForm.departureAt),
arrivalAt: eatToISO(editForm.arrivalAt),
status: editForm.status,
isPackageOnly: editForm.isPackageOnly,
coaches: editForm.coachIds.map((coachId: string, idx: number) => ({
coachId,
positionNumber: idx + 1,
})),
...(editPlannedTimes ? { plannedTimes: editPlannedTimes } : {}),
};
await updateScheduleMutation.mutateAsync({
@@ -327,26 +431,26 @@ export default function SchedulesPage() {
const handleEditClick = (schedule: Schedule) => {
setEditingSchedule(schedule);
// Convert UTC dates to local time for datetime-local input
// datetime-local expects local time (no timezone info)
const dep = new Date(schedule.departureAt);
const arr = new Date(schedule.arrivalAt);
// Convert to local time by adding the timezone offset
const depLocal = new Date(dep.getTime() + dep.getTimezoneOffset() * 60000);
const arrLocal = new Date(arr.getTime() + arr.getTimezoneOffset() * 60000);
// Format for datetime-local input (YYYY-MM-DDTHH:mm)
const depStr = depLocal.toISOString().slice(0, 16);
const arrStr = arrLocal.toISOString().slice(0, 16);
setEditForm({
departureAt: depStr,
arrivalAt: arrStr,
departureAt: isoToEAT(schedule.departureAt),
arrivalAt: isoToEAT(schedule.arrivalAt),
status: schedule.status,
coachIds: schedule.coachAssignments?.map((ca: any) => ca.coachId) || [],
isPackageOnly: schedule.isPackageOnly ?? false,
});
if (schedule.stopTimes && schedule.stopTimes.length > 0) {
const toDatetimeLocal = (iso: string | null) => iso ? isoToEAT(iso) : '';
setEditStopTimes(schedule.stopTimes.map(st => ({
sequence: st.sequence,
stationName: st.station?.name ?? `Stop ${st.sequence}`,
plannedArrivalAt: toDatetimeLocal(st.plannedArrivalAt),
plannedDepartureAt: toDatetimeLocal(st.plannedDepartureAt),
})));
} else {
setEditStopTimes([]);
}
setError(null);
setShowEditModal(true);
};
@@ -672,7 +776,7 @@ export default function SchedulesPage() {
<Modal
isOpen={showAddModal}
onClose={() => { setShowAddModal(false); setAddForm({ trainId: '', routeId: '', departureAt: '', arrivalAt: '' }); setAddCoachRows([]); setError(null); }}
onClose={() => { setShowAddModal(false); setAddForm({ trainId: '', routeId: '', departureAt: '', arrivalAt: '' }); setAddCoachRows([]); setAddStopTimes([]); setError(null); }}
title="Add Schedule"
size="lg"
>
@@ -699,14 +803,114 @@ export default function SchedulesPage() {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Departure *</label>
<input type="datetime-local" className="input" value={addForm.departureAt} onChange={(e) => setAddForm({ ...addForm, departureAt: e.target.value })} required />
<DateTimePicker
value={addForm.departureAt}
onChange={(v) => setAddForm({ ...addForm, departureAt: v })}
placeholder="Select departure"
/>
</div>
<div>
<label className="label">Arrival *</label>
<input type="datetime-local" className="input" value={addForm.arrivalAt} onChange={(e) => setAddForm({ ...addForm, arrivalAt: e.target.value })} required />
<DateTimePicker
value={addForm.arrivalAt}
onChange={(v) => setAddForm({ ...addForm, arrivalAt: v })}
placeholder="Select arrival"
/>
</div>
</div>
{addStopTimes.length > 0 && (
<div className="border-t pt-4">
<div className="flex items-center justify-between mb-3">
<div>
<label className="label mb-0">Stop Times</label>
<p className="text-xs text-muted-foreground mt-0.5">
Set planned times for each stop. Leave all blank to auto-generate from distance.
</p>
</div>
<button
type="button"
className="text-xs text-primary underline"
onClick={() => {
const dep = addForm.departureAt ? new Date(addForm.departureAt + ':00Z') : null;
const arr = addForm.arrivalAt ? new Date(addForm.arrivalAt + ':00Z') : null;
if (!dep || !arr || arr <= dep) return;
const stops = (addRouteDetail as any)?.stops ?? [];
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = stops[stops.length - 1]?.distanceKm || 1;
setAddStopTimes(addStopTimes.map((s, i) => {
const stop = stops.find((st: any) => st.sequence === s.sequence);
const dist = stop?.distanceKm ?? 0;
const t = new Date(dep.getTime() + (dist / totalDistance) * totalDuration);
const fmt = t.toISOString().slice(0, 16);
return {
...s,
plannedArrivalAt: i === 0 ? '' : fmt,
plannedDepartureAt: i === addStopTimes.length - 1 ? '' : fmt,
};
}));
}}
>
Auto-fill from departure/arrival
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-muted-foreground border-b">
<th className="pb-2 pr-3 font-medium">#</th>
<th className="pb-2 pr-3 font-medium">Station</th>
<th className="pb-2 pr-3 font-medium">Planned Arrival</th>
<th className="pb-2 font-medium">Planned Departure</th>
</tr>
</thead>
<tbody className="divide-y">
{addStopTimes.map((stop, i) => {
const isFirst = i === 0;
const isLast = i === addStopTimes.length - 1;
return (
<tr key={stop.sequence}>
<td className="py-2 pr-3 text-muted-foreground">{stop.sequence}</td>
<td className="py-2 pr-3 font-medium whitespace-nowrap">{stop.stationName}</td>
<td className="py-2 pr-3 min-w-[200px]">
{isFirst ? (
<span className="text-xs text-muted-foreground italic"></span>
) : (
<DateTimePicker
value={stop.plannedArrivalAt}
onChange={(v) => {
const updated = [...addStopTimes];
updated[i] = { ...updated[i], plannedArrivalAt: v };
setAddStopTimes(updated);
}}
placeholder="Pick arrival"
/>
)}
</td>
<td className="py-2 min-w-[200px]">
{isLast ? (
<span className="text-xs text-muted-foreground italic"></span>
) : (
<DateTimePicker
value={stop.plannedDepartureAt}
onChange={(v) => {
const updated = [...addStopTimes];
updated[i] = { ...updated[i], plannedDepartureAt: v };
setAddStopTimes(updated);
}}
placeholder="Pick departure"
/>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
<div className="border-t pt-4">
<div className="flex items-center justify-between mb-2">
<label className="label mb-0">Coaches</label>
@@ -1006,6 +1210,7 @@ export default function SchedulesPage() {
onClose={() => {
setShowEditModal(false);
setEditingSchedule(null);
setEditStopTimes([]);
setError(null);
}}
title={`Edit Schedule: ${editingSchedule?.originStation?.name ?? ''}${editingSchedule?.destinationStation?.name ?? ''}`}
@@ -1022,23 +1227,19 @@ export default function SchedulesPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="label">Departure Date & Time *</label>
<input
type="datetime-local"
<DateTimePicker
value={editForm.departureAt}
onChange={(e) => setEditForm({ ...editForm, departureAt: e.target.value })}
className="input"
required
onChange={(v) => setEditForm({ ...editForm, departureAt: v })}
placeholder="Select departure"
/>
</div>
<div>
<label className="label">Arrival Date & Time *</label>
<input
type="datetime-local"
<DateTimePicker
value={editForm.arrivalAt}
onChange={(e) => setEditForm({ ...editForm, arrivalAt: e.target.value })}
className="input"
required
onChange={(v) => setEditForm({ ...editForm, arrivalAt: v })}
placeholder="Select arrival"
/>
</div>
</div>
@@ -1072,6 +1273,101 @@ export default function SchedulesPage() {
</label>
</div>
{editStopTimes.length > 0 && (
<div className="border-t pt-4">
<div className="flex items-center justify-between mb-3">
<div>
<label className="label mb-0">Stop Times</label>
<p className="text-xs text-muted-foreground mt-0.5">
Edit planned times for each stop. All stops must be filled to update.
</p>
</div>
<button
type="button"
className="text-xs text-primary underline"
onClick={() => {
const dep = editForm.departureAt ? new Date(editForm.departureAt + ':00Z') : null;
const arr = editForm.arrivalAt ? new Date(editForm.arrivalAt + ':00Z') : null;
if (!dep || !arr || arr <= dep) return;
const stops = (editRouteDetail as any)?.stops ?? [];
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = stops[stops.length - 1]?.distanceKm || 1;
setEditStopTimes(editStopTimes.map((s, i) => {
const stop = stops.find((st: any) => st.sequence === s.sequence);
const dist = stops.length > 0 ? (stop?.distanceKm ?? 0) : 0;
const progress = stops.length > 0 && totalDistance > 0
? dist / totalDistance
: i / Math.max(editStopTimes.length - 1, 1);
const t = new Date(dep.getTime() + totalDuration * progress);
const fmt = t.toISOString().slice(0, 16);
return {
...s,
plannedArrivalAt: i === 0 ? '' : fmt,
plannedDepartureAt: i === editStopTimes.length - 1 ? '' : fmt,
};
}));
}}
>
Auto-fill from departure/arrival
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-muted-foreground border-b">
<th className="pb-2 pr-3 font-medium">#</th>
<th className="pb-2 pr-3 font-medium">Station</th>
<th className="pb-2 pr-3 font-medium">Planned Arrival</th>
<th className="pb-2 font-medium">Planned Departure</th>
</tr>
</thead>
<tbody className="divide-y">
{editStopTimes.map((stop, i) => {
const isFirst = i === 0;
const isLast = i === editStopTimes.length - 1;
return (
<tr key={stop.sequence}>
<td className="py-2 pr-3 text-muted-foreground">{stop.sequence}</td>
<td className="py-2 pr-3 font-medium whitespace-nowrap">{stop.stationName}</td>
<td className="py-2 pr-3 min-w-[200px]">
{isFirst ? (
<span className="text-xs text-muted-foreground italic"></span>
) : (
<DateTimePicker
value={stop.plannedArrivalAt}
onChange={(v) => {
const updated = [...editStopTimes];
updated[i] = { ...updated[i], plannedArrivalAt: v };
setEditStopTimes(updated);
}}
placeholder="Pick arrival"
/>
)}
</td>
<td className="py-2 min-w-[200px]">
{isLast ? (
<span className="text-xs text-muted-foreground italic"></span>
) : (
<DateTimePicker
value={stop.plannedDepartureAt}
onChange={(v) => {
const updated = [...editStopTimes];
updated[i] = { ...updated[i], plannedDepartureAt: v };
setEditStopTimes(updated);
}}
placeholder="Pick departure"
/>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
<div>
<div className="flex items-center justify-between mb-2">
<label className="label">Coaches (Optional)</label>
@@ -1129,6 +1425,7 @@ export default function SchedulesPage() {
onClick={() => {
setShowEditModal(false);
setEditingSchedule(null);
setEditStopTimes([]);
setError(null);
}}
>