Added Checkin duration configuration

This commit is contained in:
Roba Boru
2026-07-17 22:12:54 +03:00
parent 0c584cce88
commit 738b7df5cf
14 changed files with 297 additions and 132 deletions

View File

@@ -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('');