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
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:
+23
-2
@@ -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
|
# Transit Admin Control Center
|
||||||
|
|
||||||
Modern React + Vite web application for comprehensive transit system administration.
|
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
|
## Local Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- npm or yarn
|
- npm or yarn
|
||||||
|
|
||||||
@@ -87,7 +103,7 @@ http://localhost:5174
|
|||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
| ------------------ | -------------------------------------------------------- | --------------------- |
|
||||||
| `VITE_AUTH_API` | Auth API URL (same-origin path recommended in Docker) | http://localhost:5000 |
|
| `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 |
|
| `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
|
## Architecture
|
||||||
|
|
||||||
### Technology Stack
|
### Technology Stack
|
||||||
|
|
||||||
- **Frontend**: React 18.2 + Vite 5.0
|
- **Frontend**: React 18.2 + Vite 5.0
|
||||||
- **HTTP Client**: Axios 1.4
|
- **HTTP Client**: Axios 1.4
|
||||||
- **Server**: Nginx (Alpine Linux)
|
- **Server**: Nginx (Alpine Linux)
|
||||||
@@ -116,6 +133,7 @@ When running in Docker, environment variables are injected at runtime via `docke
|
|||||||
### API Integration
|
### API Integration
|
||||||
|
|
||||||
The app communicates with:
|
The app communicates with:
|
||||||
|
|
||||||
- **Auth API** (/login, /me)
|
- **Auth API** (/login, /me)
|
||||||
- **Transit API** (/admin/stops, /admin/bus-lines, /admin/fleet/buses)
|
- **Transit API** (/admin/stops, /admin/bus-lines, /admin/fleet/buses)
|
||||||
- **Error Log API** (/error_log)
|
- **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
|
## Troubleshooting
|
||||||
|
|
||||||
### Issue: "Admin UI not loading"
|
### Issue: "Admin UI not loading"
|
||||||
|
|
||||||
- Verify container is running: `docker ps`
|
- Verify container is running: `docker ps`
|
||||||
- Check logs: `docker-compose logs admin_ui`
|
- Check logs: `docker-compose logs admin_ui`
|
||||||
- Clear browser cache (Ctrl+Shift+R)
|
- Clear browser cache (Ctrl+Shift+R)
|
||||||
|
|
||||||
### Issue: "API connection refused"
|
### Issue: "API connection refused"
|
||||||
|
|
||||||
- Ensure auth_api and transit_api are running
|
- Ensure auth_api and transit_api are running
|
||||||
- Check environment variables are set correctly
|
- Check environment variables are set correctly
|
||||||
- Verify network connectivity between containers
|
- Verify network connectivity between containers
|
||||||
|
|
||||||
### Issue: "Permission denied" on admin operations
|
### Issue: "Permission denied" on admin operations
|
||||||
|
|
||||||
- Verify JWT token includes valid `userCode`
|
- Verify JWT token includes valid `userCode`
|
||||||
- Check `ADMIN_USER_CODES` environment variable
|
- Check `ADMIN_USER_CODES` environment variable
|
||||||
- Ensure database user has appropriate permissions
|
- Ensure database user has appropriate permissions
|
||||||
@@ -180,6 +201,7 @@ See [DEPLOYMENT.md](../DEPLOYMENT.md) for comprehensive deployment guide.
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues or questions:
|
For issues or questions:
|
||||||
|
|
||||||
1. Check service logs: `docker-compose logs admin_ui`
|
1. Check service logs: `docker-compose logs admin_ui`
|
||||||
2. Verify API connectivity: Visit API URLs directly
|
2. Verify API connectivity: Visit API URLs directly
|
||||||
3. Check browser console for client-side errors
|
3. Check browser console for client-side errors
|
||||||
@@ -188,4 +210,3 @@ For issues or questions:
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
Part of the XTransit project
|
Part of the XTransit project
|
||||||
|
|
||||||
|
|||||||
+120
-26
@@ -4,66 +4,149 @@
|
|||||||
|
|
||||||
The Transit API now includes a comprehensive schedule management system with both admin and public endpoints.
|
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: [...] }
|
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
|
POST /admin/schedules
|
||||||
Body: {
|
Body: {
|
||||||
busLineId: number,
|
busLineId: number,
|
||||||
busId: number,
|
busId: number,
|
||||||
driverId: number (optional),
|
driverId: number (optional),
|
||||||
startTime: "2026-05-06T08:00:00",
|
startTime: "2026-05-06T08:00:00",
|
||||||
endTime: "2026-05-06T17:00:00"
|
endTime: "2026-05-06T17:00:00"
|
||||||
}
|
}
|
||||||
Response: { schedule: {...} }
|
Response: { schedule: {...} }
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Delete a schedule
|
#### Delete a schedule
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
DELETE /admin/schedules/:scheduleId
|
DELETE /admin/schedules/:scheduleId
|
||||||
Response: { success: true, schedule: {...} }
|
Response: { success: true, schedule: {...} }
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Public Endpoints (no authentication required)
|
### Public Endpoints (no authentication required)
|
||||||
|
|
||||||
#### Get schedules (with optional filters)
|
#### Get schedules (with optional filters)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
GET /schedules?date=2026-05-06&lineId=1
|
GET /schedules?date=2026-05-06&lineId=1
|
||||||
Query params:
|
Query params:
|
||||||
- date: YYYY-MM-DD (optional)
|
|
||||||
- lineId: number (optional)
|
- date: YYYY-MM-DD (optional)
|
||||||
Response: { schedules: [...] }
|
- lineId: number (optional)
|
||||||
|
Response: { schedules: [...] }
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get schedule detail
|
#### Get schedule detail
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
GET /schedules/:scheduleId
|
GET /schedules/:scheduleId
|
||||||
Response: { schedule: {...} }
|
Response: { schedule: {...} }
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Get stops for a schedule's line
|
#### Get stops for a schedule's line
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
GET /schedules/:scheduleId/stops
|
GET /schedules/:scheduleId/stops
|
||||||
Response: { stops: [...] }
|
Response: { stops: [...] }
|
||||||
Shows all stops on the bus line with order, location, and ETA.
|
Shows all stops on the bus line with order, location, and ETA.
|
||||||
```
|
|
||||||
|
````
|
||||||
|
|
||||||
## Admin UI Features
|
## Admin UI Features
|
||||||
|
|
||||||
The ScheduleMaker component provides:
|
The ScheduleMaker component provides:
|
||||||
|
|
||||||
1. **Service Creation Form**
|
1. **Weekly schedule editor**
|
||||||
- Select bus line, vehicle, date, and time
|
|
||||||
- Optional driver ID assignment
|
- Select a bus line and one of weekdays, Saturday, or Sunday
|
||||||
- Submit to create schedule
|
- Define start and end times
|
||||||
- Admin-only access
|
- Assign drivers through shifts
|
||||||
|
- Admin-only access
|
||||||
|
|
||||||
2. **Timetable View**
|
2. **Timetable View**
|
||||||
- 24-hour time slots (hourly grouping)
|
- 24-hour time slots (hourly grouping)
|
||||||
@@ -86,12 +169,13 @@ The ScheduleMaker component provides:
|
|||||||
The TimetableViewer component provides a public-facing schedule display:
|
The TimetableViewer component provides a public-facing schedule display:
|
||||||
|
|
||||||
```jsx
|
```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:
|
Features:
|
||||||
|
|
||||||
- Filter by date and bus line
|
- Filter by date and bus line
|
||||||
- Real-time loading state
|
- Real-time loading state
|
||||||
- Error handling
|
- Error handling
|
||||||
@@ -104,6 +188,7 @@ Features:
|
|||||||
With `DEV_MOCK=1`, all schedule operations work in-memory without a database.
|
With `DEV_MOCK=1`, all schedule operations work in-memory without a database.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ADMIN_USER_CODES=42 DEV_MOCK=1 JWT_SECRET=devsecret npm --prefix ./transit_api start
|
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 (
|
CREATE TABLE transit.schedules (
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
bus_line_id BIGINT REFERENCES transit.bus_lines(id),
|
bus_line_id BIGINT REFERENCES transit.bus_lines(id),
|
||||||
bus_id BIGINT REFERENCES fleet.bus(id),
|
start_time TIME,
|
||||||
driver_id INTEGER,
|
end_time TIME,
|
||||||
start_time TIMESTAMP,
|
|
||||||
end_time TIMESTAMP,
|
|
||||||
status VARCHAR(20) DEFAULT 'planned',
|
status VARCHAR(20) DEFAULT 'planned',
|
||||||
|
day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_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
|
## Styling & UX
|
||||||
@@ -129,6 +219,7 @@ CREATE TABLE transit.schedules (
|
|||||||
The schedule manager uses:
|
The schedule manager uses:
|
||||||
|
|
||||||
### Admin Timetable Styling
|
### Admin Timetable Styling
|
||||||
|
|
||||||
- Responsive table with hourly time slots
|
- Responsive table with hourly time slots
|
||||||
- Gradient schedule cards (blue/indigo theme)
|
- Gradient schedule cards (blue/indigo theme)
|
||||||
- Hover effects and smooth transitions
|
- Hover effects and smooth transitions
|
||||||
@@ -137,6 +228,7 @@ The schedule manager uses:
|
|||||||
- Confirmation modals with semi-transparent overlay
|
- Confirmation modals with semi-transparent overlay
|
||||||
|
|
||||||
### Public Timetable Styling
|
### Public Timetable Styling
|
||||||
|
|
||||||
- Clean, simple table layout
|
- Clean, simple table layout
|
||||||
- Status badges (green for active, gray for planned)
|
- Status badges (green for active, gray for planned)
|
||||||
- Mobile-responsive with scrolling
|
- Mobile-responsive with scrolling
|
||||||
@@ -152,6 +244,7 @@ The schedule manager uses:
|
|||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
### Create a schedule (Admin)
|
### Create a schedule (Admin)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:5001/admin/schedules \
|
curl -X POST http://localhost:5001/admin/schedules \
|
||||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
@@ -166,11 +259,13 @@ curl -X POST http://localhost:5001/admin/schedules \
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Get today's schedules (Public)
|
### Get today's schedules (Public)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:5001/schedules?date=$(date +%Y-%m-%d)
|
curl http://localhost:5001/schedules?date=$(date +%Y-%m-%d)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get schedules for specific line (Public)
|
### Get schedules for specific line (Public)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:5001/schedules?lineId=3
|
curl http://localhost:5001/schedules?lineId=3
|
||||||
```
|
```
|
||||||
@@ -184,4 +279,3 @@ curl http://localhost:5001/schedules?lineId=3
|
|||||||
- Schedule conflict detection
|
- Schedule conflict detection
|
||||||
- Integration with GPS tracking for actual vs scheduled times
|
- Integration with GPS tracking for actual vs scheduled times
|
||||||
- Historical schedule analytics
|
- Historical schedule analytics
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +1,61 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from "react";
|
||||||
import { transitClient } from '../api'
|
import { transitClient } from "../api";
|
||||||
import TimetableDisplay from './TimetableDisplay'
|
import WeeklyScheduleEditor from "./WeeklyScheduleEditor";
|
||||||
import FixedScheduleEditor from './FixedScheduleEditor'
|
|
||||||
|
|
||||||
export default function ScheduleMaker({ token, user }) {
|
export default function ScheduleMaker({ token }) {
|
||||||
const client = transitClient(token)
|
const client = transitClient(token);
|
||||||
const [scheduleTemplates, setScheduleTemplates] = useState([])
|
const [schedules, setSchedules] = useState([]);
|
||||||
const [busLines, setBusLines] = useState([])
|
const [busLines, setBusLines] = useState([]);
|
||||||
const [buses, setBuses] = useState([])
|
const [drivers, setDrivers] = useState([]);
|
||||||
const [drivers, setDrivers] = useState([])
|
const [message, setMessage] = useState(null);
|
||||||
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])
|
|
||||||
|
|
||||||
const loadScheduleTemplates = async () => {
|
const loadSchedules = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await client.get('/admin/schedules?templates=1')
|
const res = await client.get("/admin/weekly-schedules");
|
||||||
setScheduleTemplates((res.data.schedules || []).filter(schedule => Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0))
|
setSchedules(res.data.schedules || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage('Error loading fixed schedules: ' + (err.response?.data?.error || err.message))
|
setMessage("Could not load weekly schedules.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadBusLines = async () => {
|
const loadBusLines = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await client.get('/admin/bus-lines')
|
const res = await client.get("/admin/bus-lines");
|
||||||
setBusLines(res.data.busLines || [])
|
setBusLines(res.data.busLines || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage('Error loading bus lines: ' + (err.response?.data?.error || err.message))
|
setMessage("Could not load bus lines.");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadDrivers = async () => {
|
const loadDrivers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await client.get('/admin/drivers')
|
const res = await client.get("/admin/shift-drivers");
|
||||||
setDrivers(res.data.drivers || [])
|
setDrivers(res.data.drivers || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage('Error loading drivers: ' + (err.response?.data?.error || err.message))
|
setMessage("Could not load drivers.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadScheduleTemplates()
|
loadSchedules();
|
||||||
loadBusLines()
|
loadBusLines();
|
||||||
loadBuses()
|
loadDrivers();
|
||||||
loadDrivers()
|
}, []);
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleScheduleChange = async () => {
|
const handleScheduleChange = async () => {
|
||||||
await loadScheduleTemplates()
|
await loadSchedules();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Timetable widget - compact daily view */}
|
<WeeklyScheduleEditor
|
||||||
<TimetableDisplay
|
|
||||||
client={client}
|
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}
|
busLines={busLines}
|
||||||
buses={buses}
|
|
||||||
drivers={drivers}
|
drivers={drivers}
|
||||||
message={message}
|
schedules={schedules}
|
||||||
setMessage={setMessage}
|
onChange={handleScheduleChange}
|
||||||
onScheduleChange={handleScheduleChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{message && <div className="message">{message}</div>}
|
||||||
</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
@@ -61,9 +61,56 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
|
|||||||
|
|
||||||
### Schedule
|
### 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`
|
**GET** `/schedule/daily?date=2024-05-05`
|
||||||
|
|
||||||
- Ophalen van dagplanning voor chauffeur
|
- Ophalen van dagplanning voor chauffeur
|
||||||
- Response:
|
- Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"date": "2024-05-05",
|
"date": "2024-05-05",
|
||||||
@@ -88,14 +135,16 @@ Alle endpoints vereisen een `Authorization: Bearer {token}` header.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**GET** `/routes/:routeId`
|
**GET** `/routes/:routeId`
|
||||||
|
|
||||||
- Details van een specifieke route
|
- Details van een specifieke route
|
||||||
- Response: `{ "route": {...} }`
|
- Response: `{ "route": {...} }`
|
||||||
|
|
||||||
### Public Schedule Overview
|
### Public Schedule Overview
|
||||||
|
|
||||||
**GET** `/schedules/overview?date=2026-05-06&lineId=1`
|
**GET** `/schedules/overview?date=2026-05-06&lineId=1`
|
||||||
|
|
||||||
- Haalt een datum-gebonden overzicht op van geplande diensten
|
- 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
|
- Incidentele diensten worden alleen meegenomen op hun exacte datum
|
||||||
- Filters: `driverId`/`chauffeurId`, `stopId`/`busStopId`, `lineId`, `status`
|
- Filters: `driverId`/`chauffeurId`, `stopId`/`busStopId`, `lineId`, `status`
|
||||||
- Response: `{ "date": "2026-05-06", "summary": {...}, "schedules": [...] }`
|
- 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.
|
De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande clients.
|
||||||
|
|
||||||
Voorbeeld:
|
Voorbeeld:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
|
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
|
### Navigation
|
||||||
|
|
||||||
**POST** `/navigation/route`
|
**POST** `/navigation/route`
|
||||||
|
|
||||||
- Opslaan van berekende route van OSRM
|
- Opslaan van berekende route van OSRM
|
||||||
- Body:
|
- Body:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"routeId": 1,
|
"routeId": 1,
|
||||||
@@ -129,8 +181,10 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
|
|||||||
```
|
```
|
||||||
|
|
||||||
**GET** `/bus-lines/:busLineId/stops`
|
**GET** `/bus-lines/:busLineId/stops`
|
||||||
|
|
||||||
- Alle stops van een buslijn (voor navigatie waypoints)
|
- Alle stops van een buslijn (voor navigatie waypoints)
|
||||||
- Response:
|
- Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"stops": [
|
"stops": [
|
||||||
@@ -138,8 +192,8 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"name": "Central Station",
|
"name": "Central Station",
|
||||||
"latitude": 52.5200,
|
"latitude": 52.52,
|
||||||
"longitude": 13.4050
|
"longitude": 13.405
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -148,13 +202,15 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
|
|||||||
### GPS Tracking
|
### GPS Tracking
|
||||||
|
|
||||||
**POST** `/gps/track`
|
**POST** `/gps/track`
|
||||||
|
|
||||||
- Real-time positie opslaan
|
- Real-time positie opslaan
|
||||||
- Body:
|
- Body:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"routeId": 1,
|
"routeId": 1,
|
||||||
"latitude": 52.5210,
|
"latitude": 52.521,
|
||||||
"longitude": 13.4060,
|
"longitude": 13.406,
|
||||||
"speed": 45.5,
|
"speed": 45.5,
|
||||||
"heading": 180,
|
"heading": 180,
|
||||||
"accuracy": 5.0
|
"accuracy": 5.0
|
||||||
@@ -162,15 +218,17 @@ curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&bu
|
|||||||
```
|
```
|
||||||
|
|
||||||
**GET** `/gps/route/:routeId`
|
**GET** `/gps/route/:routeId`
|
||||||
|
|
||||||
- Alle opgeslagen positie punten van een route
|
- Alle opgeslagen positie punten van een route
|
||||||
- Response:
|
- Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"latitude": 52.5210,
|
"latitude": 52.521,
|
||||||
"longitude": 13.4060,
|
"longitude": 13.406,
|
||||||
"speed": 45.5,
|
"speed": 45.5,
|
||||||
"heading": 180,
|
"heading": 180,
|
||||||
"recordedAt": "2024-05-05T08:15:00Z"
|
"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
|
### Route Status
|
||||||
|
|
||||||
**PATCH** `/routes/:routeId/status`
|
**PATCH** `/routes/:routeId/status`
|
||||||
|
|
||||||
- Update route status
|
- Update route status
|
||||||
- Body: `{ "status": "in_progress" }`
|
- Body: `{ "status": "in_progress" }`
|
||||||
- Valid statuses: `scheduled`, `in_progress`, `completed`, `cancelled`
|
- 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`.
|
Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` voorkomt in `ADMIN_USER_CODES`.
|
||||||
|
|
||||||
**POST** `/admin/stops`
|
**POST** `/admin/stops`
|
||||||
|
|
||||||
- Maak een herbruikbare stop aan
|
- Maak een herbruikbare stop aan
|
||||||
- Body:
|
- Body:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Central Station",
|
"name": "Central Station",
|
||||||
"latitude": 52.5200,
|
"latitude": 52.52,
|
||||||
"longitude": 13.4050
|
"longitude": 13.405
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**POST** `/admin/bus-lines/:busLineId/stops`
|
**POST** `/admin/bus-lines/:busLineId/stops`
|
||||||
|
|
||||||
- Koppel een bestaande stop aan een buslijn
|
- Koppel een bestaande stop aan een buslijn
|
||||||
- Body:
|
- Body:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"stopId": 1,
|
"stopId": 1,
|
||||||
@@ -215,12 +278,14 @@ Alle admin-endpoints vereisen naast een geldig JWT-token ook dat je `userCode` v
|
|||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
### bus_types
|
### bus_types
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `name` - Bus type name (Standard Bus, etc.)
|
- `name` - Bus type name (Standard Bus, etc.)
|
||||||
- `seat_capacity` - Number of seats
|
- `seat_capacity` - Number of seats
|
||||||
- `license_plate` - Unique license plate
|
- `license_plate` - Unique license plate
|
||||||
|
|
||||||
### bus_lines
|
### bus_lines
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `line_number` - Unique line number (1, 5, 12, etc.)
|
- `line_number` - Unique line number (1, 5, 12, etc.)
|
||||||
- `start_stop` - Start stop name
|
- `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
|
- `estimated_duration_minutes` - Expected route duration
|
||||||
|
|
||||||
### stops
|
### stops
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `name` - Stop name
|
- `name` - Stop name
|
||||||
- `latitude`, `longitude` - Stop location
|
- `latitude`, `longitude` - Stop location
|
||||||
|
|
||||||
### route_stops
|
### route_stops
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `bus_line_id` - Which bus line the stop belongs to
|
- `bus_line_id` - Which bus line the stop belongs to
|
||||||
- `stop_id` - Reusable stop reference
|
- `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
|
- `estimated_arrival_minutes` - Optional ETA per stop
|
||||||
|
|
||||||
### routes
|
### routes
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `user_id` - Chauffeur (references workers.users)
|
- `user_id` - Chauffeur (references workers.users)
|
||||||
- `bus_line_id` - Which line (references bus_lines)
|
- `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
|
- `status` - scheduled/in_progress/completed/cancelled
|
||||||
|
|
||||||
### navigation_routes
|
### navigation_routes
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `route_id` - Link to route (unique)
|
- `route_id` - Link to route (unique)
|
||||||
- `polyline_geojson` - GeoJSON LineString van de route
|
- `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
|
- `total_duration_seconds` - Estimated duration
|
||||||
|
|
||||||
### gps_tracking
|
### gps_tracking
|
||||||
|
|
||||||
- `id` - Primary key
|
- `id` - Primary key
|
||||||
- `route_id` - Which route
|
- `route_id` - Which route
|
||||||
- `latitude`, `longitude` - Current position
|
- `latitude`, `longitude` - Current position
|
||||||
@@ -287,6 +357,7 @@ await scheduleService.updateRouteStatus(1, 'in_progress');
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
Alle endpoints retourneren error responses:
|
Alle endpoints retourneren error responses:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": "error_code",
|
"error": "error_code",
|
||||||
@@ -295,6 +366,7 @@ Alle endpoints retourneren error responses:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Common errors:
|
Common errors:
|
||||||
|
|
||||||
- `missing_token` - No Authorization header
|
- `missing_token` - No Authorization header
|
||||||
- `invalid_token` - Token verification failed
|
- `invalid_token` - Token verification failed
|
||||||
- `missing_required_fields` - Required data missing
|
- `missing_required_fields` - Required data missing
|
||||||
|
|||||||
+2130
-690
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||||
@@ -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
Executable
BIN
Binary file not shown.
@@ -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
|
||||||
Reference in New Issue
Block a user