Update: 2026-07-13 22:23:21
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
<?php
|
||||
// backend/transit/cron_approaching_alerts.php
|
||||
// يُنفذ كل دقيقة عبر Cron Job لإرسال تنبيهات اقتراب الباص من المحطات (أقل من 2 كم)
|
||||
//
|
||||
// ملاحظة أداء: الاستعلام الأول رخيص (فهرس idx على status)، ولكل رحلة نشطة
|
||||
// فقط استعلامان إضافيان — ويُتجاهل فوراً أي رحلة بلا موقع GPS حديث (< 3 دقائق)
|
||||
// لتفادي ضرب القاعدة لرحلات متوقفة أو باصات فقدت الاتصال.
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/connect_transit.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
try {
|
||||
$transit_con = Database::get('transit');
|
||||
} catch (Exception $e) {
|
||||
echo "[ERROR] Cannot connect to siroTransitDb: " . $e->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'];
|
||||
|
||||
// حساب المسافة تقريبياً
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// transit/driver/suspend.php — تعليق/إعادة تفعيل سائق
|
||||
// POST: driver_id, action (suspend|activate)
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$driverId = filterRequest('driver_id', 'int');
|
||||
$action = filterRequest('action');
|
||||
if (!$driverId) jsonError('driver_id is required', 400);
|
||||
if (!in_array($action, ['suspend', 'activate'])) jsonError('action must be suspend or activate', 400);
|
||||
|
||||
$chk = $transit_con->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]);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// transit/enrollment/rosters_list.php — سجل كشوف الطلاب المستوردة للمؤسسة
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$st = $transit_con->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()]);
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
// transit/route/update.php — تعديل خط قائم (اسم/اتجاه/مسار/محطات)
|
||||
// POST: route_id, name_ar, [name_en, direction, polyline, distance_km, duration_min], stops (JSON)
|
||||
//
|
||||
// أي تعديل على خط كان active أو suspended يُعيده إلى draft — يتطلب اعتماداً
|
||||
// جديداً من فريق سيرو (نفس مسار Admin/transit/route/approve.php)
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$routeId = filterRequest('route_id', 'int');
|
||||
if (!$routeId) jsonError('route_id is required', 400);
|
||||
|
||||
$nameAr = filterRequest('name_ar');
|
||||
if (!$nameAr) jsonError('name_ar is required', 400);
|
||||
|
||||
$direction = filterRequest('direction') ?: 'outbound';
|
||||
if (!in_array($direction, ['outbound','inbound','circular'])) $direction = 'outbound';
|
||||
|
||||
$stops = json_decode(filterRequest('stops') ?? '[]', true) ?: [];
|
||||
|
||||
$chk = $transit_con->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');
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// transit/schedule/delete.php — حذف جدول زمني
|
||||
// POST: schedule_id
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$scheduleId = filterRequest('schedule_id', 'int');
|
||||
if (!$scheduleId) jsonError('schedule_id is required', 400);
|
||||
|
||||
$chk = $transit_con->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');
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// transit/schedule/list.php — جداول خط معيّن
|
||||
// POST: route_id
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$routeId = filterRequest('route_id', 'int');
|
||||
if (!$routeId) jsonError('route_id is required', 400);
|
||||
|
||||
$chk = $transit_con->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()]);
|
||||
@@ -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 دائماً)
|
||||
-- -----------------------------------------------------------------
|
||||
|
||||
-- -----------------------------------------------------------------
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// transit/trip/cancel.php — المشرف يلغي رحلة (قبل بدئها عادة — عطل مركبة، غياب سائق...)
|
||||
// POST: trip_id, [reason]
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$tripId = filterRequest('trip_id', 'int');
|
||||
if (!$tripId) jsonError('trip_id is required', 400);
|
||||
|
||||
$st = $transit_con->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');
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// transit/vehicle/toggle.php — تفعيل/تعطيل مركبة
|
||||
// POST: vehicle_id, is_active (1|0)
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$vehicleId = filterRequest('vehicle_id', 'int');
|
||||
$isActive = filterRequest('is_active', 'int');
|
||||
if (!$vehicleId || $isActive === null) jsonError('vehicle_id and is_active are required', 400);
|
||||
|
||||
$chk = $transit_con->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]);
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// transit/vehicle/update.php — تعديل بيانات مركبة
|
||||
// POST: vehicle_id, [plate, make, model, year, color, capacity, vehicle_type, notes]
|
||||
|
||||
require_once __DIR__ . '/../../transit/connect_admin.php';
|
||||
|
||||
$vehicleId = filterRequest('vehicle_id', 'int');
|
||||
if (!$vehicleId) jsonError('vehicle_id is required', 400);
|
||||
|
||||
$chk = $transit_con->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');
|
||||
Reference in New Issue
Block a user