96 lines
4.6 KiB
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);
|
|
}
|