Skip to main content

trips

id: trips title: Trips sidebar_label: Trips

Trips

Trip State Machine

scheduled → in_progress → paused → resumed → in_progress → completed
↘ (cancelled at any point)
StatusDescriptionAvailable Actions
scheduledTrip not yet startedStart
in_progressTrip underwayPause, End
pausedTemporarily haltedResume, End
completedAll stops reached
cancelledCancelled before completion

Trip Types

TypeDescription
pickupMorning — collecting students from stops
dropoffAfternoon — dropping students at stops

Get Trip Details

GET /driver/bus-tracking/trips/{tripId}
{
"data": {
"trip": {
"id": "uuid",
"busId": "uuid",
"scheduleId": "uuid",
"tripType": "pickup",
"status": "in_progress",
"startedAt": "2024-04-01T07:30:00Z",
"totalDistanceKm": 5.2,
"date": "2024-04-01"
},
"stops": [
{
"id": "uuid",
"stopName": "Stop 1 — Phoenix Marketcity",
"stopOrder": 1,
"studentCount": 3,
"boardedCount": 2,
"absentCount": 1,
"status": "departed",
"arrivedAt": "2024-04-01T07:45:00Z",
"departedAt": "2024-04-01T07:52:00Z",
"latitude": 12.9698,
"longitude": 77.6265
},
{
"id": "uuid",
"stopName": "Stop 2 — Whitefield",
"stopOrder": 2,
"studentCount": 5,
"boardedCount": 0,
"absentCount": 0,
"status": "pending",
"latitude": 12.9698,
"longitude": 77.6265
}
]
}
}

Trip Actions

Start Trip

POST /driver/bus-tracking/trips/{tripId}/start

Pause Trip

POST /driver/bus-tracking/trips/{tripId}/pause

Resume Trip

POST /driver/bus-tracking/trips/{tripId}/resume

End Trip

POST /driver/bus-tracking/trips/{tripId}/end

Reorder Stops

PUT /driver/bus-tracking/bus-stops/reorder
{
"data": {
"tripId": "uuid",
"stopIds": ["uuid", "uuid", "uuid"]
}
}

UI — Trip Detail View

class TripDetailView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.watch<TripDetailViewModel>();

return Scaffold(
appBar: AppBar(
title: Text('${vm.tripType} Trip'),
actions: [
if (vm.trip.status == 'scheduled')
TextButton(
onPressed: () => vm.startTrip(),
child: const Text('START'),
),
if (vm.trip.status == 'in_progress')
TextButton(
onPressed: () => vm.pauseTrip(),
child: const Text('PAUSE'),
),
if (vm.trip.status == 'paused')
TextButton(
onPressed: () => vm.resumeTrip(),
child: const Text('RESUME'),
),
if (vm.trip.status == 'in_progress' || vm.trip.status == 'paused')
TextButton(
onPressed: () => vm.endTrip(),
child: const Text('END', style: TextStyle(color: Colors.red)),
),
],
),
body: ListView.builder(
itemCount: vm.stops.length,
itemBuilder: (_, i) {
final stop = vm.stops[i];
return StopCard(
stop: stop,
onArrived: () => vm.markStopArrived(stop.id),
onDeparted: () => vm.markStopDeparted(stop.id),
onViewStudents: () => AppNavigator.push(
context,
'/students/${vm.tripId}',
arguments: {'stopId': stop.id},
),
);
},
),
);
}
}

UI — Stop Card

class StopCard extends StatelessWidget {
final StopModel stop;
final VoidCallback onArrived;
final VoidCallback onDeparted;
final VoidCallback onViewStudents;

@override
Widget build(BuildContext context) {
final color = {
'pending': Colors.grey,
'arrived': Colors.orange,
'departed': Colors.green,
}[stop.status]!;

return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: color.shade100,
child: stop.status == 'pending'
? Text('${stop.stopOrder}')
: Icon(Icons.check, color: color),
),
title: Text(stop.stopName),
subtitle: Text(
'${stop.boardedCount}/${stop.studentCount} boarded • '
'${stop.absentCount} absent',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (stop.status == 'pending')
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: onArrived,
),
if (stop.status == 'arrived')
IconButton(
icon: const Icon(Icons.logout),
onPressed: onDeparted,
),
IconButton(
icon: const Icon(Icons.people),
onPressed: onViewStudents,
),
],
),
),
);
}
}