Files
Siro/backend/safety/incident.php
T

161 lines
8.0 KiB
PHP

<?php
// ============================================================
// safety/incident.php — تسجيل بلاغ سلامة
//
// ‏كان زر SOS ينتهي عند رسالة واتساب لجهة اتصال شخصية. الشركة لا تعلم،
// ‏ولا تملك دليلاً، ولا تستطيع التدخل. هذه النقطة هي الأثر المفقود.
//
// ‏رسالة الواتساب تبقى كما هي: التطبيق يرسلها **بالتوازي** لا بدلاً —
// ‏هي الأسرع وصولاً لمن يهمّه أمر الراكب فعلاً، وهذه النقطة للشركة.
//
// ‏ثلاثة أشياء تحدث هنا، بهذا الترتيب عمداً:
// ١. تسجيل البلاغ ولقطة الحالة ← الأهم، ولا يعتمد على شيء
// ٢. تجميد إسناد الرحلات للسائق ← يمنع تكرار الواقعة فوراً
// ٣. تنبيه الإدارة ← أفضل جهد، لا يُسقط ما قبله
// ============================================================
require_once __DIR__ . '/../connect.php';
$reporterType = filterRequest("reporter_type") ?: 'passenger';
$reporterId = filterRequest("reporter_id");
$rideId = filterRequest("ride_id");
$triggerType = filterRequest("trigger_type") ?: 'sos_button';
$note = filterRequest("note");
$lat = filterRequest("lat");
$lng = filterRequest("lng");
if (!$reporterId) {
jsonError("Missing reporter_id");
}
// ‏قائمة بيضاء: القيم تُخزَّن وتُعرض في لوحة الإدارة، ولا نريد نصاً حراً.
$allowedReporters = ['passenger', 'driver'];
$allowedTriggers = ['sos_button', 'shake', 'route_deviation', 'long_stop', 'overtime'];
if (!in_array($reporterType, $allowedReporters, true)) $reporterType = 'passenger';
if (!in_array($triggerType, $allowedTriggers, true)) $triggerType = 'sos_button';
$driverId = null;
$passengerId = null;
$snapshot = ['reported_at' => date('c'), 'trigger' => $triggerType];
try {
// ── ١. لقطة الحالة ──────────────────────────────────────
// ‏تُلتقط الآن لأن الرحلة تتغيّر بعد الحادثة (تُلغى، يُصفَّر السائق)،
// ‏فقراءتها لاحقاً لا تصف اللحظة التي وقع فيها البلاغ.
if ($rideId) {
$stmt = $con->prepare("SELECT * FROM ride WHERE id = ? LIMIT 1");
$stmt->execute([$rideId]);
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
if ($ride) {
$driverId = ($ride['driver_id'] ?? '0') !== '0' ? $ride['driver_id'] : null;
$passengerId = $ride['passenger_id'] ?? null;
$snapshot['ride'] = [
'status' => $ride['status'] ?? null,
'start_location' => $ride['start_location'] ?? null,
'end_location' => $ride['end_location'] ?? null,
'carType' => $ride['carType'] ?? $ride['car_type'] ?? null,
'price' => $ride['price'] ?? null,
'rideTimeStart' => $ride['rideTimeStart'] ?? null,
];
}
}
// ‏آخر موقع معروف للسائق من Redis — أدق من أي شيء في MySQL، ويجيب
// ‏على السؤال الأول الذي تسأله غرفة العمليات: أين هم الآن؟
if ($driverId && isset($redisLocation) && $redisLocation) {
try {
$loc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($loc)) {
$snapshot['driver_location'] = [
'lat' => $loc['lat'] ?? null,
'lng' => $loc['lng'] ?? null,
'heading' => $loc['heading'] ?? null,
'speed' => $loc['speed'] ?? null,
'updated_at' => $loc['updated_at'] ?? null,
];
}
} catch (Throwable $e) {
error_log("[safety] تعذّرت قراءة موقع السائق: " . $e->getMessage());
}
}
// ‏موقع المبلِّغ كما أرسله التطبيق. نخزّنه كما هو ولا نبني عليه قراراً —
// ‏قيمة قادمة من العميل، لكنها في بلاغ سلامة أفضل من لا شيء.
if (is_numeric($lat) && is_numeric($lng)) {
$snapshot['reporter_location'] = ['lat' => (float) $lat, 'lng' => (float) $lng];
}
// ── ٢. تسجيل البلاغ ─────────────────────────────────────
$ins = $con->prepare("
INSERT INTO safety_incidents
(reporter_type, reporter_id, trigger_type, ride_id,
driver_id, passenger_id, lat, lng, note, snapshot)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$ins->execute([
$reporterType, $reporterId, $triggerType, $rideId ?: null,
$driverId, $passengerId,
is_numeric($lat) ? $lat : null,
is_numeric($lng) ? $lng : null,
$note ? mb_substr($note, 0, 500) : null,
json_encode($snapshot, JSON_UNESCAPED_UNICODE),
]);
$incidentId = (int) $con->lastInsertId();
error_log("[safety] بلاغ #$incidentId — $triggerType من $reporterType"
. " $reporterId (رحلة=" . ($rideId ?: '-') . " سائق=" . ($driverId ?: '-') . ")");
} catch (PDOException $e) {
error_log("[safety] فشل تسجيل البلاغ: " . $e->getMessage());
jsonError("Failed to record incident", 500);
}
// ── ٣. تجميد إسناد الرحلات للسائق ───────────────────────────
// ‏يوقف تكرار الواقعة فوراً. منفصل عن blockedUntil (عقوبة الإلغاءات) الذي
// ‏ينتهي بعد ساعات وحده — هذا يبقى حتى يرفعه إنسان.
// ‏لا يُطبَّق على بلاغ السائق عن نفسه: قد يكون هو المعتدى عليه.
if ($driverId && $reporterType === 'passenger') {
try {
$con->prepare("UPDATE driver SET safetyHoldIncidentId = ? WHERE id = ?")
->execute([$incidentId, $driverId]);
// ‏نسخة Redis هي ما يفحصه acceptRide.php — بلا TTL: التجميد لا ينتهي بالوقت.
if (isset($redisLocation) && $redisLocation) {
$redisLocation->set("driver:$driverId:safety_hold", (string) $incidentId);
}
} catch (Throwable $e) {
error_log("[safety] تعذّر تجميد السائق $driverId: " . $e->getMessage());
}
}
// ── ٤. تنبيه الإدارة ────────────────────────────────────────
// ‏أفضل جهد: البلاغ مسجَّل بالفعل، وفشل التنبيه لا يُسقطه.
try {
if (isset($redis) && $redis) {
// ‏قائمة يقرأها بانل الإدارة — أسرع من استعلام على الجدول عند كل تحديث
$redis->lPush('safety:open_incidents', (string) $incidentId);
$redis->lTrim('safety:open_incidents', 0, 199);
}
if (function_exists('sendFCM_Internal')) {
$topic = getenv('SAFETY_ALERT_TOPIC') ?: 'siro_safety_ops';
sendFCM_Internal(
$topic,
"🚨 بلاغ سلامة #$incidentId",
"بلاغ $triggerType من $reporterType" . ($rideId ? " — رحلة $rideId" : ""),
['category' => 'safety_incident', 'incident_id' => (string) $incidentId],
"safety_incident",
true // topic
);
}
} catch (Throwable $e) {
error_log("[safety] تعذّر تنبيه الإدارة للبلاغ #$incidentId: " . $e->getMessage());
}
jsonSuccess([
'incident_id' => $incidentId,
'driver_frozen' => (bool) ($driverId && $reporterType === 'passenger'),
], "Incident recorded");