feat: implement multi-city support with city management features

- Added a new cities table to the database to manage multiple cities independently.
- Created a middleware to resolve city context based on X-City-Id header.
- Updated routes and services to handle city-specific data for routes, stops, and alerts.
- Implemented a CityContext in the frontend to manage active city state and provide city data across the application.
- Added a CitySelector component for users to switch between cities.
- Created a CitiesPage for admins to manage cities, including adding new cities with validation.
- Updated various components and pages to reflect the active city context, ensuring data is displayed correctly based on the selected city.
- Enhanced the schedule import functionality to restrict imports based on the active city.
This commit is contained in:
2026-09-17 19:41:17 +03:00
parent d284c6e940
commit d13cc92d82
27 changed files with 730 additions and 144 deletions

View File

@@ -0,0 +1,46 @@
const pool = require('../config/database');
const logger = require('../config/logger');
const DEFAULT_CITY_SLUG = 'polotsk';
async function resolveCity(req, res, next) {
const rawCityId = req.get('X-City-Id');
if (rawCityId !== undefined && (!/^\d+$/.test(rawCityId) || Number(rawCityId) <= 0)) {
return res.status(400).json({ error: 'Invalid city identifier' });
}
try {
const result = rawCityId
? await pool.query('SELECT * FROM cities WHERE id = $1 AND is_active = true', [Number(rawCityId)])
: await pool.query('SELECT * FROM cities WHERE slug = $1 AND is_active = true', [DEFAULT_CITY_SLUG]);
if (!result.rows.length) {
return res.status(404).json({ error: 'City not found or inactive' });
}
req.city = result.rows[0];
return next();
} catch (error) {
logger.error('Unable to resolve city context', { message: error.message });
return res.status(500).json({ error: 'Unable to resolve city' });
}
}
async function getRouteInCity(routeId, cityId, db = pool) {
const result = await db.query(
'SELECT * FROM routes WHERE id = $1 AND city_id = $2',
[routeId, cityId]
);
return result.rows[0] || null;
}
async function getStopInCity(stopId, cityId, db = pool) {
const result = await db.query(
'SELECT * FROM stops WHERE id = $1 AND city_id = $2',
[stopId, cityId]
);
return result.rows[0] || null;
}
module.exports = { DEFAULT_CITY_SLUG, resolveCity, getRouteInCity, getStopInCity };