Update: 2026-08-07 04:35:28
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?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);
|
||||
}
|
||||
Reference in New Issue
Block a user