Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
// Admin/fuel/drawdowns.php — حركة القسائم وملخّص الائتمان
|
||||
// POST (اختياري): station_id, status, driver_id, limit
|
||||
//
|
||||
// الشاشة التي تُجيب: كم أقرضنا؟ كم استُرد؟ وأي محطة تعمل؟
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
$stationId = (int) (filterRequest('station_id', 'int') ?: 0);
|
||||
$status = filterRequest('status');
|
||||
$driverId = filterRequest('driver_id');
|
||||
$limit = (int) (filterRequest('limit', 'int') ?: 100);
|
||||
$limit = max(1, min($limit, 500));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($stationId > 0) {
|
||||
$where[] = 'v.station_id = ?';
|
||||
$params[] = $stationId;
|
||||
}
|
||||
if (!empty($status)) {
|
||||
$where[] = 'v.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
if (!empty($driverId)) {
|
||||
$where[] = 'v.driver_id = ?';
|
||||
$params[] = $driverId;
|
||||
}
|
||||
|
||||
$clause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
try {
|
||||
$st = $con->prepare("
|
||||
SELECT v.id, v.driver_id, v.code, v.amount_authorized, v.amount_redeemed,
|
||||
v.currency, v.status, v.expires_at, v.redeemed_at, v.created_at,
|
||||
s.name_ar AS station_name
|
||||
FROM fuel_vouchers v
|
||||
LEFT JOIN fuel_stations s ON s.id = v.station_id
|
||||
$clause
|
||||
ORDER BY v.id DESC
|
||||
LIMIT $limit
|
||||
");
|
||||
$st->execute($params);
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// الملخّص على كامل المنتج لا على الصفحة المعروضة: رقمٌ يتغيّر بتغيّر
|
||||
// الفلتر يُقرأ كأنه حقيقة عن المنصة وهو ليس كذلك.
|
||||
$summary = $con->query("
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) AS lent_total,
|
||||
COALESCE(SUM(amount_collected), 0) AS collected_total,
|
||||
COALESCE(SUM(amount_remaining), 0) AS outstanding_total,
|
||||
COUNT(DISTINCT driver_id) AS drivers_count
|
||||
FROM obligation_ledger
|
||||
WHERE product_code = 'fuel_wallet'
|
||||
")->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/drawdowns] ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
|
||||
$vouchers = array_map(static fn(array $r): array => [
|
||||
'id' => (int) $r['id'],
|
||||
'driver_id' => $r['driver_id'],
|
||||
'code' => $r['code'],
|
||||
'amount_authorized' => (float) $r['amount_authorized'],
|
||||
'amount_redeemed' => (float) $r['amount_redeemed'],
|
||||
'currency' => $r['currency'],
|
||||
'status' => $r['status'],
|
||||
'station' => $r['station_name'],
|
||||
'expires_at' => $r['expires_at'],
|
||||
'redeemed_at' => $r['redeemed_at'],
|
||||
'created_at' => $r['created_at'],
|
||||
], $rows);
|
||||
|
||||
jsonSuccess([
|
||||
'vouchers' => $vouchers,
|
||||
'summary' => [
|
||||
'lent_total' => (float) ($summary['lent_total'] ?? 0),
|
||||
'collected_total' => (float) ($summary['collected_total'] ?? 0),
|
||||
'outstanding_total' => (float) ($summary['outstanding_total'] ?? 0),
|
||||
'drivers_count' => (int) ($summary['drivers_count'] ?? 0),
|
||||
],
|
||||
], 'success');
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
// Admin/fuel/list.php — قائمة محطات الوقود الشريكة
|
||||
// POST (اختياري): search, city, only_active
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
$search = filterRequest('search');
|
||||
$city = filterRequest('city');
|
||||
$onlyActive = filterRequest('only_active') === '1';
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
$where[] = '(s.name_ar LIKE ? OR s.chain_name LIKE ? OR s.address LIKE ?)';
|
||||
$like = '%' . $search . '%';
|
||||
array_push($params, $like, $like, $like);
|
||||
}
|
||||
if (!empty($city)) {
|
||||
$where[] = 's.city = ?';
|
||||
$params[] = $city;
|
||||
}
|
||||
if ($onlyActive) {
|
||||
$where[] = 's.is_active = 1';
|
||||
}
|
||||
|
||||
$clause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
try {
|
||||
// إحصاء الصرف مع كل محطة: لوحة بلا أرقام لا تخبر أحداً أي محطة
|
||||
// تعمل فعلاً وأيها اسمٌ في جدول. `redeemed` وحدها — القسائم الملغاة
|
||||
// والمنتهية لا تقول شيئاً عن أداء المحطة.
|
||||
$st = $con->prepare("
|
||||
SELECT s.*,
|
||||
COUNT(v.id) AS redeem_count,
|
||||
COALESCE(SUM(v.amount_redeemed), 0) AS redeem_total,
|
||||
MAX(v.redeemed_at) AS last_redeem_at
|
||||
FROM fuel_stations s
|
||||
LEFT JOIN fuel_vouchers v
|
||||
ON v.station_id = s.id AND v.status = 'redeemed'
|
||||
$clause
|
||||
GROUP BY s.id
|
||||
ORDER BY s.is_active DESC, s.city ASC, s.name_ar ASC
|
||||
LIMIT 200
|
||||
");
|
||||
$st->execute($params);
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/list] ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
|
||||
$stations = array_map(static fn(array $r): array => [
|
||||
'id' => (int) $r['id'],
|
||||
'name_ar' => $r['name_ar'],
|
||||
'chain_name' => $r['chain_name'],
|
||||
'city' => $r['city'],
|
||||
'address' => $r['address'],
|
||||
'lat' => $r['lat'] !== null ? (float) $r['lat'] : null,
|
||||
'lng' => $r['lng'] !== null ? (float) $r['lng'] : null,
|
||||
'phone' => $r['phone'],
|
||||
'discount_percent' => (float) $r['discount_percent'],
|
||||
'is_active' => (int) $r['is_active'] === 1,
|
||||
'created_at' => $r['created_at'],
|
||||
'redeem_count' => (int) $r['redeem_count'],
|
||||
'redeem_total' => (float) $r['redeem_total'],
|
||||
'last_redeem_at' => $r['last_redeem_at'],
|
||||
// المفتاح لا يخرج أبداً — لا هنا ولا في أي نقطة أخرى. مخزَّن مُهشَّراً
|
||||
// أصلاً، ويُعرض مرة واحدة فقط لحظة إنشائه أو تدويره.
|
||||
], $rows);
|
||||
|
||||
// حالة المنتج تُرسل مع القائمة لا في نقطة منفصلة: الشاشة تحتاج أن تعرض
|
||||
// «مُجهَّز وغير مُشغَّل» بوضوح، وطلبان لبناء لافتة واحدة إسراف.
|
||||
$productActive = false;
|
||||
try {
|
||||
$st = $con->prepare("SELECT is_active FROM obligation_products WHERE code = 'fuel_wallet' LIMIT 1");
|
||||
$st->execute();
|
||||
$productActive = (int) $st->fetchColumn() === 1;
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/list] تعذّرت قراءة حالة المنتج: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
jsonSuccess([
|
||||
'stations' => $stations,
|
||||
'product_active' => $productActive,
|
||||
], 'success');
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// Admin/fuel/rotate_key.php — توليد مفتاح جديد لمحطة
|
||||
// POST: station_id
|
||||
//
|
||||
// يُستعمل عند فقدان المحطة مفتاحها أو الشك في تسريبه. المفتاح القديم
|
||||
// يتوقّف فوراً — وهذا مقصود: التدوير إجراء أمني، وإبقاء القديم يعمل
|
||||
// «حتى تنتقل المحطة» يُفرغه من معناه.
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
$stationId = (int) (filterRequest('station_id', 'int') ?: 0);
|
||||
if ($stationId <= 0) {
|
||||
jsonError('station_id مطلوب');
|
||||
}
|
||||
|
||||
try {
|
||||
$st = $con->prepare("SELECT name_ar FROM fuel_stations WHERE id = ? LIMIT 1");
|
||||
$st->execute([$stationId]);
|
||||
$name = $st->fetchColumn();
|
||||
|
||||
if ($name === false) {
|
||||
jsonError('المحطة غير موجودة', 404);
|
||||
}
|
||||
|
||||
$plainKey = bin2hex(random_bytes(24));
|
||||
|
||||
$con->prepare("UPDATE fuel_stations SET api_key_hash = ? WHERE id = ?")
|
||||
->execute([hash('sha256', $plainKey), $stationId]);
|
||||
|
||||
error_log("[admin/fuel] دُوِّر مفتاح المحطة #$stationId بواسطة $user_id");
|
||||
|
||||
jsonSuccess([
|
||||
'station_id' => $stationId,
|
||||
'station' => $name,
|
||||
'api_key' => $plainKey,
|
||||
], 'المفتاح القديم توقّف. انسخ الجديد الآن — لن يظهر مرة أخرى.');
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/rotate_key] ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
// Admin/fuel/save.php — إضافة محطة أو تعديلها
|
||||
// POST: station_id (اختياري للتعديل), name_ar, chain_name, city, address,
|
||||
// lat, lng, phone, discount_percent, is_active
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
$stationId = (int) (filterRequest('station_id', 'int') ?: 0);
|
||||
$nameAr = filterRequest('name_ar');
|
||||
|
||||
if (empty($nameAr)) {
|
||||
jsonError('اسم المحطة مطلوب');
|
||||
}
|
||||
|
||||
$discount = (float) (filterRequest('discount_percent', 'numeric') ?: 0);
|
||||
if ($discount < 0 || $discount > 100) {
|
||||
jsonError('نسبة الخصم يجب أن تكون بين 0 و 100');
|
||||
}
|
||||
|
||||
$lat = filterRequest('lat', 'numeric');
|
||||
$lng = filterRequest('lng', 'numeric');
|
||||
|
||||
$fields = [
|
||||
'name_ar' => $nameAr,
|
||||
'chain_name' => filterRequest('chain_name'),
|
||||
'city' => filterRequest('city'),
|
||||
'address' => filterRequest('address'),
|
||||
'lat' => $lat !== null ? (float) $lat : null,
|
||||
'lng' => $lng !== null ? (float) $lng : null,
|
||||
'phone' => filterRequest('phone'),
|
||||
'discount_percent' => $discount,
|
||||
'is_active' => filterRequest('is_active') === '0' ? 0 : 1,
|
||||
];
|
||||
|
||||
try {
|
||||
if ($stationId > 0) {
|
||||
$sets = implode(', ', array_map(static fn($k) => "`$k` = ?", array_keys($fields)));
|
||||
|
||||
$st = $con->prepare("UPDATE fuel_stations SET $sets WHERE id = ?");
|
||||
$st->execute([...array_values($fields), $stationId]);
|
||||
|
||||
if ($st->rowCount() === 0) {
|
||||
// rowCount صفر قد يعني «غير موجودة» وقد يعني «حُفظت بلا تغيير».
|
||||
// نفرّق بينهما بقراءة صريحة: إخبار المستخدم أن المحطة غير
|
||||
// موجودة بينما هي موجودة يدفعه لإنشاء نسخة ثانية منها.
|
||||
$chk = $con->prepare("SELECT 1 FROM fuel_stations WHERE id = ? LIMIT 1");
|
||||
$chk->execute([$stationId]);
|
||||
if (!$chk->fetchColumn()) {
|
||||
jsonError('المحطة غير موجودة', 404);
|
||||
}
|
||||
}
|
||||
|
||||
error_log("[admin/fuel] عُدّلت المحطة #$stationId بواسطة $user_id");
|
||||
|
||||
jsonSuccess(['station_id' => $stationId, 'created' => false], 'تم حفظ التعديلات');
|
||||
}
|
||||
|
||||
// ── إنشاء جديد ──
|
||||
// المفتاح يُولَّد هنا ولا يُطلب من المستخدم: مفتاح يختاره إنسان يكون
|
||||
// قصيراً ومتوقَّعاً، وهذا المفتاح يأذن بصرف مال.
|
||||
$plainKey = bin2hex(random_bytes(24));
|
||||
|
||||
$cols = array_keys($fields);
|
||||
$st = $con->prepare(
|
||||
'INSERT INTO fuel_stations (`' . implode('`, `', $cols) . '`, `api_key_hash`) '
|
||||
. 'VALUES (' . implode(', ', array_fill(0, count($cols), '?')) . ', ?)'
|
||||
);
|
||||
$st->execute([...array_values($fields), hash('sha256', $plainKey)]);
|
||||
|
||||
$stationId = (int) $con->lastInsertId();
|
||||
|
||||
error_log("[admin/fuel] أُنشئت المحطة #$stationId بواسطة $user_id");
|
||||
|
||||
jsonSuccess([
|
||||
'station_id' => $stationId,
|
||||
'created' => true,
|
||||
// المرة **الوحيدة** التي يظهر فيها المفتاح. مخزَّن مُهشَّراً، فلا
|
||||
// سبيل لاستعادته لاحقاً — من يفقده يدوّر مفتاحاً جديداً.
|
||||
'api_key' => $plainKey,
|
||||
], 'أُنشئت المحطة. انسخ المفتاح الآن — لن يظهر مرة أخرى.');
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/save] ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
// Admin/fuel/toggle_product.php — تشغيل محفظة الوقود أو إيقافها
|
||||
// POST: is_active (1 | 0)
|
||||
//
|
||||
// قرار المالك: تُبنى الشاشات وتُجهَّز الأمور، ولا يُشغَّل المنتج إلا بعد
|
||||
// إتمام الاتفاق مع سلسلة المحطات. هذه هي نقطة ذلك التبديل.
|
||||
//
|
||||
// ما الذي يتغيّر فعلاً؟ المنتج المُطفأ لا يظهر في `obligationProduct`،
|
||||
// فلا يستطيع سائق تفعيل محفظته ولا إصدار قسيمة. أما الالتزامات القائمة
|
||||
// ودفترها فلا تتأثر — الإطفاء يغلق الباب ولا يمحو ما خلفه.
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// super_admin وحده. تشغيل المنتج يفتح باب إقراض حقيقي بمال الشركة،
|
||||
// وهو قرار مالك لا قرار مناوب على اللوحة.
|
||||
if ($role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Super admin access required', 403);
|
||||
}
|
||||
|
||||
$isActive = filterRequest('is_active') === '1' ? 1 : 0;
|
||||
|
||||
try {
|
||||
// التشغيل بلا محطة واحدة فعّالة يعني سائقاً يفعّل محفظته، يطلب
|
||||
// قسيمة، ثم لا يجد أين يصرفها. الرفض هنا أرحم من اكتشافه عند مضخة.
|
||||
if ($isActive === 1) {
|
||||
$st = $con->query("SELECT COUNT(*) FROM fuel_stations WHERE is_active = 1");
|
||||
if ((int) $st->fetchColumn() === 0) {
|
||||
jsonError('لا توجد محطة فعّالة واحدة. أضف محطة قبل تشغيل المنتج.');
|
||||
}
|
||||
}
|
||||
|
||||
$st = $con->prepare("UPDATE obligation_products SET is_active = ? WHERE code = 'fuel_wallet'");
|
||||
$st->execute([$isActive]);
|
||||
|
||||
if ($st->rowCount() === 0) {
|
||||
$chk = $con->query("SELECT 1 FROM obligation_products WHERE code = 'fuel_wallet' LIMIT 1");
|
||||
if (!$chk->fetchColumn()) {
|
||||
jsonError('منتج محفظة الوقود غير موجود — نفّذ الترحيل أولاً', 404);
|
||||
}
|
||||
}
|
||||
|
||||
error_log('[admin/fuel] ' . ($isActive ? 'شُغّلت' : 'أُوقفت') . " محفظة الوقود بواسطة $user_id");
|
||||
|
||||
jsonSuccess(
|
||||
['product_active' => $isActive === 1],
|
||||
$isActive === 1
|
||||
? 'شُغّلت محفظة الوقود — صارت متاحة للسائقين المؤهَّلين'
|
||||
: 'أُوقفت محفظة الوقود — لا تفعيل جديد ولا قسائم جديدة'
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[admin/fuel/toggle_product] ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
Reference in New Issue
Block a user