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

@@ -29,6 +29,16 @@
.sidebar-content { @apply hidden min-h-0 flex-col px-4 pb-4; max-height: calc(100dvh - 68px); overflow-y: auto; }
.sidebar-content.is-open { display: flex; }
.sidebar-account { @apply flex items-center justify-between gap-3 rounded-xl bg-white px-3 py-2 text-sm; min-height: 56px; }
.city-selector { position: relative; margin-top: 12px; }
.sidebar-city-card { @apply flex w-full items-center justify-between gap-3 rounded-xl bg-white px-3 py-2 text-sm transition-colors hover:bg-gray-50; min-height: 64px; }
.city-emblem { @apply flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary-50 text-lg; overflow: hidden; }
.city-emblem.is-small { @apply h-8 w-8 text-base; }
.city-emblem.is-large { @apply h-14 w-14 rounded-xl text-3xl; }
.city-switcher { position: absolute; z-index: 50; top: calc(100% + 8px); left: 0; right: 0; min-width: 232px; padding: 8px; border-radius: 12px; background: #fff; box-shadow: 0 12px 32px #00000026, 0 2px 8px #0000000d; }
.city-switcher-heading { @apply px-3 pb-2 pt-1 text-xs uppercase tracking-wide text-gray-500; }
.city-switcher-option { @apply flex min-h-[44px] w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-gray-800 hover:bg-gray-50; }
.city-switcher-divider { height: 1px; margin: 6px 4px; background: #f0f0f0; }
.city-switcher-add { @apply text-primary-700; }
.sidebar-group-title { @apply px-3 pb-2 pt-5 text-xs font-normal uppercase tracking-wide text-gray-500; }
.sidebar-link { @apply flex min-h-[44px] items-center gap-3 rounded-lg px-3 py-3 text-sm text-gray-600 transition-colors hover:bg-black/[0.04]; }
.sidebar-link.is-active { @apply bg-black/[0.04] font-semibold text-gray-900; }
@@ -50,6 +60,23 @@
.stat-card-icon { @apply flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 text-gray-600; }
.stat-card-link { @apply mt-auto flex items-center justify-between border-t border-gray-100 pt-3 text-sm text-primary-600; }
.dashboard-intro { @apply flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between; }
.cities-page { @apply space-y-6; }
.cities-heading { @apply flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between; }
.cities-heading > div > p { @apply mt-2 text-gray-500; }
.city-form { @apply space-y-5; }
.city-form h2 { @apply text-lg font-semibold; }
.city-form h2 + p { @apply mt-1 text-sm text-gray-500; }
.city-form-grid { @apply grid gap-4 sm:grid-cols-2; }
.city-form-hint { @apply mt-1 block text-xs text-gray-500; }
.city-form-error { @apply rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700; }
.cities-grid { @apply grid gap-4 md:grid-cols-2; }
.city-network-card { @apply flex items-center gap-4; min-height: 104px; }
.city-network-card.is-active { border-color: #0958d9; box-shadow: inset 0 0 0 1px #0958d9; }
.city-network-card h2 { @apply text-lg font-semibold; }
.city-network-card p { @apply mt-1 text-sm text-gray-500; }
.city-current { @apply inline-flex items-center gap-1 rounded-full bg-primary-50 px-2 py-1 text-xs font-medium text-primary-700; }
.city-isolation-note { @apply flex items-start gap-3 rounded-xl bg-primary-50 p-4 text-sm text-primary-700; }
.city-isolation-note svg { @apply mt-0.5 shrink-0; }
.login-surface { @apply flex min-h-screen items-center justify-center p-4; background: #f5f5f5; }
.login-card { @apply rounded-xl bg-white p-6 sm:p-8; }
@media (max-width: 767px) {