feat: add weekly schedule shifts and bulk departures
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m1s
Build and Push Docker Images / build-employee-api (push) Successful in 59s
Build and Push Docker Images / build-transit-api (push) Successful in 56s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m11s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m37s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s

This commit is contained in:
Sven laptop
2026-09-18 18:00:51 +02:00
parent 9d1a37e248
commit 3f0a02aa04
12 changed files with 4818 additions and 1809 deletions
+23 -2
View File
@@ -1,3 +1,18 @@
## Weekly schedules and shifts
The schedule manager uses the weekly template model. Administrators create a
departure for a bus line in one of three categories: weekdays, Saturday, or
Sunday. The UI creates the corresponding `day_of_week` rows and assigns drivers
through `transit.shifts`, rather than storing a driver on `transit.schedules`.
The active UI uses these transit API endpoints:
- `GET /admin/weekly-schedules`
- `GET /admin/shift-drivers`
- `POST /admin/weekly-schedules`
- `PUT /admin/weekly-schedules/:scheduleId/shift`
- `DELETE /admin/weekly-schedules/:scheduleId`
# Transit Admin Control Center
Modern React + Vite web application for comprehensive transit system administration.
@@ -15,6 +30,7 @@ Modern React + Vite web application for comprehensive transit system administrat
## Local Development
### Prerequisites
- Node.js 18+
- npm or yarn
@@ -87,7 +103,7 @@ http://localhost:5174
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| ------------------ | -------------------------------------------------------- | --------------------- |
| `VITE_AUTH_API` | Auth API URL (same-origin path recommended in Docker) | http://localhost:5000 |
| `VITE_TRANSIT_API` | Transit API URL (same-origin path recommended in Docker) | http://localhost:5001 |
@@ -98,6 +114,7 @@ When running in Docker, environment variables are injected at runtime via `docke
## Architecture
### Technology Stack
- **Frontend**: React 18.2 + Vite 5.0
- **HTTP Client**: Axios 1.4
- **Server**: Nginx (Alpine Linux)
@@ -116,6 +133,7 @@ When running in Docker, environment variables are injected at runtime via `docke
### API Integration
The app communicates with:
- **Auth API** (/login, /me)
- **Transit API** (/admin/stops, /admin/bus-lines, /admin/fleet/buses)
- **Error Log API** (/error_log)
@@ -148,16 +166,19 @@ Users must have a `userCode` in their JWT and, for admin access, the `isAdmin` f
## Troubleshooting
### Issue: "Admin UI not loading"
- Verify container is running: `docker ps`
- Check logs: `docker-compose logs admin_ui`
- Clear browser cache (Ctrl+Shift+R)
### Issue: "API connection refused"
- Ensure auth_api and transit_api are running
- Check environment variables are set correctly
- Verify network connectivity between containers
### Issue: "Permission denied" on admin operations
- Verify JWT token includes valid `userCode`
- Check `ADMIN_USER_CODES` environment variable
- Ensure database user has appropriate permissions
@@ -180,6 +201,7 @@ See [DEPLOYMENT.md](../DEPLOYMENT.md) for comprehensive deployment guide.
## Support
For issues or questions:
1. Check service logs: `docker-compose logs admin_ui`
2. Verify API connectivity: Visit API URLs directly
3. Check browser console for client-side errors
@@ -188,4 +210,3 @@ For issues or questions:
## License
Part of the XTransit project
+120 -26
View File
@@ -4,66 +4,149 @@
The Transit API now includes a comprehensive schedule management system with both admin and public endpoints.
### Admin Endpoints (requires authentication + admin privileges)
### Weekly Admin Endpoints (requires authentication + admin privileges)
The active admin UI uses weekly schedule templates. `dayOfWeek` follows the
JavaScript/PostgreSQL convention: `0` Sunday, `1` through `5` weekdays, and `6`
Saturday. Driver assignment is stored in `transit.shifts` and references
`workers.users(id)`.
#### Get weekly schedules
#### Get all schedules
```
GET /admin/schedules
GET /admin/weekly-schedules
Response: { schedules: [...] }
```
#### Create a schedule
#### Get shift drivers
```
GET /admin/shift-drivers
Response: { drivers: [{ id, name }] }
```
#### Create a weekly schedule
````
#### Create multiple departures
```text
POST /admin/weekly-schedules/bulk
Body: {
busLineId: 1,
dayOfWeeks: [1, 2, 3, 4, 5],
startTimes: ["06:00", "06:30", "07:00"],
driverId: 12
}
````
The API creates one schedule row for every day/time combination and derives the
end time from the bus line's configured duration.
POST /admin/weekly-schedules
Body: {
busLineId: 1,
dayOfWeek: 1,
startTime: "08:00",
endTime: "09:00",
driverId: 12
}
```
#### Replace a schedule's driver
```
PUT /admin/weekly-schedules/:scheduleId/shift
Body: { driverId: 12 }
```
Use `{ "driverId": null }` to unassign the driver. The current `shifts` table
does not contain a week/date column, so this is an active assignment rather than
historical per-week assignment. Add `week_start` to support that later.
### Legacy Admin Endpoints
#### Get all schedules
```
GET /admin/schedules
Response: { schedules: [...] }
```
#### Create a schedule
```
POST /admin/schedules
Body: {
busLineId: number,
busId: number,
driverId: number (optional),
startTime: "2026-05-06T08:00:00",
endTime: "2026-05-06T17:00:00"
busLineId: number,
busId: number,
driverId: number (optional),
startTime: "2026-05-06T08:00:00",
endTime: "2026-05-06T17:00:00"
}
Response: { schedule: {...} }
```
#### Delete a schedule
```
DELETE /admin/schedules/:scheduleId
Response: { success: true, schedule: {...} }
```
### Public Endpoints (no authentication required)
#### Get schedules (with optional filters)
```
GET /schedules?date=2026-05-06&lineId=1
Query params:
- date: YYYY-MM-DD (optional)
- lineId: number (optional)
Response: { schedules: [...] }
- date: YYYY-MM-DD (optional)
- lineId: number (optional)
Response: { schedules: [...] }
```
#### Get schedule detail
```
GET /schedules/:scheduleId
Response: { schedule: {...} }
```
#### Get stops for a schedule's line
```
GET /schedules/:scheduleId/stops
Response: { stops: [...] }
Shows all stops on the bus line with order, location, and ETA.
```
````
## Admin UI Features
The ScheduleMaker component provides:
1. **Service Creation Form**
- Select bus line, vehicle, date, and time
- Optional driver ID assignment
- Submit to create schedule
- Admin-only access
1. **Weekly schedule editor**
- Select a bus line and one of weekdays, Saturday, or Sunday
- Define start and end times
- Assign drivers through shifts
- Admin-only access
2. **Timetable View**
- 24-hour time slots (hourly grouping)
@@ -86,12 +169,13 @@ The ScheduleMaker component provides:
The TimetableViewer component provides a public-facing schedule display:
```jsx
import TimetableViewer from './components/TimetableViewer'
import TimetableViewer from "./components/TimetableViewer";
<TimetableViewer apiBaseUrl="http://api.transit.local:5001" />
```
<TimetableViewer apiBaseUrl="http://api.transit.local:5001" />;
````
Features:
- Filter by date and bus line
- Real-time loading state
- Error handling
@@ -104,6 +188,7 @@ Features:
With `DEV_MOCK=1`, all schedule operations work in-memory without a database.
Example:
```bash
ADMIN_USER_CODES=42 DEV_MOCK=1 JWT_SECRET=devsecret npm --prefix ./transit_api start
```
@@ -114,14 +199,19 @@ ADMIN_USER_CODES=42 DEV_MOCK=1 JWT_SECRET=devsecret npm --prefix ./transit_api s
CREATE TABLE transit.schedules (
id BIGINT PRIMARY KEY,
bus_line_id BIGINT REFERENCES transit.bus_lines(id),
bus_id BIGINT REFERENCES fleet.bus(id),
driver_id INTEGER,
start_time TIMESTAMP,
end_time TIMESTAMP,
start_time TIME,
end_time TIME,
status VARCHAR(20) DEFAULT 'planned',
day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE transit.shifts (
id BIGINT PRIMARY KEY,
schedule_id BIGINT REFERENCES transit.schedules(id) ON DELETE CASCADE,
driver_id BIGINT REFERENCES workers.users(id)
);
```
## Styling & UX
@@ -129,6 +219,7 @@ CREATE TABLE transit.schedules (
The schedule manager uses:
### Admin Timetable Styling
- Responsive table with hourly time slots
- Gradient schedule cards (blue/indigo theme)
- Hover effects and smooth transitions
@@ -137,6 +228,7 @@ The schedule manager uses:
- Confirmation modals with semi-transparent overlay
### Public Timetable Styling
- Clean, simple table layout
- Status badges (green for active, gray for planned)
- Mobile-responsive with scrolling
@@ -152,6 +244,7 @@ The schedule manager uses:
## Example Usage
### Create a schedule (Admin)
```bash
curl -X POST http://localhost:5001/admin/schedules \
-H "Authorization: Bearer YOUR_TOKEN" \
@@ -166,11 +259,13 @@ curl -X POST http://localhost:5001/admin/schedules \
```
### Get today's schedules (Public)
```bash
curl http://localhost:5001/schedules?date=$(date +%Y-%m-%d)
```
### Get schedules for specific line (Public)
```bash
curl http://localhost:5001/schedules?lineId=3
```
@@ -184,4 +279,3 @@ curl http://localhost:5001/schedules?lineId=3
- Schedule conflict detection
- Integration with GPS tracking for actual vs scheduled times
- Historical schedule analytics
+33 -65
View File
@@ -1,93 +1,61 @@
import React, { useEffect, useState } from 'react'
import { transitClient } from '../api'
import TimetableDisplay from './TimetableDisplay'
import FixedScheduleEditor from './FixedScheduleEditor'
import React, { useEffect, useState } from "react";
import { transitClient } from "../api";
import WeeklyScheduleEditor from "./WeeklyScheduleEditor";
export default function ScheduleMaker({ token, user }) {
const client = transitClient(token)
const [scheduleTemplates, setScheduleTemplates] = useState([])
const [busLines, setBusLines] = useState([])
const [buses, setBuses] = useState([])
const [drivers, setDrivers] = useState([])
const [selectedDriverFilter, setSelectedDriverFilter] = useState('')
const [message, setMessage] = useState(null)
const [isAdmin] = useState(Boolean(user?.isAdmin))
const [overviewDate, setOverviewDate] = useState(new Date().toISOString().split('T')[0])
export default function ScheduleMaker({ token }) {
const client = transitClient(token);
const [schedules, setSchedules] = useState([]);
const [busLines, setBusLines] = useState([]);
const [drivers, setDrivers] = useState([]);
const [message, setMessage] = useState(null);
const loadScheduleTemplates = async () => {
const loadSchedules = async () => {
try {
const res = await client.get('/admin/schedules?templates=1')
setScheduleTemplates((res.data.schedules || []).filter(schedule => Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0))
const res = await client.get("/admin/weekly-schedules");
setSchedules(res.data.schedules || []);
} catch (err) {
setMessage('Error loading fixed schedules: ' + (err.response?.data?.error || err.message))
}
setMessage("Could not load weekly schedules.");
}
};
const loadBusLines = async () => {
try {
const res = await client.get('/admin/bus-lines')
setBusLines(res.data.busLines || [])
const res = await client.get("/admin/bus-lines");
setBusLines(res.data.busLines || []);
} catch (err) {
setMessage('Error loading bus lines: ' + (err.response?.data?.error || err.message))
}
}
const loadBuses = async () => {
try {
const res = await client.get('/admin/fleet/buses')
setBuses(res.data.buses || [])
} catch (err) {
setMessage('Error loading buses: ' + (err.response?.data?.error || err.message))
}
setMessage("Could not load bus lines.");
}
};
const loadDrivers = async () => {
try {
const res = await client.get('/admin/drivers')
setDrivers(res.data.drivers || [])
const res = await client.get("/admin/shift-drivers");
setDrivers(res.data.drivers || []);
} catch (err) {
setMessage('Error loading drivers: ' + (err.response?.data?.error || err.message))
}
setMessage("Could not load drivers.");
}
};
useEffect(() => {
loadScheduleTemplates()
loadBusLines()
loadBuses()
loadDrivers()
}, [])
loadSchedules();
loadBusLines();
loadDrivers();
}, []);
const handleScheduleChange = async () => {
await loadScheduleTemplates()
}
await loadSchedules();
};
return (
<div>
{/* Timetable widget - compact daily view */}
<TimetableDisplay
<WeeklyScheduleEditor
client={client}
overviewDate={overviewDate}
setOverviewDate={setOverviewDate}
selectedDriverFilter={selectedDriverFilter}
setSelectedDriverFilter={setSelectedDriverFilter}
drivers={drivers}
/>
{/* Fixed service editor - separate widget */}
{isAdmin && (
<FixedScheduleEditor
client={client}
token={token}
user={user}
scheduleTemplates={scheduleTemplates}
busLines={busLines}
buses={buses}
drivers={drivers}
message={message}
setMessage={setMessage}
onScheduleChange={handleScheduleChange}
schedules={schedules}
onChange={handleScheduleChange}
/>
)}
{message && <div className="message">{message}</div>}
</div>
)
);
}
@@ -0,0 +1,268 @@
import React, { useMemo, useState } from "react";
const DAY_GROUPS = [
{ value: "weekdays", label: "Weekdays", days: [1, 2, 3, 4, 5] },
{ value: "saturday", label: "Saturday", days: [6] },
{ value: "sunday", label: "Sunday", days: [0] },
];
const dayLabel = (day) =>
[
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
][day];
export default function WeeklyScheduleEditor({
client,
busLines,
drivers,
schedules,
onChange,
}) {
const [lineId, setLineId] = useState("");
const [category, setCategory] = useState("weekdays");
const [startTimes, setStartTimes] = useState(["08:00"]);
const [newStartTime, setNewStartTime] = useState("");
const [driverId, setDriverId] = useState("");
const [message, setMessage] = useState(null);
const [saving, setSaving] = useState(false);
const groupedSchedules = useMemo(() => {
return schedules.reduce((groups, schedule) => {
const key =
schedule.dayOfWeek >= 1 && schedule.dayOfWeek <= 5
? "weekdays"
: schedule.dayOfWeek === 6
? "saturday"
: "sunday";
groups[key] = [...(groups[key] || []), schedule];
return groups;
}, {});
}, [schedules]);
const createSchedules = async (event) => {
event.preventDefault();
setMessage(null);
if (!lineId || startTimes.length === 0) {
setMessage("Choose a line and add at least one departure time.");
return;
}
const group = DAY_GROUPS.find((item) => item.value === category);
setSaving(true);
try {
const response = await client.post("/admin/weekly-schedules/bulk", {
busLineId: Number(lineId),
dayOfWeeks: group.days,
startTimes,
driverId: driverId ? Number(driverId) : null,
});
setMessage(
`${response.data.createdCount} departures created for ${group.label}.`,
);
setStartTimes(["08:00"]);
setNewStartTime("");
onChange();
} catch (error) {
setMessage(error.response?.data?.error || "Could not save the schedule.");
} finally {
setSaving(false);
}
};
const addStartTime = () => {
if (!newStartTime || startTimes.includes(newStartTime)) return;
setStartTimes([...startTimes, newStartTime].sort());
setNewStartTime("");
};
const removeStartTime = (time) => {
setStartTimes(startTimes.filter((item) => item !== time));
};
const assignDriver = async (scheduleId, value) => {
try {
await client.put(`/admin/weekly-schedules/${scheduleId}/shift`, {
driverId: value ? Number(value) : null,
});
setMessage("Driver assignment updated.");
onChange();
} catch (error) {
setMessage(
error.response?.data?.error ||
"Could not update the driver assignment.",
);
}
};
const removeSchedule = async (scheduleId) => {
if (!window.confirm("Delete this weekly schedule?")) return;
try {
await client.delete(`/admin/weekly-schedules/${scheduleId}`);
setMessage("Weekly schedule deleted.");
onChange();
} catch (error) {
setMessage(
error.response?.data?.error || "Could not delete the schedule.",
);
}
};
return (
<section className="card">
<div className="section-header">
<div>
<h3>Weekly schedules</h3>
<p>
Add multiple daily departures for weekdays, Saturday, or Sunday,
then assign a driver to the generated templates.
</p>
</div>
</div>
<form onSubmit={createSchedules} className="stack-form">
<div className="form-row">
<select
value={lineId}
onChange={(event) => setLineId(event.target.value)}
>
<option value="">Choose bus line</option>
{busLines.map((line) => (
<option key={line.id} value={line.id}>
Line {line.lineNumber} ({line.startStop} to {line.endStop})
</option>
))}
</select>
<select
value={category}
onChange={(event) => setCategory(event.target.value)}
>
{DAY_GROUPS.map((group) => (
<option key={group.value} value={group.value}>
{group.label}
</option>
))}
</select>
</div>
<div className="form-row" style={{ alignItems: "center" }}>
<input
type="time"
value={newStartTime}
onChange={(event) => setNewStartTime(event.target.value)}
aria-label="New departure time"
/>
<button type="button" className="secondary" onClick={addStartTime}>
Add departure
</button>
<div
style={{
display: "flex",
gap: "0.5rem",
flexWrap: "wrap",
flex: 1,
}}
>
{startTimes.map((time) => (
<span
key={time}
style={{
display: "inline-flex",
gap: "0.4rem",
alignItems: "center",
padding: "0.4rem 0.6rem",
border: "1px solid #c7dcff",
borderRadius: "999px",
background: "#eaf2ff",
}}
>
{time}
<button
type="button"
onClick={() => removeStartTime(time)}
aria-label={`Remove departure ${time}`}
style={{
border: "none",
background: "transparent",
cursor: "pointer",
}}
>
x
</button>
</span>
))}
</div>
<select
value={driverId}
onChange={(event) => setDriverId(event.target.value)}
>
<option value="">Unassigned</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
<button type="submit" disabled={saving}>
{saving ? "Saving..." : `Create ${startTimes.length} departures`}
</button>
</div>
</form>
{message && <div className="message">{message}</div>}
{DAY_GROUPS.map((group) => (
<div key={group.value} style={{ marginTop: "1.5rem" }}>
<h4>{group.label}</h4>
{(groupedSchedules[group.value] || []).length === 0 ? (
<p>No schedules configured.</p>
) : (
<div style={{ display: "grid", gap: "0.75rem" }}>
{groupedSchedules[group.value].map((schedule) => (
<article key={schedule.id} className="schedule-card">
<div className="schedule-header">
<strong>Line {schedule.lineNumber}</strong>
<span className="schedule-time">
{String(schedule.startTime).slice(0, 5)} -{" "}
{String(schedule.endTime).slice(0, 5)}
</span>
</div>
<div className="schedule-details">
{dayLabel(schedule.dayOfWeek)}: {schedule.startStop} to{" "}
{schedule.endStop}
</div>
<div className="form-row" style={{ marginTop: "0.75rem" }}>
<select
value={schedule.driverId || ""}
onChange={(event) =>
assignDriver(schedule.id, event.target.value)
}
>
<option value="">Unassigned</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
<button
type="button"
className="danger"
onClick={() => removeSchedule(schedule.id)}
>
Delete
</button>
</div>
</article>
))}
</div>
)}
</div>
))}
</section>
);
}
+81 -9
View File
@@ -61,9 +61,56 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
### Schedule
Schedules are weekly templates. Each row in `transit.schedules` represents one
departure time for one bus line and one `day_of_week`:
- `1` to `5`: weekdays
- `6`: Saturday
- `0`: Sunday
Driver assignments are stored separately in `transit.shifts` with
`(schedule_id, driver_id)`, where `driver_id` references `workers.users(id)`.
The current `shifts` shape represents the active assignment. To retain different
assignments for different calendar weeks, add a `week_start` (or assignment date
range) column and include it in the unique key.
**GET** `/driver/schedules/daily?date=2026-09-18`
- Returns only weekly schedule templates assigned to the authenticated driver through `transit.shifts`.
**PATCH** `/driver/weekly-schedules/:scheduleId/status`
- Updates the status of an assigned weekly schedule.
**GET** `/admin/weekly-schedules`
- Lists weekly templates and their current driver assignments.
**GET** `/admin/shift-drivers`
- Lists drivers from `workers.users` for shift assignment.
**POST** `/admin/weekly-schedules`
- Creates one template. Body: `{ "busLineId": 1, "dayOfWeek": 1, "startTime": "08:00", "endTime": "09:00", "driverId": 12 }`.
**POST** `/admin/weekly-schedules/bulk`
- Creates multiple departures in one transaction. The API derives each end time
from the selected bus line duration. Body: `{ "busLineId": 1, "dayOfWeeks": [1,2,3,4,5], "startTimes": ["06:00", "06:30", "07:00"], "driverId": 12 }`.
**PUT** `/admin/weekly-schedules/:scheduleId/shift`
- Replaces the driver assignment. Pass `{ "driverId": 12 }` or `{ "driverId": null }`.
The legacy `/admin/schedules` and `/schedule/daily` endpoints remain for older
clients, but new clients must use the weekly schedule endpoints above.
**GET** `/schedule/daily?date=2024-05-05`
- Ophalen van dagplanning voor chauffeur
- Response:
```json
{
"date": "2024-05-05",
@@ -88,14 +135,16 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
```
**GET** `/routes/:routeId`
- Details van een specifieke route
- Response: `{ "route": {...} }`
### Public Schedule Overview
**GET** `/schedules/overview?date=2026-05-06&lineId=1`
- Haalt een datum-gebonden overzicht op van geplande diensten
- Herhaalde diensten worden naar de geselecteerde datum gematerialiseerd via `weekdays`
- Weekly templates are selected by `day_of_week` and driver assignments by `shifts`
- Incidentele diensten worden alleen meegenomen op hun exacte datum
- Filters: `driverId`/`chauffeurId`, `stopId`/`busStopId`, `lineId`, `status`
- Response: `{ "date": "2026-05-06", "summary": {...}, "schedules": [...] }`
@@ -103,6 +152,7 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande clients.
Voorbeeld:
```bash
curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
```
@@ -110,8 +160,10 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
### Navigation
**POST** `/navigation/route`
- Opslaan van berekende route van OSRM
- Body:
```json
{
"routeId": 1,
@@ -129,8 +181,10 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
```
**GET** `/bus-lines/:busLineId/stops`
- Alle stops van een buslijn (voor navigatie waypoints)
- Response:
```json
{
"stops": [
@@ -138,8 +192,8 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
"id": 1,
"order": 1,
"name": "Central Station",
"latitude": 52.5200,
"longitude": 13.4050
"latitude": 52.52,
"longitude": 13.405
}
]
}
@@ -148,13 +202,15 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
### GPS Tracking
**POST** `/gps/track`
- Real-time positie opslaan
- Body:
```json
{
"routeId": 1,
"latitude": 52.5210,
"longitude": 13.4060,
"latitude": 52.521,
"longitude": 13.406,
"speed": 45.5,
"heading": 180,
"accuracy": 5.0
@@ -162,15 +218,17 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
```
**GET** `/gps/route/:routeId`
- Alle opgeslagen positie punten van een route
- Response:
```json
{
"positions": [
{
"id": 1,
"latitude": 52.5210,
"longitude": 13.4060,
"latitude": 52.521,
"longitude": 13.406,
"speed": 45.5,
"heading": 180,
"recordedAt": "2024-05-05T08:15:00Z"
@@ -182,6 +240,7 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
### Route Status
**PATCH** `/routes/:routeId/status`
- Update route status
- Body: `{ "status": "in_progress" }`
- Valid statuses: `scheduled`, `in_progress`, `completed`, `cancelled`
@@ -191,19 +250,23 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` voorkomt in `ADMIN_USER_CODES`.
**POST** `/admin/stops`
- Maak een herbruikbare stop aan
- Body:
```json
{
"name": "Central Station",
"latitude": 52.5200,
"longitude": 13.4050
"latitude": 52.52,
"longitude": 13.405
}
```
**POST** `/admin/bus-lines/:busLineId/stops`
- Koppel een bestaande stop aan een buslijn
- Body:
```json
{
"stopId": 1,
@@ -215,12 +278,14 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
## Database Schema
### bus_types
- `id` - Primary key
- `name` - Bus type name (Standard Bus, etc.)
- `seat_capacity` - Number of seats
- `license_plate` - Unique license plate
### bus_lines
- `id` - Primary key
- `line_number` - Unique line number (1, 5, 12, etc.)
- `start_stop` - Start stop name
@@ -228,11 +293,13 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
- `estimated_duration_minutes` - Expected route duration
### stops
- `id` - Primary key
- `name` - Stop name
- `latitude`, `longitude` - Stop location
### route_stops
- `id` - Primary key
- `bus_line_id` - Which bus line the stop belongs to
- `stop_id` - Reusable stop reference
@@ -240,6 +307,7 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
- `estimated_arrival_minutes` - Optional ETA per stop
### routes
- `id` - Primary key
- `user_id` - Chauffeur (references workers.users)
- `bus_line_id` - Which line (references bus_lines)
@@ -248,6 +316,7 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
- `status` - scheduled/in_progress/completed/cancelled
### navigation_routes
- `id` - Primary key
- `route_id` - Link to route (unique)
- `polyline_geojson` - GeoJSON LineString van de route
@@ -255,6 +324,7 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
- `total_duration_seconds` - Estimated duration
### gps_tracking
- `id` - Primary key
- `route_id` - Which route
- `latitude`, `longitude` - Current position
@@ -287,6 +357,7 @@ await scheduleService.updateRouteStatus(1, 'in_progress');
## Error Handling
Alle endpoints retourneren error responses:
```json
{
"error": "error_code",
@@ -295,6 +366,7 @@ Alle endpoints retourneren error responses:
```
Common errors:
- `missing_token` - No Authorization header
- `invalid_token` - Token verification failed
- `missing_required_fields` - Required data missing
+2130 -690
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
CC = i686-w64-mingw32-gcc
CFLAGS = -O2 -Wall -D_WIN32_WINNT=0x0501 -mwindows
LDFLAGS = -lwinhttp
TARGET = driver_xp.exe
all: $(TARGET)
$(TARGET): driver_xp.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
rm -f $(TARGET)
+27
View File
@@ -0,0 +1,27 @@
# Transit Driver XP
Native Win32 driver client targeting Windows XP-era systems.
The client authenticates with `auth_api` at `192.168.68.66:5000`, then requests
assigned weekly schedule templates from `transit_api` at
`192.168.68.66:5001/driver/schedules/daily`.
Schedules are assigned through `transit.shifts`; the client does not expect a
`driver_id`, `bus_id`, or `weekdays` column on `transit.schedules`.
## Build
```bash
make clean && make
```
## Distribution
Build the installer with:
```bash
makensis installer.nsi
```
Distribute `TransitDriverXPSetup.exe`. It detects an existing installation,
stops the previous client, and replaces it in the same directory.
Binary file not shown.
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
!include "MUI2.nsh"
!define APPNAME "Transit Driver XP"
!define COMPANYNAME "Transit Project"
!define VERSION "2.0.0"
Name "${APPNAME}"
OutFile "TransitDriverXPSetup.exe"
InstallDir "$PROGRAMFILES\TransitDriverXP"
InstallDirRegKey HKCU "Software\TransitDriverXP" "InstallDir"
RequestExecutionLevel admin
VIProductVersion "2.0.0.0"
VIAddVersionKey "ProductName" "${APPNAME}"
VIAddVersionKey "CompanyName" "${COMPANYNAME}"
VIAddVersionKey "FileDescription" "Native Windows XP driver client"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
VIAddVersionKey "LegalCopyright" "Transit Project"
Function .onInit
ReadRegStr $0 HKCU "Software\TransitDriverXP" "InstallDir"
StrCmp $0 "" done
IfFileExists "$0\driver_xp.exe" 0 done
StrCpy $INSTDIR $0
MessageBox MB_OK|MB_ICONINFORMATION "An existing Transit Driver XP installation was found. It will be updated."
ExecWait '"$SYSDIR\taskkill.exe" /F /IM driver_xp.exe'
done:
FunctionEnd
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Section "Install"
SetOutPath "$INSTDIR"
SetOverwrite on
File "driver_xp.exe"
WriteRegStr HKCU "Software\TransitDriverXP" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\TransitDriverXP" "DisplayName" "${APPNAME}"
WriteRegStr HKCU "Software\TransitDriverXP" "Version" "${VERSION}"
CreateDirectory "$SMPROGRAMS\TransitDriverXP"
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
CreateShortCut "$DESKTOP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" "$INSTDIR\Uninstall.exe"
SectionEnd
Section "Uninstall"
Delete "$DESKTOP\Transit Driver XP.lnk"
Delete "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk"
Delete "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk"
RMDir "$SMPROGRAMS\TransitDriverXP"
Delete "$INSTDIR\driver_xp.exe"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
DeleteRegKey HKCU "Software\TransitDriverXP"
SectionEnd