mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
Added Checkin duration configuration
This commit is contained in:
@@ -16,6 +16,7 @@ interface RouteStop {
|
||||
sequence: number;
|
||||
distanceKm?: number;
|
||||
distanceFromOrigin?: number;
|
||||
checkinMinutesBefore?: number;
|
||||
}
|
||||
|
||||
type Tab = 'routes' | 'coaches';
|
||||
@@ -167,10 +168,13 @@ export default function RoutesPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('routes');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingRoute, setEditingRoute] = useState<any>(null);
|
||||
const [editLoading, setEditLoading] = useState(false);
|
||||
const [stops, setStops] = useState<RouteStop[]>([]);
|
||||
const [originStationId, setOriginStationId] = useState('');
|
||||
const [destinationStationId, setDestinationStationId] = useState('');
|
||||
const [destinationDistance, setDestinationDistance] = useState<number | undefined>(undefined);
|
||||
const [originCheckinMinutes, setOriginCheckinMinutes] = useState<number | undefined>(undefined);
|
||||
const [destinationCheckinMinutes, setDestinationCheckinMinutes] = useState<number | undefined>(undefined);
|
||||
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();
|
||||
@@ -241,19 +245,22 @@ export default function RoutesPage() {
|
||||
|
||||
// distanceKm = cumulative distance from origin (fare engine uses destStop.distanceKm - originStop.distanceKm)
|
||||
const stopsArray = [
|
||||
{ stationId: originStationId, sequence: 1, distanceKm: 0 },
|
||||
{ stationId: originStationId, sequence: 1, distanceKm: 0, checkinMinutesBefore: originCheckinMinutes ?? undefined },
|
||||
...sortedMiddleStops.map((stop, idx) => ({
|
||||
stationId: stop.stationId,
|
||||
sequence: idx + 2,
|
||||
distanceKm: stop.distanceFromOrigin || 0,
|
||||
checkinMinutesBefore: stop.checkinMinutesBefore ?? undefined,
|
||||
})),
|
||||
{
|
||||
stationId: destinationStationId,
|
||||
sequence: sortedMiddleStops.length + 2,
|
||||
distanceKm: destinationDistance || 0,
|
||||
checkinMinutesBefore: destinationCheckinMinutes ?? undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const checkinRaw = formData.get('checkinMinutesBefore') as string;
|
||||
const routeData = {
|
||||
code: formData.get('code') as string,
|
||||
name: formData.get('name') as string,
|
||||
@@ -261,6 +268,7 @@ export default function RoutesPage() {
|
||||
active: !editingRoute ? (formData.get('active') !== 'false') : undefined,
|
||||
effectiveFrom: formData.get('effectiveFrom') as string,
|
||||
effectiveUntil: formData.get('effectiveUntil') as string || undefined,
|
||||
checkinMinutesBefore: checkinRaw ? parseInt(checkinRaw) : undefined,
|
||||
stops: stopsArray,
|
||||
};
|
||||
|
||||
@@ -341,6 +349,11 @@ export default function RoutesPage() {
|
||||
{ key: 'code', label: 'Route Code', sortable: true },
|
||||
{ key: 'name', label: 'Route Name', sortable: true },
|
||||
{ key: 'description', label: 'Description', render: (route: any) => route.description || 'N/A' },
|
||||
{
|
||||
key: 'checkinMinutesBefore',
|
||||
label: 'Check-in Cutoff',
|
||||
render: (route: any) => `${route.checkinMinutesBefore ?? 30} min`,
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
label: 'Status',
|
||||
@@ -363,29 +376,38 @@ export default function RoutesPage() {
|
||||
);
|
||||
});
|
||||
|
||||
const openEditModal = async (route: any) => {
|
||||
setEditLoading(true);
|
||||
try {
|
||||
const full = await routesApi.getById(route.id) as any;
|
||||
const routeStops: any[] = full?.stops || [];
|
||||
setEditingRoute(full ?? route);
|
||||
if (routeStops.length >= 2) {
|
||||
const originStop = routeStops[0];
|
||||
const destStop = routeStops[routeStops.length - 1];
|
||||
setOriginStationId(originStop.stationId);
|
||||
setOriginCheckinMinutes(originStop.checkinMinutesBefore ?? undefined);
|
||||
setDestinationStationId(destStop.stationId);
|
||||
setDestinationCheckinMinutes(destStop.checkinMinutesBefore ?? undefined);
|
||||
setDestinationDistance(destStop.distanceKm || 0);
|
||||
setStops(routeStops.slice(1, -1).map((s: any) => ({
|
||||
stationId: s.stationId,
|
||||
sequence: s.sequence,
|
||||
distanceKm: s.distanceKm,
|
||||
distanceFromOrigin: s.distanceKm || 0,
|
||||
checkinMinutesBefore: s.checkinMinutesBefore ?? undefined,
|
||||
})));
|
||||
}
|
||||
setShowModal(true);
|
||||
} finally {
|
||||
setEditLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const routeActions = [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: (route: any) => {
|
||||
setEditingRoute(route);
|
||||
const routeStops = route.stops || [];
|
||||
if (routeStops.length >= 2) {
|
||||
setOriginStationId(routeStops[0].stationId);
|
||||
setDestinationStationId(routeStops[routeStops.length - 1].stationId);
|
||||
|
||||
// distanceKm is cumulative from origin — read directly
|
||||
setDestinationDistance(routeStops[routeStops.length - 1].distanceKm || 0);
|
||||
|
||||
const middleStops = routeStops.slice(1, -1).map((stop: any) => ({
|
||||
stationId: stop.stationId,
|
||||
sequence: stop.sequence,
|
||||
distanceKm: stop.distanceKm,
|
||||
distanceFromOrigin: stop.distanceKm || 0,
|
||||
}));
|
||||
setStops(middleStops);
|
||||
}
|
||||
setShowModal(true);
|
||||
},
|
||||
onClick: openEditModal,
|
||||
variant: 'secondary' as const,
|
||||
icon: Edit,
|
||||
},
|
||||
@@ -410,10 +432,11 @@ export default function RoutesPage() {
|
||||
onClick={() => {
|
||||
setEditingRoute(null);
|
||||
setOriginStationId('');
|
||||
setOriginCheckinMinutes(undefined);
|
||||
setDestinationStationId('');
|
||||
setDestinationCheckinMinutes(undefined);
|
||||
setDestinationDistance(undefined);
|
||||
setStops([]);
|
||||
setSearch('');
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -456,7 +479,7 @@ export default function RoutesPage() {
|
||||
data={displayedRoutes}
|
||||
columns={routeColumns}
|
||||
actions={routeActions}
|
||||
loading={routesLoading}
|
||||
loading={routesLoading || editLoading}
|
||||
emptyMessage={search ? 'No routes match your search' : 'No routes found'}
|
||||
/>
|
||||
</>
|
||||
@@ -488,7 +511,9 @@ export default function RoutesPage() {
|
||||
setShowModal(false);
|
||||
setEditingRoute(null);
|
||||
setOriginStationId('');
|
||||
setOriginCheckinMinutes(undefined);
|
||||
setDestinationStationId('');
|
||||
setDestinationCheckinMinutes(undefined);
|
||||
setDestinationDistance(undefined);
|
||||
setStops([]);
|
||||
setSearch('');
|
||||
@@ -588,6 +613,23 @@ export default function RoutesPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Check-in Cutoff (minutes before departure)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="checkinMinutesBefore"
|
||||
className="input"
|
||||
defaultValue={editingRoute?.checkinMinutesBefore ?? 30}
|
||||
min={1}
|
||||
max={480}
|
||||
step={1}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Booking closes and check-in ends this many minutes before each stop's departure. Default: 30.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!editingRoute && (
|
||||
<div>
|
||||
<label className="label">Status</label>
|
||||
@@ -623,7 +665,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 intermediate stops</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -641,9 +683,18 @@ export default function RoutesPage() {
|
||||
<span className="text-muted-foreground">Select origin station above</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
0 km
|
||||
<div className="w-28 flex-shrink-0">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-sm"
|
||||
placeholder="cutoff min"
|
||||
value={originCheckinMinutes ?? ''}
|
||||
onChange={(e) => setOriginCheckinMinutes(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
min={1}
|
||||
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>
|
||||
|
||||
{stops.map((stop, index) => (
|
||||
@@ -678,7 +729,7 @@ export default function RoutesPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<div className="w-28">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-sm"
|
||||
@@ -690,6 +741,17 @@ export default function RoutesPage() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="w-28">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-sm"
|
||||
placeholder="cutoff min"
|
||||
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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeStop(index)}
|
||||
@@ -728,7 +790,20 @@ export default function RoutesPage() {
|
||||
<span className="text-muted-foreground">Select destination station above</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<div className="w-28">
|
||||
{destinationStationId && (
|
||||
<input
|
||||
type="number"
|
||||
className="input input-sm"
|
||||
placeholder="cutoff min"
|
||||
value={destinationCheckinMinutes ?? ''}
|
||||
onChange={(e) => setDestinationCheckinMinutes(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
min={1}
|
||||
title="Check-in cutoff override (minutes) for this stop"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-28">
|
||||
{destinationStationId && (
|
||||
<input
|
||||
type="number"
|
||||
@@ -754,7 +829,9 @@ export default function RoutesPage() {
|
||||
setShowModal(false);
|
||||
setEditingRoute(null);
|
||||
setOriginStationId('');
|
||||
setOriginCheckinMinutes(undefined);
|
||||
setDestinationStationId('');
|
||||
setDestinationCheckinMinutes(undefined);
|
||||
setDestinationDistance(undefined);
|
||||
setStops([]);
|
||||
setSearch('');
|
||||
|
||||
Reference in New Issue
Block a user