Files
Siro/backend/serviceapp/complaint_sla_stats.php

96 lines
4.6 KiB
PHP

<?php
// ============================================================
// serviceapp/complaint_sla_stats.php — قياس دورة الشكوى
//
// ‏دورة بلا قياس تبقى "استقبال شكاوى" لا "إغلاق قضايا". هذه النقطة
// ‏تعطي الأرقام الثلاثة التي تدير بها الفريق:
// • زمن أول رد ← ما يشعر به العميل فعلاً
// • زمن الإغلاق ← كفاءة الفريق
// • المفتوح الآن ← عبء اللحظة
//
// ‏محمية بالبوّابة المسارية في connect.php (كل ما تحت serviceapp/).
// ============================================================
require_once __DIR__ . '/../connect.php';
$days = (int) (filterRequest("days", 'int') ?: 30);
// ‏يُقصّ إلى عدد صحيح ضمن مدى آمن قبل أي إقحام في SQL. INTERVAL لا يقبل
// ‏متغيراً مربوطاً، فالقصّ هنا هو الضمانة الوحيدة ضد الحقن.
$days = max(1, min(365, $days));
try {
// ── الأزمنة ─────────────────────────────────────────────
// ‏المتوسط وحده يخدع: شكوى واحدة أُهملت أسبوعاً ترفعه بينما الأغلبية
// ‏تُعالَج بسرعة. الوسيط التقريبي عبر أقصى قيمة يكمّل الصورة.
$stmt = $con->prepare("
SELECT
COUNT(*) AS total,
SUM(first_response_at IS NOT NULL) AS answered,
ROUND(AVG(TIMESTAMPDIFF(MINUTE, date_filed, first_response_at)), 1) AS avg_first_response_min,
MAX(TIMESTAMPDIFF(MINUTE, date_filed, first_response_at)) AS worst_first_response_min,
SUM(statusComplaint = 'Resolved') AS resolved,
ROUND(AVG(CASE WHEN statusComplaint = 'Resolved'
THEN TIMESTAMPDIFF(MINUTE, date_filed, date_resolved) END), 1) AS avg_resolution_min
FROM complaint
WHERE date_filed >= NOW() - INTERVAL $days DAY
");
$stmt->execute();
$timing = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
// ‏المفتوح الآن بلا نافذة زمنية: شكوى من الشهر الماضي ما زالت مفتوحة
// ‏هي المشكلة، وحصرها في النافذة كان سيخفيها.
$openNow = (int) $con->query(
"SELECT COUNT(*) FROM complaint WHERE statusComplaint <> 'Resolved'"
)->fetchColumn();
$neverAnswered = (int) $con->query(
"SELECT COUNT(*) FROM complaint
WHERE statusComplaint <> 'Resolved' AND first_response_at IS NULL"
)->fetchColumn();
// ── التصنيف ─────────────────────────────────────────────
$stmtReasons = $con->prepare("
SELECT COALESCE(reason_code, 'unclassified') AS reason, COUNT(*) AS cnt
FROM complaint
WHERE date_filed >= NOW() - INTERVAL $days DAY
GROUP BY reason ORDER BY cnt DESC
");
$stmtReasons->execute();
// ── التعويضات ───────────────────────────────────────────
$compensation = ['total' => 0, 'count' => 0, 'failed' => 0];
try {
$stmtComp = $con->prepare("
SELECT
COUNT(*) AS cnt,
COALESCE(SUM(CASE WHEN transfer_status = 'success' THEN amount END), 0) AS total,
SUM(transfer_status = 'failed') AS failed
FROM complaint_compensations
WHERE created_at >= NOW() - INTERVAL $days DAY
");
$stmtComp->execute();
$row = $stmtComp->fetch(PDO::FETCH_ASSOC) ?: [];
$compensation = [
'count' => (int) ($row['cnt'] ?? 0),
'total' => (float) ($row['total'] ?? 0),
// ‏التعويضات الفاشلة أهم رقم هنا: عميل وُعد بمبلغ ولم يصله.
'failed' => (int) ($row['failed'] ?? 0),
];
} catch (PDOException $e) {
error_log("[complaint/sla] جدول التعويضات غير مُرحَّل بعد: " . $e->getMessage());
}
jsonSuccess([
'window_days' => $days,
'timing' => $timing,
'open_now' => $openNow,
'never_answered' => $neverAnswered,
'by_reason' => $stmtReasons->fetchAll(PDO::FETCH_ASSOC),
'compensation' => $compensation,
], "ok");
} catch (PDOException $e) {
error_log("[complaint/sla] " . $e->getMessage());
jsonError("DB Error", 500);
}