diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..bcc045bb --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "transit-dashboard", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5183, + "cwd": "transit_dashboard" + } + ] +} diff --git a/backend/transit/cron_approaching_alerts.php b/backend/transit/cron_approaching_alerts.php index 7266abe9..0ead2d27 100644 --- a/backend/transit/cron_approaching_alerts.php +++ b/backend/transit/cron_approaching_alerts.php @@ -1,9 +1,20 @@ getMessage() . "\n"; + exit(1); +} // 1. جلب الرحلات النشطة حالياً $stTrips = $transit_con->query( @@ -11,17 +22,23 @@ $stTrips = $transit_con->query( ); $trips = $stTrips->fetchAll(); +if (empty($trips)) { + echo "No active trips. Nothing to check.\n"; + exit(0); +} + foreach ($trips as $trip) { $tripId = (int)$trip['id']; $routeId = (int)$trip['route_id']; $currentSeq = (int)$trip['current_stop_seq']; - // 2. جلب موقع الباص من Redis + // 2. جلب موقع الباص من Redis — تجاهل إن كان قديماً (> 3 دقائق، أي الباص فقد الاتصال) $pos = transitGetBusPosition($tripId); if (!$pos || empty($pos['lat'])) continue; + if (empty($pos['ts']) || (time() - (int)$pos['ts']) > 180) continue; - $busLat = (double)$pos['lat']; - $busLng = (double)$pos['lng']; + $busLat = (float)$pos['lat']; + $busLng = (float)$pos['lng']; // 3. جلب المحطة التالية $nextSeq = $currentSeq + 1; @@ -35,8 +52,8 @@ foreach ($trips as $trip) { if ($nextStop) { $stopId = (int)$nextStop['id']; - $stopLat = (double)$nextStop['latitude']; - $stopLng = (double)$nextStop['longitude']; + $stopLat = (float)$nextStop['latitude']; + $stopLng = (float)$nextStop['longitude']; $stopName = $nextStop['name_ar']; // حساب المسافة تقريبياً diff --git a/backend/transit/driver/suspend.php b/backend/transit/driver/suspend.php new file mode 100644 index 00000000..be2523e4 --- /dev/null +++ b/backend/transit/driver/suspend.php @@ -0,0 +1,27 @@ +prepare("SELECT id, status FROM transit_drivers WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$driverId, $transit_org_id]); +$driver = $chk->fetch(); +if (!$driver) jsonError('Driver not found or access denied', 404); + +// إعادة التفعيل غير ممكنة إلا لسائق سبق تفعيله عبر الدعوة (status كان suspended) +if ($action === 'activate' && $driver['status'] !== 'suspended') { + jsonError('Only a suspended driver can be reactivated', 409); +} + +$newStatus = $action === 'suspend' ? 'suspended' : 'active'; +$transit_con->prepare("UPDATE transit_drivers SET status=? WHERE id=?")->execute([$newStatus, $driverId]); + +appLog("[TRANSIT][DRIVER][suspend] driver #{$driverId} org#{$transit_org_id} → {$newStatus}"); + +jsonSuccess(['driver_id' => $driverId, 'status' => $newStatus]); diff --git a/backend/transit/enrollment/rosters_list.php b/backend/transit/enrollment/rosters_list.php new file mode 100644 index 00000000..d4a40cec --- /dev/null +++ b/backend/transit/enrollment/rosters_list.php @@ -0,0 +1,14 @@ +prepare( + "SELECT id, filename, total_rows, matched_rows, new_enrollments, expired_count, + semester, notes, created_at + FROM transit_rosters + WHERE org_id=? ORDER BY created_at DESC LIMIT 50" +); +$st->execute([$transit_org_id]); + +jsonSuccess(['rosters' => $st->fetchAll()]); diff --git a/backend/transit/functions.php b/backend/transit/functions.php index bbfd3c75..3b9169e7 100644 --- a/backend/transit/functions.php +++ b/backend/transit/functions.php @@ -137,6 +137,34 @@ function transitOrgTopic(int $orgId): string return 'transit_org_' . $orgId; } +// إشعار مباشر لراكب واحد عبر موضوعه الشخصي passenger_{id} +// (التطبيق يشترك بهذا الموضوع عند تسجيل الدخول — انظر firbase_messge.dart) +function transitSendNotificationToPassenger(string $passengerId, string $title, string $body, array $data = []): void +{ + if (!$passengerId) return; + transitSendTopicNotification('passenger_' . $passengerId, $title, $body, $data); +} + +// إشعار أولياء الأمور المرتبطين بعضوية طالب — يحترم أعمدة notify_* بالجدول +// $event: depart | board | arrive | return +function transitNotifyGuardians(int $enrollmentId, string $event, string $title, string $body): void +{ + global $transit_con; + if (!$transit_con) return; + + $column = 'notify_' . $event; + if (!in_array($event, ['depart', 'board', 'arrive', 'return'])) return; + + $st = $transit_con->prepare( + "SELECT guardian_passenger_id FROM transit_guardians + WHERE enrollment_id=? AND {$column}=1" + ); + $st->execute([$enrollmentId]); + foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $guardianId) { + transitSendNotificationToPassenger((string)$guardianId, $title, $body, ['type' => 'transit_guardian_' . $event]); + } +} + function transitSendTopicNotification(string $topic, string $title, string $body, array $data = []): void { global $redis; diff --git a/backend/transit/route/list.php b/backend/transit/route/list.php index 1ac32059..b2435f08 100644 --- a/backend/transit/route/list.php +++ b/backend/transit/route/list.php @@ -4,7 +4,7 @@ require_once __DIR__ . '/../../transit/connect_admin.php'; $status = filterRequest('status') ?: 'active'; -if (!in_array($status, ['all','draft','active','suspended'])) $status = 'active'; +if (!in_array($status, ['all','draft','active','suspended','rejected'])) $status = 'active'; $sql = "SELECT id, name_ar, name_en, direction, distance_km, duration_min, status, created_at FROM transit_routes WHERE org_id=?"; $params = [$transit_org_id]; diff --git a/backend/transit/route/update.php b/backend/transit/route/update.php new file mode 100644 index 00000000..1090d87e --- /dev/null +++ b/backend/transit/route/update.php @@ -0,0 +1,80 @@ +prepare("SELECT id, status FROM transit_routes WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$routeId, $transit_org_id]); +$route = $chk->fetch(); +if (!$route) jsonError('Route not found or access denied', 404); + +// إن كان معتمداً أو موقوفاً، التعديل يعيده لمسودة بانتظار اعتماد جديد +$newStatus = in_array($route['status'], ['active', 'suspended']) ? 'draft' : $route['status']; + +$transit_con->beginTransaction(); +try { + $transit_con->prepare( + "UPDATE transit_routes + SET name_ar=?, name_en=?, direction=?, polyline=?, distance_km=?, duration_min=?, + status=?, approved_by=NULL, approved_at=NULL, updated_at=NOW() + WHERE id=?" + )->execute([ + $nameAr, + filterRequest('name_en'), + $direction, + filterRequest('polyline'), + filterRequest('distance_km', 'float'), + filterRequest('duration_min', 'int'), + $newStatus, + $routeId, + ]); + + // استبدال كامل لمحطات الخط + $transit_con->prepare("DELETE FROM transit_stops WHERE route_id=?")->execute([$routeId]); + + $insertStop = $transit_con->prepare( + "INSERT INTO transit_stops + (route_id,org_id,sequence,name_ar,name_en,latitude,longitude,geofence_radius,eta_offset_min,is_major) + VALUES (?,?,?,?,?,?,?,?,?,?)" + ); + foreach ($stops as $i => $s) { + if (empty($s['lat']) || empty($s['lng']) || empty($s['name_ar'])) continue; + $insertStop->execute([ + $routeId, $transit_org_id, $i + 1, + $s['name_ar'], $s['name_en'] ?? null, + (float)$s['lat'], (float)$s['lng'], + (int)($s['radius'] ?? 150), + isset($s['eta_offset_min']) ? (int)$s['eta_offset_min'] : null, + (int)($s['is_major'] ?? 0), + ]); + } + + $transit_con->commit(); +} catch (Throwable $e) { + $transit_con->rollBack(); + appLog('[TRANSIT][ROUTE][update] ' . $e->getMessage(), 'ERROR'); + jsonError('Failed to update route', 500); +} + +appLog("[TRANSIT][ROUTE][update] route #{$routeId} org#{$transit_org_id} updated, status={$newStatus}"); + +jsonSuccess([ + 'route_id' => $routeId, + 'status' => $newStatus, + 'stops_count'=> count($stops), +], $newStatus === 'draft' ? 'Route updated — pending re-approval' : 'Route updated'); diff --git a/backend/transit/schedule/delete.php b/backend/transit/schedule/delete.php new file mode 100644 index 00000000..c83c26a3 --- /dev/null +++ b/backend/transit/schedule/delete.php @@ -0,0 +1,18 @@ +prepare("SELECT id FROM transit_schedules WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$scheduleId, $transit_org_id]); +if (!$chk->fetch()) jsonError('Schedule not found or access denied', 404); + +$transit_con->prepare("DELETE FROM transit_schedules WHERE id=?")->execute([$scheduleId]); + +appLog("[TRANSIT][SCHEDULE][delete] schedule #{$scheduleId} org#{$transit_org_id}"); + +jsonSuccess(['schedule_id' => $scheduleId], 'Schedule deleted'); diff --git a/backend/transit/schedule/list.php b/backend/transit/schedule/list.php new file mode 100644 index 00000000..d900d63e --- /dev/null +++ b/backend/transit/schedule/list.php @@ -0,0 +1,25 @@ +prepare("SELECT id FROM transit_routes WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$routeId, $transit_org_id]); +if (!$chk->fetch()) jsonError('Route not found or access denied', 404); + +$st = $transit_con->prepare( + "SELECT s.id, s.departure_time, s.days_mask, s.valid_from, s.valid_until, s.is_active, + s.driver_id, s.vehicle_id, d.name AS driver_name, v.plate AS vehicle_plate + FROM transit_schedules s + LEFT JOIN transit_drivers d ON d.id = s.driver_id + LEFT JOIN transit_vehicles v ON v.id = s.vehicle_id + WHERE s.route_id=? AND s.org_id=? + ORDER BY s.departure_time ASC" +); +$st->execute([$routeId, $transit_org_id]); + +jsonSuccess(['schedules' => $st->fetchAll()]); diff --git a/backend/transit/schema_transit.sql b/backend/transit/schema_transit.sql index 6b3f2e73..46abd12a 100644 --- a/backend/transit/schema_transit.sql +++ b/backend/transit/schema_transit.sql @@ -138,7 +138,7 @@ CREATE TABLE IF NOT EXISTS `transit_routes` ( `polyline` TEXT DEFAULT NULL COMMENT 'Encoded polyline للمسار', `distance_km` DECIMAL(7,2) DEFAULT NULL, `duration_min` SMALLINT DEFAULT NULL, - `status` ENUM('draft','active','suspended') NOT NULL DEFAULT 'draft', + `status` ENUM('draft','active','suspended','rejected') NOT NULL DEFAULT 'draft', `created_by` INT UNSIGNED DEFAULT NULL COMMENT 'admin_id من أنشأه', `approved_by` INT UNSIGNED DEFAULT NULL COMMENT 'admin_id من اعتمده (فريق سيرو)', `approved_at` TIMESTAMP DEFAULT NULL, @@ -337,6 +337,8 @@ SET FOREIGN_KEY_CHECKS = 1; -- COMMENT 'معرّف في جدول passengers الرئيسي — NULL لصفوف الكشف قبل تفعيل الطالب'; -- ALTER TABLE transit_trips MODIFY vehicle_id INT UNSIGNED DEFAULT NULL -- COMMENT 'NULL إن كان الجدول بلا باص مخصص'; +-- ALTER TABLE transit_routes MODIFY status ENUM('draft','active','suspended','rejected') NOT NULL DEFAULT 'draft'; +-- (بدونها: Admin/transit/route/approve.php بإجراء reject يفشل بخطأ SQL دائماً) -- ----------------------------------------------------------------- -- ----------------------------------------------------------------- diff --git a/backend/transit/trip/board_passenger.php b/backend/transit/trip/board_passenger.php index 864abf21..432e2470 100644 --- a/backend/transit/trip/board_passenger.php +++ b/backend/transit/trip/board_passenger.php @@ -4,6 +4,8 @@ require_once __DIR__ . '/../../transit/connect_app.php'; +if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403); + $stopId = (int)filterRequest('stop_id'); $passengerId = filterRequest('passenger_id'); @@ -11,7 +13,16 @@ if (!$stopId || !$passengerId) { jsonError('Missing parameters', 400); } -// 1. تحقق من السائق ورحلته الحالية +// استخرج سجل السائق من JWT — لا نثق بأي معرّف سائق من العميل +$driverRow = $transit_con->prepare( + "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" +); +$driverRow->execute([(string)$transit_user_id]); +$driverRec = $driverRow->fetch(); +if (!$driverRec) jsonError('Driver not found or not active', 403); +$transit_driver_id = (int)$driverRec['id']; + +// 1. تحقق من رحلة السائق الحالية $stTrip = $transit_con->prepare( "SELECT id, org_id FROM transit_trips WHERE driver_id=? AND status='started' LIMIT 1" ); @@ -55,27 +66,11 @@ $stInsert = $transit_con->prepare( ); $stInsert->execute([$tripId, $enrollId, $stopId]); -// 5. إرسال إشعار FCM لولي الأمر (إن وُجد) -$stGuard = $transit_con->prepare( - "SELECT guardian_passenger_id FROM transit_guardians - WHERE enrollment_id=? AND notify_board=1" -); -$stGuard->execute([$enrollId]); -$guardians = $stGuard->fetchAll(PDO::FETCH_COLUMN); +// 5. إرسال إشعار صعود — لولي الأمر (إن وُجد) وللطالب نفسه (تأكيد بمحفظته) +$title = "صعود الحافلة 🚌"; +$body = "لقد صعد " . ($enroll['member_name'] ?? 'الطالب') . " إلى الحافلة بنجاح."; -// إذا كان الطالب نفسه يملك التطبيق، أرسل له أيضاً (للتأكيد في محفظته) -$guardians[] = $passengerId; - -if (!empty($guardians)) { - $title = "صعود الحافلة 🚌"; - $body = "لقد صعد " . ($enroll['member_name'] ?? 'الطالب') . " إلى الحافلة بنجاح."; - - // إرسال الإشعار باستخدام دالة الإشعارات الأساسية بالنظام (عبر passenger_id) - foreach ($guardians as $gId) { - // يمكنك هنا مناداة دالة sendFCMToPassenger الموجودة في functions.php الأساسية - // للتبسيط، نكتفي بافتراض وجود دالة transitSendNotificationToPassenger أو نستخدم FCM_Engine - transitSendNotificationToPassenger($gId, $title, $body); - } -} +transitNotifyGuardians($enrollId, 'board', $title, $body); +transitSendNotificationToPassenger((string)$passengerId, $title, $body, ['type' => 'transit_boarded']); jsonSuccess(['boarded' => true]); diff --git a/backend/transit/trip/cancel.php b/backend/transit/trip/cancel.php new file mode 100644 index 00000000..da822ddb --- /dev/null +++ b/backend/transit/trip/cancel.php @@ -0,0 +1,47 @@ +prepare( + "SELECT t.id, t.status, t.route_id, r.name_ar AS route_name + FROM transit_trips t JOIN transit_routes r ON r.id = t.route_id + WHERE t.id=? AND t.org_id=? LIMIT 1" +); +$st->execute([$tripId, $transit_org_id]); +$trip = $st->fetch(); +if (!$trip) jsonError('Trip not found or access denied', 404); + +if (in_array($trip['status'], ['completed', 'cancelled'])) { + jsonError('Trip is already ' . $trip['status'], 409); +} + +$wasStarted = $trip['status'] === 'started'; + +$transit_con->prepare( + "UPDATE transit_trips SET status='cancelled', delay_reason=?, updated_at=NOW() WHERE id=?" +)->execute([filterRequest('reason'), $tripId]); + +// إن كانت الرحلة قد بدأت فعلياً، أوقف بث الباص وحرّر ملكية الرحلة فوراً +if ($wasStarted) { + transitClearTripOwner($tripId); + global $redisLocation; + if ($redisLocation) $redisLocation->del("transit:trip:{$tripId}:pos"); + global $redis; + if ($redis) $redis->del("transit:trip:{$tripId}:status"); +} + +transitSendTopicNotification( + transitRouteTopic((int)$trip['route_id']), + 'تم إلغاء الرحلة', + 'أُلغيت رحلة خط ' . $trip['route_name'] . ' اليوم.', + ['type' => 'transit_cancelled', 'trip_id' => (string)$tripId] +); + +appLog("[TRANSIT][TRIP][cancel] trip #{$tripId} org#{$transit_org_id} cancelled by admin #{$transit_admin_id}"); + +jsonSuccess(['trip_id' => $tripId, 'status' => 'cancelled'], 'Trip cancelled'); diff --git a/backend/transit/trip/end.php b/backend/transit/trip/end.php index 305bca83..f61317c1 100644 --- a/backend/transit/trip/end.php +++ b/backend/transit/trip/end.php @@ -18,7 +18,9 @@ if (!$driverRec) jsonError('Driver not found or not active', 403); $driverId = (int)$driverRec['id']; $st = $transit_con->prepare( - "SELECT id, route_id, status FROM transit_trips WHERE id=? AND driver_id=? LIMIT 1" + "SELECT t.id, t.route_id, t.status, r.name_ar AS route_name, r.direction + FROM transit_trips t JOIN transit_routes r ON r.id = t.route_id + WHERE t.id=? AND t.driver_id=? LIMIT 1" ); $st->execute([$tripId, $driverId]); $trip = $st->fetch(); @@ -38,4 +40,19 @@ transitClearTripOwner($tripId); global $redisLocation; if ($redisLocation) $redisLocation->del("transit:trip:{$tripId}:pos"); +// إشعار أولياء الأمور — وصول (ذهاب) أو عودة (إياب) حسب اتجاه الخط +$guardEvent = ($trip['direction'] === 'inbound') ? 'return' : 'arrive'; +$guardTitle = ($guardEvent === 'return') ? 'عاد الباص 🏠' : 'وصل الباص 🎓'; +$guardBody = ($guardEvent === 'return') + ? 'وصل باص خط ' . $trip['route_name'] . ' إلى محطته الأخيرة.' + : 'وصل باص خط ' . $trip['route_name'] . ' إلى وجهته.'; + +$stGuardEnroll = $transit_con->prepare( + "SELECT id FROM transit_enrollments WHERE preferred_route_id=? AND status='active'" +); +$stGuardEnroll->execute([(int)$trip['route_id']]); +foreach ($stGuardEnroll->fetchAll(PDO::FETCH_COLUMN) as $enrollId) { + transitNotifyGuardians((int)$enrollId, $guardEvent, $guardTitle, $guardBody); +} + jsonSuccess(['trip_id' => $tripId, 'status' => 'completed'], 'Trip completed'); diff --git a/backend/transit/trip/start.php b/backend/transit/trip/start.php index ad95fa48..c13d63c4 100644 --- a/backend/transit/trip/start.php +++ b/backend/transit/trip/start.php @@ -56,4 +56,18 @@ transitSendTopicNotification( ['type' => 'transit_started', 'trip_id' => (string)$tripId] ); +// إشعار أولياء أمور جميع الأعضاء المسجَّلين على هذا الخط بانطلاق الباص +$stGuardEnroll = $transit_con->prepare( + "SELECT id FROM transit_enrollments WHERE preferred_route_id=? AND status='active'" +); +$stGuardEnroll->execute([(int)$trip['route_id']]); +foreach ($stGuardEnroll->fetchAll(PDO::FETCH_COLUMN) as $enrollId) { + transitNotifyGuardians( + (int)$enrollId, + 'depart', + 'انطلق الباص 🚌', + 'انطلق باص خط ' . $trip['route_name'] . ' الآن.' + ); +} + jsonSuccess(['trip_id' => $tripId, 'status' => 'started', 'started_at' => date('Y-m-d H:i:s')], 'Trip started'); diff --git a/backend/transit/trip/stop_passengers.php b/backend/transit/trip/stop_passengers.php index 0f5f1072..c4ddc25e 100644 --- a/backend/transit/trip/stop_passengers.php +++ b/backend/transit/trip/stop_passengers.php @@ -4,10 +4,21 @@ require_once __DIR__ . '/../../transit/connect_app.php'; +if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403); + $stopId = (int)filterRequest('stop_id'); if (!$stopId) jsonError('Missing stop_id', 400); -// 1. تحقق من السائق ورحلته +// استخرج سجل السائق من JWT — لا نثق بأي معرّف سائق من العميل +$driverRow = $transit_con->prepare( + "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" +); +$driverRow->execute([(string)$transit_user_id]); +$driverRec = $driverRow->fetch(); +if (!$driverRec) jsonError('Driver not found or not active', 403); +$transit_driver_id = (int)$driverRec['id']; + +// 1. تحقق من رحلة السائق $stTrip = $transit_con->prepare( "SELECT id, org_id FROM transit_trips WHERE driver_id=? AND status='started' LIMIT 1" ); diff --git a/backend/transit/vehicle/toggle.php b/backend/transit/vehicle/toggle.php new file mode 100644 index 00000000..3e17c566 --- /dev/null +++ b/backend/transit/vehicle/toggle.php @@ -0,0 +1,18 @@ +prepare("SELECT id FROM transit_vehicles WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$vehicleId, $transit_org_id]); +if (!$chk->fetch()) jsonError('Vehicle not found or access denied', 404); + +$transit_con->prepare("UPDATE transit_vehicles SET is_active=? WHERE id=?") + ->execute([$isActive ? 1 : 0, $vehicleId]); + +jsonSuccess(['vehicle_id' => $vehicleId, 'is_active' => (bool)$isActive]); diff --git a/backend/transit/vehicle/update.php b/backend/transit/vehicle/update.php new file mode 100644 index 00000000..12b0e702 --- /dev/null +++ b/backend/transit/vehicle/update.php @@ -0,0 +1,38 @@ +prepare("SELECT id FROM transit_vehicles WHERE id=? AND org_id=? LIMIT 1"); +$chk->execute([$vehicleId, $transit_org_id]); +if (!$chk->fetch()) jsonError('Vehicle not found or access denied', 404); + +$plate = filterRequest('plate'); +if ($plate) { + $plate = strtoupper($plate); + $dup = $transit_con->prepare("SELECT id FROM transit_vehicles WHERE plate=? AND id<>? LIMIT 1"); + $dup->execute([$plate, $vehicleId]); + if ($dup->fetch()) jsonError('Vehicle plate already registered', 409); +} + +$vType = filterRequest('vehicle_type'); +if ($vType && !in_array($vType, ['bus','minibus','van','other'])) $vType = null; + +$transit_con->prepare( + "UPDATE transit_vehicles + SET plate=COALESCE(?, plate), make=COALESCE(?, make), model=COALESCE(?, model), + year=COALESCE(?, year), color=COALESCE(?, color), capacity=COALESCE(?, capacity), + vehicle_type=COALESCE(?, vehicle_type), notes=COALESCE(?, notes) + WHERE id=?" +)->execute([ + $plate, filterRequest('make'), filterRequest('model'), + filterRequest('year', 'int'), filterRequest('color'), filterRequest('capacity', 'int'), + $vType, filterRequest('notes'), + $vehicleId, +]); + +jsonSuccess(['vehicle_id' => $vehicleId], 'Vehicle updated'); diff --git a/siro_driver/lib/controller/transit/transit_driver_controller.dart b/siro_driver/lib/controller/transit/transit_driver_controller.dart index d7c5c669..a34679f1 100644 --- a/siro_driver/lib/controller/transit/transit_driver_controller.dart +++ b/siro_driver/lib/controller/transit/transit_driver_controller.dart @@ -3,8 +3,8 @@ import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart' show LatLng; +import '../../views/widgets/error_snakbar.dart'; import '../functions/location_controller.dart'; -import '../widgets/error_snakbar.dart'; import 'transit_driver_models.dart'; import 'transit_driver_service.dart'; @@ -41,7 +41,8 @@ class TransitDriverController extends GetxController { if (res.success && res.data != null) { isBusDriver = res.data!['is_bus_driver'] == true; if (isBusDriver) { - driverTransitId = int.tryParse(res.data!['driver_transit_id'].toString()); + driverTransitId = + int.tryParse(res.data!['driver_transit_id'].toString()); orgName = res.data!['org_name']?.toString() ?? ''; await fetchTodayTrips(); } @@ -184,8 +185,10 @@ class TransitDriverController extends GetxController { for (final stop in stops) { if (stop.sequence <= _lastAnnouncedStopSeq) continue; final dist = Geolocator.distanceBetween( - pos.latitude, pos.longitude, - stop.latitude, stop.longitude, + pos.latitude, + pos.longitude, + stop.latitude, + stop.longitude, ); if (dist <= stop.geofenceRadius) { nextSeq = stop.sequence; diff --git a/transit_dashboard/.env.example b/transit_dashboard/.env.example new file mode 100644 index 00000000..8a6a632f --- /dev/null +++ b/transit_dashboard/.env.example @@ -0,0 +1,3 @@ +# رابط الباك اند — يشير إلى backend/transit عبر connect_admin.php +# VITE_API_BASE=https://api.siromove.com/transit +VITE_API_BASE=https://jordan-siro.intaleqapp.com diff --git a/transit_dashboard/.gitignore b/transit_dashboard/.gitignore new file mode 100644 index 00000000..01473fa9 --- /dev/null +++ b/transit_dashboard/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.env +*.local diff --git a/transit_dashboard/README.md b/transit_dashboard/README.md new file mode 100644 index 00000000..964fb4a9 --- /dev/null +++ b/transit_dashboard/README.md @@ -0,0 +1,39 @@ +# لوحة مشرف المؤسسة — مواصلاتي + +واجهة ويب (Vue 3 + Vite) يستخدمها مشرف الجامعة/الفندق/الشركة لإدارة أسطول +الباصات، الخطوط، الجداول، الطلاب، والإعلانات. تتصل حصراً بـ +`backend/transit/*` عبر بوابة `connect_admin.php` (مصادقة هاتف + OTP، جلسة +Bearer صالحة 24 ساعة). + +## التشغيل محلياً + +```bash +cp .env.example .env # اضبط VITE_API_BASE على رابط الباك اند +npm install +npm run dev # http://localhost:5183 +``` + +## البناء للإنتاج + +```bash +npm run build # يُنتج dist/ +``` + +انشر محتوى `dist/` على `transit.siromove.com` (أو `admin.siromove.com` — كلا +الأصلين مضبوطان في CORS ضمن `backend/transit/connect_admin.php` عبر متغير +البيئة `TRANSIT_ADMIN_ORIGINS`). + +## البنية + +- `src/api/` — عميل HTTP (`client.js`) وكل نداءات `transit/*` (`transit.js`) +- `src/stores/auth.js` — جلسة المشرف (Pinia) وتخزين التوكن في `localStorage` +- `src/components/MapRouteBuilder.vue` — منشئ خطوط بخريطة Leaflet (نقر لإضافة + محطة بالترتيب، سحب لتعديل الموقع) — لا يتطلب مفتاح API (OpenStreetMap) +- `src/views/RouteEditorView.vue` — محرر خط كامل (بيانات + محطات + جداول) +- الأيام في الجداول الزمنية: قناع بت (`bit0=أحد … bit6=سبت`، 62=الأحد–الخميس) + +## ملاحظات أمنية + +- لا يوجد أي اتصال مباشر بقواعد main/ride/tracking — فقط `siroTransitDb` عبر + الباك اند. +- التوكن يُحذف تلقائياً من `localStorage` عند استجابة 401 من الخادم. diff --git a/transit_dashboard/index.html b/transit_dashboard/index.html new file mode 100644 index 00000000..d7debea8 --- /dev/null +++ b/transit_dashboard/index.html @@ -0,0 +1,19 @@ + + + + + + مواصلاتي — لوحة المؤسسة + + + + + + +
+ + + diff --git a/transit_dashboard/package-lock.json b/transit_dashboard/package-lock.json new file mode 100644 index 00000000..b42f1bcb --- /dev/null +++ b/transit_dashboard/package-lock.json @@ -0,0 +1,1203 @@ +{ + "name": "transit-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "transit-dashboard", + "version": "1.0.0", + "dependencies": { + "leaflet": "^1.9.4", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.2.8" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/transit_dashboard/package.json b/transit_dashboard/package.json new file mode 100644 index 00000000..b00b7ba2 --- /dev/null +++ b/transit_dashboard/package.json @@ -0,0 +1,21 @@ +{ + "name": "transit-dashboard", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "pinia": "^2.1.7", + "leaflet": "^1.9.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.2.8" + } +} diff --git a/transit_dashboard/src/App.vue b/transit_dashboard/src/App.vue new file mode 100644 index 00000000..98240aef --- /dev/null +++ b/transit_dashboard/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/transit_dashboard/src/api/client.js b/transit_dashboard/src/api/client.js new file mode 100644 index 00000000..5478681d --- /dev/null +++ b/transit_dashboard/src/api/client.js @@ -0,0 +1,67 @@ +// api/client.js — عميل HTTP رفيع لبوابة transit/connect_admin.php +// كل الردود بنمط {status:'success'|'failure', message: ...} + +const API_BASE = import.meta.env.VITE_API_BASE || 'https://api.siromove.com/transit' +const TOKEN_KEY = 'transit_admin_token' + +export function getToken() { + return localStorage.getItem(TOKEN_KEY) || '' +} +export function setToken(token) { + if (token) localStorage.setItem(TOKEN_KEY, token) + else localStorage.removeItem(TOKEN_KEY) +} + +class ApiError extends Error { + constructor(message, status) { + super(message) + this.status = status + } +} + +async function request(path, body, { isForm = false } = {}) { + const headers = {} + const token = getToken() + if (token) headers['Authorization'] = `Bearer ${token}` + + let payload + if (isForm) { + payload = body // already FormData + } else { + headers['Content-Type'] = 'application/x-www-form-urlencoded' + const params = new URLSearchParams() + Object.entries(body || {}).forEach(([k, v]) => { + if (v !== undefined && v !== null) params.append(k, v) + }) + payload = params + } + + let res + try { + res = await fetch(`${API_BASE}/${path}`, { method: 'POST', headers, body: payload }) + } catch (e) { + throw new ApiError('تعذّر الاتصال بالخادم — تحقق من اتصالك بالإنترنت', 0) + } + + if (res.status === 401) { + setToken(null) + throw new ApiError('انتهت الجلسة — سجّل الدخول مجدداً', 401) + } + + let json + try { + json = await res.json() + } catch (e) { + throw new ApiError('استجابة غير صالحة من الخادم', res.status) + } + + if (json.status !== 'success') { + const msg = typeof json.message === 'string' ? json.message : 'حدث خطأ، حاول مجدداً' + throw new ApiError(msg, res.status) + } + + return json.message +} + +export const api = { request } +export { ApiError } diff --git a/transit_dashboard/src/api/transit.js b/transit_dashboard/src/api/transit.js new file mode 100644 index 00000000..b80b775f --- /dev/null +++ b/transit_dashboard/src/api/transit.js @@ -0,0 +1,54 @@ +// api/transit.js — كل نداءات backend/transit/* (بوابة connect_admin.php) +import { api } from './client' + +// ── الدخول ────────────────────────────────────────────── +export const loginRequest = (phone) => api.request('admin/login_request.php', { phone }) +export const loginVerify = (phone, otp) => api.request('admin/login_verify.php', { phone, otp }) +export const logout = () => api.request('admin/logout.php', {}) + +// ── لوحة اليوم ────────────────────────────────────────── +export const getDashboard = () => api.request('admin/dashboard.php', {}) + +// ── الأسطول: مركبات ───────────────────────────────────── +export const listVehicles = () => api.request('vehicle/list.php', {}) +export const addVehicle = (v) => api.request('vehicle/add.php', v) +export const updateVehicle = (v) => api.request('vehicle/update.php', v) +export const toggleVehicle = (vehicle_id, is_active) => + api.request('vehicle/toggle.php', { vehicle_id, is_active: is_active ? 1 : 0 }) + +// ── الأسطول: سائقون ────────────────────────────────────── +export const listDrivers = (status = 'all') => api.request('driver/list.php', { status }) +export const inviteDriver = (d) => api.request('driver/invite.php', d) +export const suspendDriver = (driver_id, action) => api.request('driver/suspend.php', { driver_id, action }) + +// ── الخطوط ────────────────────────────────────────────── +export const listRoutes = (status = 'all') => api.request('route/list.php', { status }) +export const getRoute = (route_id) => api.request('route/get.php', { route_id }) +export const addRoute = (payload) => + api.request('route/add.php', { ...payload, stops: JSON.stringify(payload.stops || []) }) +export const updateRoute = (payload) => + api.request('route/update.php', { ...payload, stops: JSON.stringify(payload.stops || []) }) + +// ── الجداول ───────────────────────────────────────────── +export const listSchedules = (route_id) => api.request('schedule/list.php', { route_id }) +export const addSchedule = (s) => api.request('schedule/add.php', s) +export const deleteSchedule = (schedule_id) => api.request('schedule/delete.php', { schedule_id }) + +// ── الطلاب / الأعضاء ──────────────────────────────────── +export const listEnrollments = (status = 'all', page = 1) => + api.request('enrollment/list.php', { status, page }) +export const approveEnrollment = (enrollment_id, action, expires_at) => + api.request('enrollment/approve.php', { enrollment_id, action, expires_at }) +export const listRosters = () => api.request('enrollment/rosters_list.php', {}) +export const importRoster = (file, semester) => { + const fd = new FormData() + fd.append('file', file) + if (semester) fd.append('semester', semester) + return api.request('enrollment/import_roster.php', fd, { isForm: true }) +} + +// ── الرحلات ───────────────────────────────────────────── +export const cancelTrip = (trip_id, reason) => api.request('trip/cancel.php', { trip_id, reason }) + +// ── الإعلانات ─────────────────────────────────────────── +export const sendBroadcast = (b) => api.request('broadcast/send.php', b) diff --git a/transit_dashboard/src/components/AppShell.vue b/transit_dashboard/src/components/AppShell.vue new file mode 100644 index 00000000..a70856dc --- /dev/null +++ b/transit_dashboard/src/components/AppShell.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/transit_dashboard/src/components/ConfirmDialog.vue b/transit_dashboard/src/components/ConfirmDialog.vue new file mode 100644 index 00000000..56a74097 --- /dev/null +++ b/transit_dashboard/src/components/ConfirmDialog.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/transit_dashboard/src/components/DayMaskPicker.vue b/transit_dashboard/src/components/DayMaskPicker.vue new file mode 100644 index 00000000..6314d6d9 --- /dev/null +++ b/transit_dashboard/src/components/DayMaskPicker.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/transit_dashboard/src/components/MapRouteBuilder.vue b/transit_dashboard/src/components/MapRouteBuilder.vue new file mode 100644 index 00000000..504e9ee0 --- /dev/null +++ b/transit_dashboard/src/components/MapRouteBuilder.vue @@ -0,0 +1,110 @@ + + + + + + + diff --git a/transit_dashboard/src/components/StatCard.vue b/transit_dashboard/src/components/StatCard.vue new file mode 100644 index 00000000..73febfc7 --- /dev/null +++ b/transit_dashboard/src/components/StatCard.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/transit_dashboard/src/main.js b/transit_dashboard/src/main.js new file mode 100644 index 00000000..6d31d5f8 --- /dev/null +++ b/transit_dashboard/src/main.js @@ -0,0 +1,7 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import './styles/main.css' + +createApp(App).use(createPinia()).use(router).mount('#app') diff --git a/transit_dashboard/src/router/index.js b/transit_dashboard/src/router/index.js new file mode 100644 index 00000000..4aecaea7 --- /dev/null +++ b/transit_dashboard/src/router/index.js @@ -0,0 +1,32 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '../stores/auth' + +const routes = [ + { path: '/login', name: 'login', component: () => import('../views/LoginView.vue'), meta: { public: true } }, + { + path: '/', + component: () => import('../components/AppShell.vue'), + children: [ + { path: '', redirect: '/dashboard' }, + { path: 'dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue') }, + { path: 'fleet', name: 'fleet', component: () => import('../views/FleetView.vue') }, + { path: 'routes', name: 'routes', component: () => import('../views/RoutesView.vue') }, + { path: 'routes/:id', name: 'route-editor', component: () => import('../views/RouteEditorView.vue'), props: true }, + { path: 'routes/new', name: 'route-new', component: () => import('../views/RouteEditorView.vue') }, + { path: 'students', name: 'students', component: () => import('../views/StudentsView.vue') }, + { path: 'broadcasts', name: 'broadcasts', component: () => import('../views/BroadcastsView.vue') }, + ], + }, + { path: '/:pathMatch(.*)*', redirect: '/dashboard' }, +] + +const router = createRouter({ history: createWebHistory(), routes }) + +router.beforeEach((to) => { + const auth = useAuthStore() + if (!to.meta.public && !auth.isLoggedIn) return '/login' + if (to.name === 'login' && auth.isLoggedIn) return '/dashboard' + return true +}) + +export default router diff --git a/transit_dashboard/src/stores/auth.js b/transit_dashboard/src/stores/auth.js new file mode 100644 index 00000000..573a4de0 --- /dev/null +++ b/transit_dashboard/src/stores/auth.js @@ -0,0 +1,35 @@ +import { defineStore } from 'pinia' +import { getToken, setToken } from '../api/client' +import * as transitApi from '../api/transit' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: getToken(), + admin: JSON.parse(localStorage.getItem('transit_admin_profile') || 'null'), + org: JSON.parse(localStorage.getItem('transit_admin_org') || 'null'), + }), + getters: { + isLoggedIn: (state) => !!state.token, + }, + actions: { + async verifyOtp(phone, otp) { + const res = await transitApi.loginVerify(phone, otp) + this.token = res.token + this.admin = res.admin + this.org = res.org + setToken(res.token) + localStorage.setItem('transit_admin_profile', JSON.stringify(res.admin)) + localStorage.setItem('transit_admin_org', JSON.stringify(res.org)) + return res + }, + async logout() { + try { await transitApi.logout() } catch (e) { /* الجلسة قد تكون منتهية أصلاً */ } + this.token = '' + this.admin = null + this.org = null + setToken(null) + localStorage.removeItem('transit_admin_profile') + localStorage.removeItem('transit_admin_org') + }, + }, +}) diff --git a/transit_dashboard/src/styles/main.css b/transit_dashboard/src/styles/main.css new file mode 100644 index 00000000..c8c2b871 --- /dev/null +++ b/transit_dashboard/src/styles/main.css @@ -0,0 +1,168 @@ +/* ============================================================ + مواصلاتي — لوحة المؤسسة: نظام تصميم + هوية هادئة وواثقة لأداة تشغيلية يومية (ليست صفحة تسويقية) + ============================================================ */ + +:root { + --bg: #F4F6F9; + --surface: #FFFFFF; + --surface-2: #EAEEF3; + --ink: #101826; + --ink-soft: #4A5568; + --ink-faint: #8896A6; + --line: #E1E6ED; + --line-strong: #C9D1DD; + + --brand: #0E7C86; /* أزرق-تيل الميناء — هوية اللوحة الخاصة، مختلفة عن سيرو الأساسي */ + --brand-deep: #0A5F67; + --brand-soft: #E1F2F3; + + --good: #1F9D6B; + --good-soft: #E3F6EC; + --warn: #C9821D; + --warn-soft: #FBF0DD; + --bad: #C6473D; + --bad-soft: #FAE6E3; + --info: #4A6FA5; + --info-soft: #E7EDF7; + + --shadow-sm: 0 1px 2px rgba(16,24,38,.05); + --shadow-md: 0 4px 16px rgba(16,24,38,.08); + --shadow-lg: 0 12px 32px rgba(16,24,38,.14); + --r-sm: 8px; + --r-md: 12px; + --r-lg: 16px; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0C1118; + --surface: #141B25; + --surface-2: #1B2430; + --ink: #EAF0F6; + --ink-soft: #9DAAB9; + --ink-faint: #64707F; + --line: #253140; + --line-strong: #334154; + + --brand: #29B6C2; + --brand-deep: #1D8E97; + --brand-soft: #102A2C; + + --good: #3FC489; + --good-soft: #10281F; + --warn: #E3A53D; + --warn-soft: #2E2410; + --bad: #E5695D; + --bad-soft: #2E1714; + --info: #7C9CCF; + --info-soft: #16202F; + + --shadow-sm: 0 1px 2px rgba(0,0,0,.3); + --shadow-md: 0 6px 20px rgba(0,0,0,.35); + --shadow-lg: 0 16px 40px rgba(0,0,0,.5); + } +} + +:root[data-theme="light"] { + --bg: #F4F6F9; --surface: #FFFFFF; --surface-2: #EAEEF3; --ink: #101826; + --ink-soft: #4A5568; --ink-faint: #8896A6; --line: #E1E6ED; --line-strong: #C9D1DD; + --brand: #0E7C86; --brand-deep: #0A5F67; --brand-soft: #E1F2F3; + --good: #1F9D6B; --good-soft: #E3F6EC; --warn: #C9821D; --warn-soft: #FBF0DD; + --bad: #C6473D; --bad-soft: #FAE6E3; --info: #4A6FA5; --info-soft: #E7EDF7; +} +:root[data-theme="dark"] { + --bg: #0C1118; --surface: #141B25; --surface-2: #1B2430; --ink: #EAF0F6; + --ink-soft: #9DAAB9; --ink-faint: #64707F; --line: #253140; --line-strong: #334154; + --brand: #29B6C2; --brand-deep: #1D8E97; --brand-soft: #102A2C; + --good: #3FC489; --good-soft: #10281F; --warn: #E3A53D; --warn-soft: #2E2410; + --bad: #E5695D; --bad-soft: #2E1714; --info: #7C9CCF; --info-soft: #16202F; +} + +* { box-sizing: border-box; } +html, body, #app { height: 100%; } +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: "Cairo", "Segoe UI", Tahoma, sans-serif; + direction: rtl; + -webkit-font-smoothing: antialiased; +} +.num { font-family: "IBM Plex Mono", ui-monospace, monospace; font-variant-numeric: tabular-nums; direction: ltr; unicode-bidi: isolate; } + +a { color: inherit; } +button { font-family: inherit; } + +h1, h2, h3, h4 { margin: 0; text-wrap: balance; font-weight: 700; } + +/* ─── Buttons ──────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + font-family: inherit; font-size: 14px; font-weight: 600; + padding: 10px 18px; border-radius: var(--r-sm); border: 1px solid transparent; + cursor: pointer; transition: background .15s ease, border-color .15s ease, opacity .15s ease; + white-space: nowrap; +} +.btn:disabled { opacity: .5; cursor: not-allowed; } +.btn-primary { background: var(--brand); color: white; } +.btn-primary:hover:not(:disabled) { background: var(--brand-deep); } +.btn-ghost { background: transparent; border-color: var(--line-strong); color: var(--ink); } +.btn-ghost:hover:not(:disabled) { background: var(--surface-2); } +.btn-danger { background: var(--bad); color: white; } +.btn-danger:hover:not(:disabled) { opacity: .88; } +.btn-sm { padding: 6px 12px; font-size: 13px; } + +/* ─── Form controls ────────────────────────────────────── */ +.field { display: flex; flex-direction: column; gap: 6px; } +.field label { font-size: 13px; font-weight: 600; color: var(--ink-soft); } +.input, select.input, textarea.input { + font-family: inherit; font-size: 14.5px; color: var(--ink); + background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-sm); + padding: 10px 12px; outline: none; transition: border-color .15s ease; +} +.input:focus { border-color: var(--brand); } +.input::placeholder { color: var(--ink-faint); } + +/* ─── Cards / surfaces ─────────────────────────────────── */ +.card { + background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); + box-shadow: var(--shadow-sm); +} +.card-pad { padding: 20px; } + +/* ─── Pills / badges ───────────────────────────────────── */ +.pill { + display: inline-flex; align-items: center; gap: 6px; + font-size: 12px; font-weight: 700; padding: 4px 11px; border-radius: 100px; + white-space: nowrap; +} +.pill-good { color: var(--good); background: var(--good-soft); } +.pill-warn { color: var(--warn); background: var(--warn-soft); } +.pill-bad { color: var(--bad); background: var(--bad-soft); } +.pill-info { color: var(--info); background: var(--info-soft); } +.pill-neutral { color: var(--ink-soft); background: var(--surface-2); } + +/* ─── Tables ────────────────────────────────────────────── */ +.table-wrap { overflow-x: auto; border-radius: var(--r-md); border: 1px solid var(--line); } +table.tbl { width: 100%; border-collapse: collapse; font-size: 14px; background: var(--surface); } +table.tbl th { + text-align: right; font-size: 12px; font-weight: 700; color: var(--ink-faint); + text-transform: uppercase; letter-spacing: .04em; padding: 12px 16px; + background: var(--surface-2); border-bottom: 1px solid var(--line); +} +table.tbl td { padding: 13px 16px; border-bottom: 1px solid var(--line); color: var(--ink); } +table.tbl tr:last-child td { border-bottom: none; } +table.tbl tbody tr:hover { background: var(--surface-2); } + +/* ─── Scrollbar (subtle) ───────────────────────────────── */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 100px; } +::-webkit-scrollbar-track { background: transparent; } + +/* ─── Focus visibility ─────────────────────────────────── */ +:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; } + +@media (prefers-reduced-motion: reduce) { + * { transition: none !important; animation: none !important; } +} diff --git a/transit_dashboard/src/utils/polyline.js b/transit_dashboard/src/utils/polyline.js new file mode 100644 index 00000000..8d70ed1f --- /dev/null +++ b/transit_dashboard/src/utils/polyline.js @@ -0,0 +1,40 @@ +// utils/polyline.js — ترميز Google Polyline القياسي (يطابق PolylineUtils.decode في تطبيقات الفلاتر) + +export function encodePolyline(points) { + let output = '' + let prevLat = 0 + let prevLng = 0 + + for (const [lat, lng] of points) { + const lat5 = Math.round(lat * 1e5) + const lng5 = Math.round(lng * 1e5) + output += encodeValue(lat5 - prevLat) + output += encodeValue(lng5 - prevLng) + prevLat = lat5 + prevLng = lng5 + } + return output +} + +function encodeValue(value) { + let v = value < 0 ? ~(value << 1) : value << 1 + let output = '' + while (v >= 0x20) { + output += String.fromCharCode((0x20 | (v & 0x1f)) + 63) + v >>= 5 + } + output += String.fromCharCode(v + 63) + return output +} + +// هافرسين — مسافة بالمتر بين نقطتين +export function haversineMeters(lat1, lng1, lat2, lng2) { + const R = 6371000 + const dLat = deg2rad(lat2 - lat1) + const dLng = deg2rad(lng2 - lng1) + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLng / 2) ** 2 + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) +} +function deg2rad(d) { return (d * Math.PI) / 180 } diff --git a/transit_dashboard/src/views/BroadcastsView.vue b/transit_dashboard/src/views/BroadcastsView.vue new file mode 100644 index 00000000..31aaf433 --- /dev/null +++ b/transit_dashboard/src/views/BroadcastsView.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/transit_dashboard/src/views/DashboardView.vue b/transit_dashboard/src/views/DashboardView.vue new file mode 100644 index 00000000..f9dcf684 --- /dev/null +++ b/transit_dashboard/src/views/DashboardView.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/transit_dashboard/src/views/FleetView.vue b/transit_dashboard/src/views/FleetView.vue new file mode 100644 index 00000000..db9a7b4a --- /dev/null +++ b/transit_dashboard/src/views/FleetView.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/transit_dashboard/src/views/LoginView.vue b/transit_dashboard/src/views/LoginView.vue new file mode 100644 index 00000000..ca17a3d7 --- /dev/null +++ b/transit_dashboard/src/views/LoginView.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/transit_dashboard/src/views/RouteEditorView.vue b/transit_dashboard/src/views/RouteEditorView.vue new file mode 100644 index 00000000..d20034c7 --- /dev/null +++ b/transit_dashboard/src/views/RouteEditorView.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/transit_dashboard/src/views/RoutesView.vue b/transit_dashboard/src/views/RoutesView.vue new file mode 100644 index 00000000..19fbf844 --- /dev/null +++ b/transit_dashboard/src/views/RoutesView.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/transit_dashboard/src/views/StudentsView.vue b/transit_dashboard/src/views/StudentsView.vue new file mode 100644 index 00000000..6f3143ad --- /dev/null +++ b/transit_dashboard/src/views/StudentsView.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/transit_dashboard/vite.config.js b/transit_dashboard/vite.config.js new file mode 100644 index 00000000..5cf28e01 --- /dev/null +++ b/transit_dashboard/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { port: 5183 }, +})