335 lines
9.1 KiB
JavaScript
335 lines
9.1 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
require('dotenv').config();
|
|
|
|
const db = require('./db');
|
|
const { authMiddleware } = require('./middleware');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use(cors());
|
|
|
|
// ============================================
|
|
// ROUTES - Get daily schedule
|
|
// ============================================
|
|
app.get('/schedule/daily', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { date } = req.query;
|
|
const userId = req.user.userId;
|
|
|
|
if (!date) {
|
|
return res.status(400).json({ error: 'date query parameter required' });
|
|
}
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
r.id,
|
|
r.start_time,
|
|
r.end_time,
|
|
r.status,
|
|
bl.id as line_id,
|
|
bl.line_number,
|
|
bl.start_stop,
|
|
bl.end_stop,
|
|
bl.estimated_duration_minutes,
|
|
bl.description,
|
|
bt.id as bus_type_id,
|
|
bt.name as bus_type,
|
|
bt.seat_capacity,
|
|
bt.license_plate
|
|
FROM routes r
|
|
JOIN bus_lines bl ON r.bus_line_id = bl.id
|
|
JOIN bus_types bt ON r.bus_type_id = bt.id
|
|
WHERE r.user_id = $1 AND DATE(r.start_time) = $2
|
|
ORDER BY r.start_time ASC
|
|
`, [userId, date]);
|
|
|
|
const routes = result.rows.map(row => ({
|
|
id: row.id,
|
|
startTime: row.start_time,
|
|
endTime: row.end_time,
|
|
status: row.status,
|
|
busLine: {
|
|
id: row.line_id,
|
|
lineNumber: row.line_number,
|
|
startStop: row.start_stop,
|
|
endStop: row.end_stop,
|
|
estimatedDuration: row.estimated_duration_minutes,
|
|
description: row.description,
|
|
},
|
|
busType: {
|
|
id: row.bus_type_id,
|
|
name: row.bus_type,
|
|
seatCapacity: row.seat_capacity,
|
|
licensePlate: row.license_plate,
|
|
},
|
|
}));
|
|
|
|
res.json({ date, routes });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// ROUTES - Get single route details
|
|
// ============================================
|
|
app.get('/routes/:routeId', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { routeId } = req.params;
|
|
const userId = req.user.userId;
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
r.id,
|
|
r.start_time,
|
|
r.end_time,
|
|
r.status,
|
|
bl.id as line_id,
|
|
bl.line_number,
|
|
bl.start_stop,
|
|
bl.end_stop,
|
|
bl.estimated_duration_minutes,
|
|
bl.description,
|
|
bt.id as bus_type_id,
|
|
bt.name as bus_type,
|
|
bt.seat_capacity,
|
|
bt.license_plate
|
|
FROM routes r
|
|
JOIN bus_lines bl ON r.bus_line_id = bl.id
|
|
JOIN bus_types bt ON r.bus_type_id = bt.id
|
|
WHERE r.id = $1 AND r.user_id = $2
|
|
`, [routeId, userId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'route_not_found' });
|
|
}
|
|
|
|
const row = result.rows[0];
|
|
const route = {
|
|
id: row.id,
|
|
startTime: row.start_time,
|
|
endTime: row.end_time,
|
|
status: row.status,
|
|
busLine: {
|
|
id: row.line_id,
|
|
lineNumber: row.line_number,
|
|
startStop: row.start_stop,
|
|
endStop: row.end_stop,
|
|
estimatedDuration: row.estimated_duration_minutes,
|
|
description: row.description,
|
|
},
|
|
busType: {
|
|
id: row.bus_type_id,
|
|
name: row.bus_type,
|
|
seatCapacity: row.seat_capacity,
|
|
licensePlate: row.license_plate,
|
|
},
|
|
};
|
|
|
|
res.json({ route });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// ROUTES - Get stops for a bus line
|
|
// ============================================
|
|
app.get('/bus-lines/:busLineId/stops', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { busLineId } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
id,
|
|
stop_order,
|
|
stop_name,
|
|
latitude,
|
|
longitude,
|
|
estimated_arrival_minutes
|
|
FROM route_stops
|
|
WHERE bus_line_id = $1
|
|
ORDER BY stop_order ASC
|
|
`, [busLineId]);
|
|
|
|
const stops = result.rows.map(row => ({
|
|
id: row.id,
|
|
order: row.stop_order,
|
|
name: row.stop_name,
|
|
latitude: parseFloat(row.latitude),
|
|
longitude: parseFloat(row.longitude),
|
|
estimatedArrivalMinutes: row.estimated_arrival_minutes,
|
|
}));
|
|
|
|
res.json({ stops });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// NAVIGATION - Save calculated route
|
|
// ============================================
|
|
app.post('/navigation/route', authMiddleware, async (req, res) => {
|
|
try {
|
|
const {
|
|
routeId,
|
|
startLat,
|
|
startLon,
|
|
endLat,
|
|
endLon,
|
|
totalDistance,
|
|
totalDuration,
|
|
geojson,
|
|
} = req.body;
|
|
|
|
if (!routeId || !startLat || !startLon || !endLat || !endLon || !totalDistance || !totalDuration) {
|
|
return res.status(400).json({ error: 'missing_required_fields' });
|
|
}
|
|
|
|
const result = await db.query(`
|
|
INSERT INTO navigation_routes
|
|
(route_id, start_lat, start_lon, end_lat, end_lon,
|
|
total_distance_meters, total_duration_seconds, polyline_geojson)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
ON CONFLICT (route_id) DO UPDATE SET
|
|
start_lat = $2,
|
|
start_lon = $3,
|
|
end_lat = $4,
|
|
end_lon = $5,
|
|
total_distance_meters = $6,
|
|
total_duration_seconds = $7,
|
|
polyline_geojson = $8
|
|
RETURNING *
|
|
`, [
|
|
routeId,
|
|
startLat,
|
|
startLon,
|
|
endLat,
|
|
endLon,
|
|
totalDistance,
|
|
totalDuration,
|
|
JSON.stringify(geojson),
|
|
]);
|
|
|
|
res.json({ route: result.rows[0] });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// GPS TRACKING - Record current position
|
|
// ============================================
|
|
app.post('/gps/track', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { routeId, latitude, longitude, speed, heading, accuracy } = req.body;
|
|
|
|
if (!routeId || !latitude || !longitude) {
|
|
return res.status(400).json({ error: 'missing_required_fields' });
|
|
}
|
|
|
|
await db.query(`
|
|
INSERT INTO gps_tracking
|
|
(route_id, latitude, longitude, speed_kmh, heading, accuracy, recorded_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
|
`, [routeId, latitude, longitude, speed || null, heading || null, accuracy || null]);
|
|
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// GPS TRACKING - Get route history
|
|
// ============================================
|
|
app.get('/gps/route/:routeId', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { routeId } = req.params;
|
|
const userId = req.user.userId;
|
|
|
|
// Verify user owns this route
|
|
const routeCheck = await db.query(
|
|
'SELECT id FROM routes WHERE id = $1 AND user_id = $2',
|
|
[routeId, userId]
|
|
);
|
|
|
|
if (routeCheck.rows.length === 0) {
|
|
return res.status(403).json({ error: 'unauthorized' });
|
|
}
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
id,
|
|
latitude,
|
|
longitude,
|
|
speed_kmh,
|
|
heading,
|
|
accuracy,
|
|
recorded_at
|
|
FROM gps_tracking
|
|
WHERE route_id = $1
|
|
ORDER BY recorded_at ASC
|
|
`, [routeId]);
|
|
|
|
const positions = result.rows.map(row => ({
|
|
id: row.id,
|
|
latitude: parseFloat(row.latitude),
|
|
longitude: parseFloat(row.longitude),
|
|
speed: row.speed_kmh,
|
|
heading: row.heading,
|
|
accuracy: row.accuracy,
|
|
recordedAt: row.recorded_at,
|
|
}));
|
|
|
|
res.json({ positions });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// ROUTES - Update route status
|
|
// ============================================
|
|
app.patch('/routes/:routeId/status', authMiddleware, async (req, res) => {
|
|
try {
|
|
const { routeId } = req.params;
|
|
const { status } = req.body;
|
|
const userId = req.user.userId;
|
|
|
|
const validStatuses = ['scheduled', 'in_progress', 'completed', 'cancelled'];
|
|
if (!validStatuses.includes(status)) {
|
|
return res.status(400).json({ error: 'invalid_status' });
|
|
}
|
|
|
|
const result = await db.query(`
|
|
UPDATE transit.routes
|
|
SET status = $1, updated_at = NOW()
|
|
WHERE id = $2 AND user_id = $3
|
|
RETURNING *
|
|
`, [status, routeId, userId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'route_not_found' });
|
|
}
|
|
|
|
res.json({ route: result.rows[0] });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
|
}
|
|
});
|
|
|
|
const PORT = process.env.PORT || 5001;
|
|
app.listen(PORT, () => console.log(`Transit API listening on ${PORT}`));
|
|
|
|
module.exports = app;
|