Update: 2026-07-12 05:40:28
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// Admin/transit/org/admin_add.php — فريق سيرو يضيف مشرفاً جديداً لمؤسسة قائمة
|
||||
// POST: org_id, name, phone, role? (owner|transport_manager|dispatcher)
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
require_once __DIR__ . '/../../../transit/functions.php';
|
||||
|
||||
requireTransitFields(['org_id', 'name', 'phone']);
|
||||
|
||||
$orgId = filterRequest('org_id', 'int');
|
||||
$name = filterRequest('name');
|
||||
$phone = normalizePhone(filterRequest('phone'));
|
||||
$adminRole = filterRequest('role') ?: 'transport_manager';
|
||||
|
||||
$allowedRoles = ['owner', 'transport_manager', 'dispatcher'];
|
||||
if (!in_array($adminRole, $allowedRoles)) jsonError('Invalid role', 400);
|
||||
|
||||
$chkOrg = $transit_con->prepare("SELECT id FROM transit_orgs WHERE id=? LIMIT 1");
|
||||
$chkOrg->execute([$orgId]);
|
||||
if (!$chkOrg->fetch()) jsonError('Organization not found', 404);
|
||||
|
||||
$phoneEnc = $encryptionHelper->encryptData($phone);
|
||||
|
||||
$chkDup = $transit_con->prepare(
|
||||
"SELECT id FROM transit_org_admins WHERE org_id=? AND phone=? LIMIT 1"
|
||||
);
|
||||
$chkDup->execute([$orgId, $phoneEnc]);
|
||||
if ($chkDup->fetch()) jsonError('An admin with this phone already exists for this organization', 409);
|
||||
|
||||
$transit_con->prepare(
|
||||
"INSERT INTO transit_org_admins (org_id, name, phone, role, is_active) VALUES (?,?,?,?,1)"
|
||||
)->execute([$orgId, $name, $phoneEnc, $adminRole]);
|
||||
|
||||
appLog("[ADMIN][TRANSIT][ORG][admin_add] org={$orgId} name={$name} role={$adminRole}");
|
||||
|
||||
jsonSuccess(['admin_id' => (int)$transit_con->lastInsertId()], 'Admin added successfully');
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// Admin/transit/org/admin_toggle.php — تفعيل/تعليق مشرف مؤسسة (لفريق سيرو)
|
||||
// POST: admin_id, is_active (1|0)
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
$adminId = filterRequest('admin_id', 'int');
|
||||
$isActive = filterRequest('is_active', 'int');
|
||||
|
||||
if (!$adminId || $isActive === null) jsonError('admin_id and is_active are required', 400);
|
||||
|
||||
$st = $transit_con->prepare("SELECT id, org_id FROM transit_org_admins WHERE id=? LIMIT 1");
|
||||
$st->execute([$adminId]);
|
||||
$admin = $st->fetch();
|
||||
if (!$admin) jsonError('Admin not found', 404);
|
||||
|
||||
$transit_con->prepare("UPDATE transit_org_admins SET is_active=? WHERE id=?")
|
||||
->execute([$isActive ? 1 : 0, $adminId]);
|
||||
|
||||
// إبطال جلساته الحالية فوراً عند التعليق
|
||||
if (!$isActive) {
|
||||
$sessions = $transit_con->prepare("SELECT token_hash FROM transit_sessions WHERE admin_id=?");
|
||||
$sessions->execute([$adminId]);
|
||||
foreach ($sessions->fetchAll(PDO::FETCH_COLUMN) as $hash) {
|
||||
if ($redis) $redis->del("transit:session:{$hash}");
|
||||
}
|
||||
$transit_con->prepare("DELETE FROM transit_sessions WHERE admin_id=?")->execute([$adminId]);
|
||||
}
|
||||
|
||||
appLog("[ADMIN][TRANSIT][ORG][admin_toggle] admin={$adminId} is_active={$isActive}");
|
||||
|
||||
jsonSuccess(['admin_id' => $adminId, 'is_active' => (bool)$isActive]);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
// Admin/transit/org/admins_list.php — قائمة مشرفي مؤسسة (لفريق سيرو)
|
||||
// POST/GET: org_id
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
$orgId = filterRequest('org_id', 'int');
|
||||
if (!$orgId) jsonError('org_id is required', 400);
|
||||
|
||||
$st = $transit_con->prepare(
|
||||
"SELECT id, name, phone, role, is_active, created_at
|
||||
FROM transit_org_admins WHERE org_id=? ORDER BY created_at ASC"
|
||||
);
|
||||
$st->execute([$orgId]);
|
||||
$admins = $st->fetchAll();
|
||||
|
||||
foreach ($admins as &$a) {
|
||||
if (!empty($a['phone'])) {
|
||||
$a['phone'] = $encryptionHelper->decryptData($a['phone']) ?: null;
|
||||
}
|
||||
}
|
||||
unset($a);
|
||||
|
||||
jsonSuccess(['admins' => $admins]);
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// Admin/transit/org/create.php — فريق سيرو يضيف مؤسسة جديدة (جامعة/مدرسة/فندق/شركة/ناقل)
|
||||
// + ينشئ أول مشرف (owner) لها مباشرة
|
||||
// POST: type, country, city, name_ar, name_en, admin_name, admin_phone, ...
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
require_once __DIR__ . '/../../../transit/functions.php';
|
||||
|
||||
requireTransitFields(['type', 'country', 'city', 'name_ar', 'name_en', 'admin_name', 'admin_phone']);
|
||||
|
||||
$type = filterRequest('type');
|
||||
$country = strtoupper(substr(filterRequest('country'), 0, 2));
|
||||
$city = filterRequest('city');
|
||||
$nameAr = filterRequest('name_ar');
|
||||
$nameEn = filterRequest('name_en');
|
||||
$adminName = filterRequest('admin_name');
|
||||
$adminPhone = normalizePhone(filterRequest('admin_phone'));
|
||||
$adminRole = filterRequest('admin_role') ?: 'owner';
|
||||
$trialEndsAt = filterRequest('trial_ends_at') ?: date('Y-m-d', strtotime('+90 days'));
|
||||
|
||||
$allowedTypes = ['university', 'school', 'hotel', 'company', 'transporter'];
|
||||
if (!in_array($type, $allowedTypes)) {
|
||||
jsonError('Invalid type. Allowed: ' . implode(', ', $allowedTypes));
|
||||
}
|
||||
|
||||
$chk = $transit_con->prepare("SELECT id FROM transit_orgs WHERE name_ar=? AND country=? LIMIT 1");
|
||||
$chk->execute([$nameAr, $country]);
|
||||
if ($chk->fetch()) jsonError('Organization already exists', 409);
|
||||
|
||||
$adminPhoneEnc = $encryptionHelper->encryptData($adminPhone);
|
||||
$contactPhoneRaw = normalizePhone(filterRequest('contact_phone') ?? '');
|
||||
$contactPhoneEnc = $contactPhoneRaw ? $encryptionHelper->encryptData($contactPhoneRaw) : null;
|
||||
|
||||
$transit_con->beginTransaction();
|
||||
try {
|
||||
$transit_con->prepare(
|
||||
"INSERT INTO transit_orgs
|
||||
(type, country, city, name_ar, name_en, contact_phone, contact_email, website, contract_status, trial_ends_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,'active',?)"
|
||||
)->execute([
|
||||
$type, $country, $city, $nameAr, $nameEn,
|
||||
$contactPhoneEnc, filterRequest('contact_email'), filterRequest('website'),
|
||||
$trialEndsAt,
|
||||
]);
|
||||
|
||||
$orgId = (int)$transit_con->lastInsertId();
|
||||
|
||||
$transit_con->prepare(
|
||||
"INSERT INTO transit_org_admins (org_id, name, phone, role) VALUES (?,?,?,?)"
|
||||
)->execute([$orgId, $adminName, $adminPhoneEnc, $adminRole]);
|
||||
|
||||
$transit_con->commit();
|
||||
} catch (Throwable $e) {
|
||||
$transit_con->rollBack();
|
||||
appLog('[ADMIN][TRANSIT][ORG][create] ' . $e->getMessage(), 'ERROR');
|
||||
jsonError('Failed to create organization', 500);
|
||||
}
|
||||
|
||||
jsonSuccess([
|
||||
'org_id' => $orgId,
|
||||
'name_ar' => $nameAr,
|
||||
'type' => $type,
|
||||
'country' => $country,
|
||||
], 'Organization created successfully');
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
// Admin/transit/org/details.php — تفاصيل وتحليلات مؤسسة واحدة (لفريق سيرو)
|
||||
// POST/GET: org_id
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
$orgId = filterRequest('org_id', 'int');
|
||||
if (!$orgId) jsonError('org_id is required', 400);
|
||||
|
||||
$st = $transit_con->prepare("SELECT * FROM transit_orgs WHERE id=? LIMIT 1");
|
||||
$st->execute([$orgId]);
|
||||
$org = $st->fetch();
|
||||
if (!$org) jsonError('Organization not found', 404);
|
||||
|
||||
// فك تشفير هاتف التواصل للعرض الإداري فقط
|
||||
if (!empty($org['contact_phone'])) {
|
||||
$org['contact_phone'] = $encryptionHelper->decryptData($org['contact_phone']) ?: null;
|
||||
}
|
||||
|
||||
// ── العدّادات الأساسية ───────────────────────────────────────
|
||||
$counts = [
|
||||
'drivers_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_drivers WHERE org_id=$orgId")->fetchColumn(),
|
||||
'drivers_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_drivers WHERE org_id=$orgId AND status='active'")->fetchColumn(),
|
||||
'vehicles_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_vehicles WHERE org_id=$orgId")->fetchColumn(),
|
||||
'vehicles_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_vehicles WHERE org_id=$orgId AND is_active=1")->fetchColumn(),
|
||||
'routes_total' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_routes WHERE org_id=$orgId")->fetchColumn(),
|
||||
'routes_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_routes WHERE org_id=$orgId AND status='active'")->fetchColumn(),
|
||||
'enrollments_active' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_enrollments WHERE org_id=$orgId AND status='active'")->fetchColumn(),
|
||||
'enrollments_pending' => (int)$transit_con->query("SELECT COUNT(*) FROM transit_enrollments WHERE org_id=$orgId AND status='pending'")->fetchColumn(),
|
||||
];
|
||||
|
||||
// ── الرحلات: اليوم / هذا الأسبوع / هذا الشهر / إجمالي ──────────
|
||||
$today = date('Y-m-d');
|
||||
$weekStart = date('Y-m-d', strtotime('monday this week'));
|
||||
$monthStart = date('Y-m-01');
|
||||
|
||||
$stTrips = $transit_con->prepare(
|
||||
"SELECT
|
||||
SUM(trip_date = ?) AS today_count,
|
||||
SUM(trip_date >= ?) AS week_count,
|
||||
SUM(trip_date >= ?) AS month_count,
|
||||
COUNT(*) AS total_count,
|
||||
SUM(status='completed') AS completed_count,
|
||||
SUM(status='cancelled') AS cancelled_count,
|
||||
SUM(status='no_show') AS no_show_count,
|
||||
AVG(CASE WHEN status='completed' THEN delay_minutes END) AS avg_delay_minutes,
|
||||
SUM(CASE WHEN status='completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL
|
||||
THEN TIMESTAMPDIFF(MINUTE, started_at, completed_at) ELSE 0 END) AS total_minutes_driven
|
||||
FROM transit_trips WHERE org_id = ?"
|
||||
);
|
||||
$stTrips->execute([$today, $weekStart, $monthStart, $orgId]);
|
||||
$tripStats = $stTrips->fetch();
|
||||
|
||||
$trips = [
|
||||
'today' => (int)($tripStats['today_count'] ?? 0),
|
||||
'this_week' => (int)($tripStats['week_count'] ?? 0),
|
||||
'this_month' => (int)($tripStats['month_count'] ?? 0),
|
||||
'total' => (int)($tripStats['total_count'] ?? 0),
|
||||
'completed' => (int)($tripStats['completed_count'] ?? 0),
|
||||
'cancelled' => (int)($tripStats['cancelled_count'] ?? 0),
|
||||
'no_show' => (int)($tripStats['no_show_count'] ?? 0),
|
||||
'avg_delay_minutes' => round((float)($tripStats['avg_delay_minutes'] ?? 0), 1),
|
||||
'total_hours_driven' => round(((int)($tripStats['total_minutes_driven'] ?? 0)) / 60, 1),
|
||||
];
|
||||
|
||||
// ── الخطوط مع ملخص لكل خط ─────────────────────────────────────
|
||||
$stRoutes = $transit_con->prepare(
|
||||
"SELECT r.id, r.name_ar, r.status, r.distance_km,
|
||||
(SELECT COUNT(*) FROM transit_stops s WHERE s.route_id = r.id) AS stops_count,
|
||||
(SELECT COUNT(*) FROM transit_trips t WHERE t.route_id = r.id AND t.status='completed') AS completed_trips
|
||||
FROM transit_routes r WHERE r.org_id = ? ORDER BY r.name_ar ASC"
|
||||
);
|
||||
$stRoutes->execute([$orgId]);
|
||||
$routes = $stRoutes->fetchAll();
|
||||
|
||||
jsonSuccess([
|
||||
'org' => $org,
|
||||
'counts' => $counts,
|
||||
'trips' => $trips,
|
||||
'routes' => $routes,
|
||||
]);
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// Admin/transit/org/list.php — قائمة كل مؤسسات مواصلاتي (لفريق سيرو)
|
||||
// GET/POST: country?, type?, contract_status?, search?, page?, per_page?
|
||||
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
jsonError('Unauthorized: Admin access required', 403);
|
||||
}
|
||||
|
||||
try { $transit_con = Database::get('transit'); }
|
||||
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
|
||||
|
||||
$country = filterRequest('country');
|
||||
$type = filterRequest('type');
|
||||
$contractStatus = filterRequest('contract_status');
|
||||
$search = filterRequest('search');
|
||||
$page = max(1, (int)(filterRequest('page', 'int') ?? 1));
|
||||
$perPage = min(100, max(10, (int)(filterRequest('per_page', 'int') ?? 30)));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$where = '1=1';
|
||||
$params = [];
|
||||
|
||||
if ($country) { $where .= ' AND country = ?'; $params[] = strtoupper(substr($country, 0, 2)); }
|
||||
if ($type) { $where .= ' AND type = ?'; $params[] = $type; }
|
||||
if ($contractStatus) { $where .= ' AND contract_status = ?'; $params[] = $contractStatus; }
|
||||
if ($search) { $where .= ' AND (name_ar LIKE ? OR name_en LIKE ?)'; $params[] = "%$search%"; $params[] = "%$search%"; }
|
||||
|
||||
$countSt = $transit_con->prepare("SELECT COUNT(*) FROM transit_orgs WHERE $where");
|
||||
$countSt->execute($params);
|
||||
$total = (int)$countSt->fetchColumn();
|
||||
|
||||
$params[] = $perPage;
|
||||
$params[] = $offset;
|
||||
$st = $transit_con->prepare(
|
||||
"SELECT id, type, country, city, name_ar, name_en, logo_url,
|
||||
contract_status, trial_ends_at, created_at
|
||||
FROM transit_orgs
|
||||
WHERE $where
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?"
|
||||
);
|
||||
$st->execute($params);
|
||||
$orgs = $st->fetchAll();
|
||||
|
||||
if ($orgs) {
|
||||
$ids = implode(',', array_map('intval', array_column($orgs, 'id')));
|
||||
|
||||
$drivers = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_drivers WHERE org_id IN ($ids) GROUP BY org_id")
|
||||
->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$vehicles = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_vehicles WHERE org_id IN ($ids) GROUP BY org_id")
|
||||
->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$routes = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_routes WHERE org_id IN ($ids) AND status='active' GROUP BY org_id")
|
||||
->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$enrollments = $transit_con->query("SELECT org_id, COUNT(*) c FROM transit_enrollments WHERE org_id IN ($ids) AND status='active' GROUP BY org_id")
|
||||
->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
foreach ($orgs as &$o) {
|
||||
$id = $o['id'];
|
||||
$o['drivers_count'] = (int)($drivers[$id] ?? 0);
|
||||
$o['vehicles_count'] = (int)($vehicles[$id] ?? 0);
|
||||
$o['active_routes'] = (int)($routes[$id] ?? 0);
|
||||
$o['active_enrollments'] = (int)($enrollments[$id] ?? 0);
|
||||
}
|
||||
unset($o);
|
||||
}
|
||||
|
||||
jsonSuccess([
|
||||
'orgs' => $orgs,
|
||||
'pagination' => ['total' => $total, 'page' => $page, 'per_page' => $perPage],
|
||||
]);
|
||||
Reference in New Issue
Block a user