Update: 2026-08-03 11:32:12
This commit is contained in:
@@ -11,7 +11,21 @@ if (!empty($promo_code)) {
|
|||||||
$stmt = $con->prepare($sql);
|
$stmt = $con->prepare($sql);
|
||||||
$stmt->bindParam(':promo_code', $promo_code, PDO::PARAM_STR);
|
$stmt->bindParam(':promo_code', $promo_code, PDO::PARAM_STR);
|
||||||
} else {
|
} else {
|
||||||
$sql = "SELECT `id`, `promo_code`, `amount`, `description`, `passengerID`, `validity_start_date`, `validity_end_date` FROM `promos` WHERE `passengerID` IN ('all', 'none', '') ORDER BY id DESC";
|
// سرد كل العروض — استعمال إداري بحت (شاشة الهدايا والعروض).
|
||||||
|
// هذا الملف تحت /ride لا تحت /Admin، فلا تغطّيه البوّابة العامة في
|
||||||
|
// connect.php؛ بدون هذا الفحص كان أي راكب يستطيع سرد العروض كلها.
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required to list promos.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// كان الشرط `passengerID IN ('all','none','')` يُخفي كل عرض مخصّص لراكب
|
||||||
|
// بعينه، فتظهر شاشة العروض ناقصة دون سبب ظاهر. الإدارة يجب أن ترى الكل.
|
||||||
|
$sql = "SELECT `id`, `promo_code`, `amount`, `description`, `passengerID`, `validity_start_date`, `validity_end_date` FROM `promos` ORDER BY id DESC";
|
||||||
$stmt = $con->prepare($sql);
|
$stmt = $con->prepare($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"6c0baaebf70e0148f485f27d5616b3d3382da7
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "3313168899" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
serviceWorkerVersion: "806885842" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+85444
-84678
File diff suppressed because one or more lines are too long
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// ride/driverPayment/withdrawal_requests.php
|
||||||
|
// قائمة طلبات سحب أرصدة السائقين للوحة التحكم
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// لماذا هذا الملف:
|
||||||
|
// جدول `driver_withdrawal_requests` يُكتب فيه من
|
||||||
|
// ride/mtn/driver_payout_syria.php عند طلب السائق السحب، لكن **لا شيء في
|
||||||
|
// المنصّة كلها يقرأه** — لا نقطة في الباك إند ولا شاشة في لوحة التحكم
|
||||||
|
// (تحقّقت بمسح كامل على backend/ و payment_server/ و siro_admin/).
|
||||||
|
// أي أن طلبات السحب كانت تتراكم دون أن يراها أحد، والإشعار الوحيد رسالة
|
||||||
|
// واتساب لحظية عند الإرسال — تضيع إن فات وقتها.
|
||||||
|
//
|
||||||
|
// GET : قائمة الطلبات مع تصفية بالحالة + مجاميع لكل حالة
|
||||||
|
// POST : تحديث حالة طلب (approved / rejected / paid)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
$role = $decodedToken->role ?? null;
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// الحالات المسموح بها — قائمة مغلقة حتى لا تُكتب حالة عشوائية في الجدول
|
||||||
|
const WITHDRAWAL_STATUSES = ['pending', 'approved', 'rejected', 'paid'];
|
||||||
|
|
||||||
|
// التوجيه بحقل action لا بطريقة HTTP: عميل اللوحة (CRUD.getWallet في
|
||||||
|
// siro_admin) يرسل كل الطلبات بـ POST form-encoded، فالتفريق بالطريقة كان
|
||||||
|
// سيوجّه طلبات العرض إلى فرع التحديث.
|
||||||
|
$body = $_POST;
|
||||||
|
if (empty($body)) {
|
||||||
|
$raw = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (is_array($raw)) $body = $raw;
|
||||||
|
}
|
||||||
|
$action = $body['action'] ?? $_GET['action'] ?? 'list';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ── تحديث حالة طلب ───────────────────────────────────────
|
||||||
|
if ($action === 'update_status') {
|
||||||
|
// صرف المال فعلياً قرار لا رجعة فيه — نقصره على super_admin
|
||||||
|
if ($role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Super Admin required to change withdrawal status.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($body['id']) ? (int)$body['id'] : 0;
|
||||||
|
$status = trim((string)($body['status'] ?? ''));
|
||||||
|
|
||||||
|
if ($id <= 0 || !in_array($status, WITHDRAWAL_STATUSES, true)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Invalid id or status. Allowed: ' . implode(', ', WITHDRAWAL_STATUSES),
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// لا نسمح بتعديل طلب خرج من pending إلا إلى paid — يمنع عكس رفض
|
||||||
|
// أو إعادة اعتماد طلب مدفوع بالخطأ.
|
||||||
|
$cur = $con->prepare("SELECT status FROM driver_withdrawal_requests WHERE id = ?");
|
||||||
|
$cur->execute([$id]);
|
||||||
|
$current = $cur->fetchColumn();
|
||||||
|
|
||||||
|
if ($current === false) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Request not found.'], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($current !== 'pending' && !($current === 'approved' && $status === 'paid')) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => "Cannot change status from '$current' to '$status'.",
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $con->prepare("UPDATE driver_withdrawal_requests SET status = ? WHERE id = ?");
|
||||||
|
$upd->execute([$status, $id]);
|
||||||
|
|
||||||
|
error_log("[withdrawal_requests] id=$id '$current' -> '$status' by " . ($decodedToken->user_id ?? 'unknown'));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => ['id' => $id, 'from' => $current, 'to' => $status],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── قائمة الطلبات ────────────────────────────────────────
|
||||||
|
$filter = $body['status'] ?? $_GET['status'] ?? 'pending';
|
||||||
|
$limit = (int)($body['limit'] ?? $_GET['limit'] ?? 100);
|
||||||
|
$limit = max(1, min(200, $limit));
|
||||||
|
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($filter !== 'all') {
|
||||||
|
if (!in_array($filter, WITHDRAWAL_STATUSES, true)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Invalid status filter.'], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$where = 'WHERE status = :status';
|
||||||
|
$params = [':status' => $filter];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $con->prepare("
|
||||||
|
SELECT id, driver_id, driver_name, amount, wallet_type, wallet_number,
|
||||||
|
status, created_at, updated_at
|
||||||
|
FROM driver_withdrawal_requests
|
||||||
|
$where
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $limit
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($rows as &$r) {
|
||||||
|
$r['amount'] = (float)$r['amount'];
|
||||||
|
$r['id'] = (int)$r['id'];
|
||||||
|
}
|
||||||
|
unset($r);
|
||||||
|
|
||||||
|
// مجاميع لكل حالة — تغذّي شارات العدّ في اللوحة دون طلب إضافي
|
||||||
|
$totals = [];
|
||||||
|
$agg = $con->query("
|
||||||
|
SELECT status, COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total
|
||||||
|
FROM driver_withdrawal_requests
|
||||||
|
GROUP BY status
|
||||||
|
");
|
||||||
|
foreach ($agg->fetchAll(PDO::FETCH_ASSOC) as $a) {
|
||||||
|
$totals[$a['status']] = [
|
||||||
|
'count' => (int)$a['cnt'],
|
||||||
|
'total' => (float)$a['total'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'filter' => $filter,
|
||||||
|
'requests' => $rows,
|
||||||
|
'totals' => $totals,
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[withdrawal_requests] ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'An internal error occurred.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// ride/siroWallet/summary.php
|
||||||
|
// ملخّص محفظة سيرو — شهري وإجمالي مع تفصيل الإيداع والصرف
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// لماذا ملف جديد بدل تعديل get.php:
|
||||||
|
// • get.php يُرجع SUM(amount) واحداً للشهر الجاري فقط، بعمود بلا اسم
|
||||||
|
// (المفتاح يصل للعميل كـ "SUM(amount)")، ولا يفرّق بين الإيداع والصرف.
|
||||||
|
// • get.php يستدعي printSuccess()/printFailure() وهما غير معرّفتين في
|
||||||
|
// payment_server/v2 إطلاقاً (لا في functions.php ولا في connect.php) —
|
||||||
|
// معرّفتان محلياً داخل ملفَّي syriatel فقط. أي أن get.php يسقط بخطأ
|
||||||
|
// "undefined function" فور اجتياز المصادقة.
|
||||||
|
// • فحص حيّ في 2026-08-03: get.php يُرجع 404 على v1 و v2 معاً — أي أنه
|
||||||
|
// غير منشور أصلاً، بينما driverWallet/get.php يُرجع 401 (موجود).
|
||||||
|
//
|
||||||
|
// لذلك هذا الملف مكتفٍ بذاته: لا يعتمد إلا على connect.php و json_encode.
|
||||||
|
//
|
||||||
|
// الرد:
|
||||||
|
// month_to_date / previous_month / all_time، ولكلٍّ:
|
||||||
|
// credits → مجموع الحركات الموجبة (إيداع)
|
||||||
|
// debits → مجموع الحركات السالبة (صرف/سحب) كقيمة موجبة
|
||||||
|
// net → الصافي
|
||||||
|
// tx_count → عدد الحركات
|
||||||
|
// by_payment_method → تفصيل الشهر الجاري حسب وسيلة الدفع
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
// connect.php ينفّذ authenticateJWT() ويُسقط الطلب عند الفشل، فالوصول إلى
|
||||||
|
// هنا يعني توكناً صالحاً. نقصر الملخّص المالي على الأدوار الإدارية.
|
||||||
|
$role = $decodedToken->role ?? null;
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* مجاميع نافذة زمنية واحدة.
|
||||||
|
* الحركات السالبة تُعاد كقيمة موجبة في debits ليقرأها العميل مباشرة.
|
||||||
|
*/
|
||||||
|
function siroWalletWindow(PDO $con, ?string $from, ?string $to): array
|
||||||
|
{
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($from !== null && $to !== null) {
|
||||||
|
$where = 'WHERE createdAt >= :from AND createdAt < :to';
|
||||||
|
$params = [':from' => $from, ':to' => $to];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $con->prepare("
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END), 0) AS credits,
|
||||||
|
COALESCE(SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END), 0) AS debits,
|
||||||
|
COALESCE(SUM(amount), 0) AS net,
|
||||||
|
COUNT(*) AS tx_count
|
||||||
|
FROM `siroWallet`
|
||||||
|
$where
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'credits' => (float)($row['credits'] ?? 0),
|
||||||
|
'debits' => (float)($row['debits'] ?? 0),
|
||||||
|
'net' => (float)($row['net'] ?? 0),
|
||||||
|
'tx_count' => (int) ($row['tx_count'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// حدود الشهر تُحسب في PHP لا في SQL، حتى تتبع المنطقة الزمنية للتطبيق
|
||||||
|
// ويسهل اختبارها، ولتفادي BETWEEN على TIMESTAMP الذي يُسقط آخر يوم.
|
||||||
|
$startThisMonth = date('Y-m-01 00:00:00');
|
||||||
|
$startNextMonth = date('Y-m-01 00:00:00', strtotime('first day of next month'));
|
||||||
|
$startPrevMonth = date('Y-m-01 00:00:00', strtotime('first day of last month'));
|
||||||
|
|
||||||
|
$monthToDate = siroWalletWindow($con, $startThisMonth, $startNextMonth);
|
||||||
|
$previousMonth = siroWalletWindow($con, $startPrevMonth, $startThisMonth);
|
||||||
|
$allTime = siroWalletWindow($con, null, null);
|
||||||
|
|
||||||
|
// تفصيل الشهر الجاري حسب وسيلة الدفع
|
||||||
|
$stmt = $con->prepare("
|
||||||
|
SELECT
|
||||||
|
paymentMethod,
|
||||||
|
COALESCE(SUM(amount), 0) AS net,
|
||||||
|
COUNT(*) AS tx_count
|
||||||
|
FROM `siroWallet`
|
||||||
|
WHERE createdAt >= :from AND createdAt < :to
|
||||||
|
GROUP BY paymentMethod
|
||||||
|
ORDER BY net DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':from' => $startThisMonth, ':to' => $startNextMonth]);
|
||||||
|
|
||||||
|
$byMethod = [];
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||||
|
$byMethod[] = [
|
||||||
|
'payment_method' => $r['paymentMethod'] !== '' ? $r['paymentMethod'] : 'unknown',
|
||||||
|
'net' => (float)$r['net'],
|
||||||
|
'tx_count' => (int)$r['tx_count'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// نسبة التغيّر عن الشهر السابق — الواجهة تعرضها كمؤشّر اتجاه
|
||||||
|
$prevNet = $previousMonth['net'];
|
||||||
|
$growth = ($prevNet != 0.0)
|
||||||
|
? round((($monthToDate['net'] - $prevNet) / abs($prevNet)) * 100, 2)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'month_to_date' => $monthToDate,
|
||||||
|
'previous_month' => $previousMonth,
|
||||||
|
'all_time' => $allTime,
|
||||||
|
'growth_percent' => $growth, // null عندما لا يوجد أساس للمقارنة
|
||||||
|
'by_payment_method' => $byMethod,
|
||||||
|
'period' => [
|
||||||
|
'month_start' => $startThisMonth,
|
||||||
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[siroWallet/summary] ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'An internal error occurred.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
@@ -403,6 +403,21 @@ class AppLink {
|
|||||||
"$paymentServerV2/Admin/v2/financial/settlements.php";
|
"$paymentServerV2/Admin/v2/financial/settlements.php";
|
||||||
static String financialStatsV2 =
|
static String financialStatsV2 =
|
||||||
"$paymentServerV2/Admin/v2/financial/stats.php";
|
"$paymentServerV2/Admin/v2/financial/stats.php";
|
||||||
|
// ملخّص محفظة سيرو من خادم المدفوعات (شهري/إجمالي/إيداع/صرف).
|
||||||
|
// يحلّ محلّ siroWallet/get.php الذي كان يُرجع SUM(amount) بلا اسم للشهر
|
||||||
|
// الجاري فقط، وهو غير منشور أصلاً (404 على v1 و v2).
|
||||||
|
static String siroWalletSummary =
|
||||||
|
"$paymentServerV2/ride/siroWallet/summary.php";
|
||||||
|
// طلبات سحب أرصدة السائقين — جدول driver_withdrawal_requests كان يُكتب
|
||||||
|
// فيه ولا يقرأه شيء في المنصّة كلها.
|
||||||
|
static String withdrawalRequests =
|
||||||
|
"$paymentServerV2/ride/driverPayment/withdrawal_requests.php";
|
||||||
|
|
||||||
|
// وحدة الطعام — الباك إند جاهز في backend/food/admin/ ولا واجهة له
|
||||||
|
static String get foodMerchants => "$server/food/admin/merchants.php";
|
||||||
|
static String get foodMerchantApprove =>
|
||||||
|
"$server/food/admin/merchant_approve.php";
|
||||||
|
static String get foodPayouts => "$server/food/admin/payouts.php";
|
||||||
static String dashboardWalletV2 =
|
static String dashboardWalletV2 =
|
||||||
"$paymentServerV2/Admin/v2/financial/dashboard_wallet.php";
|
"$paymentServerV2/Admin/v2/financial/dashboard_wallet.php";
|
||||||
static String auditLogsV2 = "$server/Admin/v2/security/audit_logs.php";
|
static String auditLogsV2 = "$server/Admin/v2/security/audit_logs.php";
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ class FinancialV2Controller extends GetxController {
|
|||||||
Map<String, dynamic> stats = {};
|
Map<String, dynamic> stats = {};
|
||||||
List<dynamic> settlements = [];
|
List<dynamic> settlements = [];
|
||||||
|
|
||||||
|
/// ملخّص محفظة سيرو القادم من خادم المدفوعات.
|
||||||
|
/// المفاتيح: month_to_date / previous_month / all_time / growth_percent
|
||||||
|
/// / by_payment_method — ولكل نافذة: credits و debits و net و tx_count.
|
||||||
|
Map<String, dynamic> walletSummary = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@@ -23,12 +28,28 @@ class FinancialV2Controller extends GetxController {
|
|||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchStats(),
|
fetchStats(),
|
||||||
fetchSettlements(),
|
fetchSettlements(),
|
||||||
|
fetchWalletSummary(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchWalletSummary() async {
|
||||||
|
try {
|
||||||
|
var res =
|
||||||
|
await CRUD().getWallet(link: AppLink.siroWalletSummary, payload: {});
|
||||||
|
if (res != 'failure' && res != null) {
|
||||||
|
var d = res is String ? jsonDecode(res) : res;
|
||||||
|
if (d['status'] == 'success' && d['data'] is Map) {
|
||||||
|
walletSummary = Map<String, dynamic>.from(d['data']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error fetching Siro wallet summary: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchStats() async {
|
Future<void> fetchStats() async {
|
||||||
try {
|
try {
|
||||||
var res =
|
var res =
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:siro_admin/constant/links.dart';
|
||||||
|
import 'package:siro_admin/controller/functions/crud.dart';
|
||||||
|
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||||
|
import '../../print.dart';
|
||||||
|
|
||||||
|
/// إدارة المطاعم (وحدة الطعام).
|
||||||
|
/// الباك إند كان جاهزاً بالكامل في backend/food/admin/ منذ البداية
|
||||||
|
/// (merchants / merchant_approve / payouts) دون أي واجهة في اللوحة.
|
||||||
|
class FoodMerchantsController extends GetxController {
|
||||||
|
bool isLoading = true;
|
||||||
|
List<dynamic> merchants = [];
|
||||||
|
String filter = 'all';
|
||||||
|
|
||||||
|
/// المطعم قيد المعالجة — لتعطيل أزراره أثناء الطلب
|
||||||
|
int? busyId;
|
||||||
|
|
||||||
|
/// الحالات كما تقبلها backend/food/admin/merchants.php حرفياً
|
||||||
|
static const statuses = <String, String>{
|
||||||
|
'all': 'الكل',
|
||||||
|
'pending_approval': 'بانتظار الاعتماد',
|
||||||
|
'active': 'نشط',
|
||||||
|
'paused': 'موقوف مؤقتاً',
|
||||||
|
'suspended': 'معلّق',
|
||||||
|
'rejected': 'مرفوض',
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchMerchants();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeFilter(String next) async {
|
||||||
|
if (filter == next) return;
|
||||||
|
filter = next;
|
||||||
|
await fetchMerchants();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchMerchants() async {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await CRUD()
|
||||||
|
.post(link: AppLink.foodMerchants, payload: {'status': filter});
|
||||||
|
if (res != 'failure' && res != null) {
|
||||||
|
var d = res is String ? jsonDecode(res) : res;
|
||||||
|
if (d['status'] == 'success') {
|
||||||
|
// jsonSuccess يغلّف الحمولة في message لا data
|
||||||
|
final payload = d['message'] ?? d['data'];
|
||||||
|
if (payload is Map && payload['merchants'] is List) {
|
||||||
|
merchants = payload['merchants'];
|
||||||
|
} else {
|
||||||
|
merchants = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mySnackbarError('${d['message'] ?? 'تعذّر جلب المطاعم'}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error fetching merchants: $e');
|
||||||
|
mySnackbarError('$e');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// approve أو reject — الخادم يقبلهما فقط لمطعم بحالة pending_approval.
|
||||||
|
Future<void> decide(int merchantId, String action) async {
|
||||||
|
busyId = merchantId;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.foodMerchantApprove,
|
||||||
|
payload: {
|
||||||
|
'merchant_id': merchantId.toString(),
|
||||||
|
'action': action,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res != 'failure' && res != null) {
|
||||||
|
var d = res is String ? jsonDecode(res) : res;
|
||||||
|
if (d['status'] == 'success') {
|
||||||
|
mySnackbarSuccess(
|
||||||
|
action == 'approve' ? 'تم اعتماد المطعم' : 'تم رفض المطعم');
|
||||||
|
busyId = null;
|
||||||
|
await fetchMerchants();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mySnackbarError('${d['message'] ?? 'تعذّر تنفيذ الإجراء'}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error deciding merchant: $e');
|
||||||
|
mySnackbarError('$e');
|
||||||
|
}
|
||||||
|
|
||||||
|
busyId = null;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:siro_admin/constant/links.dart';
|
||||||
|
import 'package:siro_admin/controller/functions/crud.dart';
|
||||||
|
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||||
|
import '../../print.dart';
|
||||||
|
|
||||||
|
/// طلبات سحب أرصدة السائقين (جدول driver_withdrawal_requests على خادم
|
||||||
|
/// المدفوعات). لم تكن لها واجهة إطلاقاً قبل هذا: السائق يرسل الطلب فيُخزَّن
|
||||||
|
/// وتُرسل رسالة واتساب لحظية، ثم لا يقرأ الجدول أحد.
|
||||||
|
class WithdrawalController extends GetxController {
|
||||||
|
bool isLoading = true;
|
||||||
|
List<dynamic> requests = [];
|
||||||
|
Map<String, dynamic> totals = {};
|
||||||
|
String filter = 'pending';
|
||||||
|
|
||||||
|
/// المعرّف قيد المعالجة — لتعطيل أزرار الصف أثناء الطلب ومنع النقر المزدوج
|
||||||
|
int? busyId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeFilter(String next) async {
|
||||||
|
if (filter == next) return;
|
||||||
|
filter = next;
|
||||||
|
await fetchRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchRequests() async {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await CRUD().getWallet(
|
||||||
|
link: AppLink.withdrawalRequests,
|
||||||
|
payload: {'action': 'list', 'status': filter},
|
||||||
|
);
|
||||||
|
if (res != 'failure' && res != null) {
|
||||||
|
var d = res is String ? jsonDecode(res) : res;
|
||||||
|
if (d['status'] == 'success' && d['data'] is Map) {
|
||||||
|
requests = d['data']['requests'] ?? [];
|
||||||
|
totals = Map<String, dynamic>.from(d['data']['totals'] ?? {});
|
||||||
|
} else {
|
||||||
|
mySnackbarError('${d['message'] ?? 'تعذّر جلب طلبات السحب'}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error fetching withdrawal requests: $e');
|
||||||
|
mySnackbarError('$e');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// تغيير الحالة يتطلّب super_admin على الخادم؛ الخادم يرفض أيضاً أي انتقال
|
||||||
|
/// غير مسموح (مثل إعادة فتح طلب مرفوض) بـ 409، فنعرض رسالته كما هي.
|
||||||
|
Future<void> updateStatus(int id, String status) async {
|
||||||
|
busyId = id;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await CRUD().getWallet(
|
||||||
|
link: AppLink.withdrawalRequests,
|
||||||
|
payload: {
|
||||||
|
'action': 'update_status',
|
||||||
|
'id': id.toString(),
|
||||||
|
'status': status,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res != 'failure' && res != null) {
|
||||||
|
var d = res is String ? jsonDecode(res) : res;
|
||||||
|
if (d['status'] == 'success') {
|
||||||
|
mySnackbarSuccess('تم تحديث حالة الطلب');
|
||||||
|
busyId = null;
|
||||||
|
await fetchRequests();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mySnackbarError('${d['message'] ?? 'تعذّر تحديث الحالة'}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error updating withdrawal status: $e');
|
||||||
|
mySnackbarError('$e');
|
||||||
|
}
|
||||||
|
|
||||||
|
busyId = null;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,8 @@ import 'staff/pending_admins_page.dart';
|
|||||||
import 'dashboard_v2_widget.dart';
|
import 'dashboard_v2_widget.dart';
|
||||||
import 'static/advanced_analytics_page.dart';
|
import 'static/advanced_analytics_page.dart';
|
||||||
import 'financial/financial_v2_page.dart';
|
import 'financial/financial_v2_page.dart';
|
||||||
|
import 'financial/withdrawal_requests_page.dart';
|
||||||
|
import 'food/food_merchants_page.dart';
|
||||||
import 'security/audit_logs_page.dart';
|
import 'security/audit_logs_page.dart';
|
||||||
import 'analytics/live_analytics_page.dart';
|
import 'analytics/live_analytics_page.dart';
|
||||||
import 'package:siro_admin/views/widgets/responsive_layout.dart';
|
import 'package:siro_admin/views/widgets/responsive_layout.dart';
|
||||||
@@ -1378,7 +1380,11 @@ class _AdminHomePageState extends State<AdminHomePage>
|
|||||||
title: 'المالية والإدارة',
|
title: 'المالية والإدارة',
|
||||||
items: [
|
items: [
|
||||||
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded,
|
ActionItem('الإدارة المالية V2', Icons.account_balance_rounded,
|
||||||
const Color(0xFF6366F1), () => Get.to(() => const FinancialV2Page())),
|
cs.primary, () => Get.to(() => const FinancialV2Page())),
|
||||||
|
ActionItem('طلبات السحب', Icons.request_quote_rounded, cs.warning,
|
||||||
|
() => Get.to(() => const WithdrawalRequestsPage())),
|
||||||
|
ActionItem('إدارة المطاعم', Icons.storefront_rounded, cs.tertiary,
|
||||||
|
() => Get.to(() => const FoodMerchantsPage())),
|
||||||
ActionItem('المحفظة', Icons.account_balance_wallet_rounded, const Color(0xFF6366F1),
|
ActionItem('المحفظة', Icons.account_balance_wallet_rounded, const Color(0xFF6366F1),
|
||||||
() => Get.to(() => Wallet())),
|
() => Get.to(() => Wallet())),
|
||||||
ActionItem('هدية 300', Icons.card_giftcard_rounded, cs.warning,
|
ActionItem('هدية 300', Icons.card_giftcard_rounded, cs.warning,
|
||||||
@@ -1654,6 +1660,7 @@ class _GlowOrb extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ class LiveAnalyticsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final c = Get.put(LiveAnalyticsController());
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final c = Get.put(LiveAnalyticsController());
|
||||||
|
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
@@ -308,8 +308,8 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
super.build(context);
|
||||||
final c = widget.ctrl;
|
final c = widget.ctrl;
|
||||||
final rt = c.realtime;
|
final rt = c.realtime;
|
||||||
|
|
||||||
|
|||||||
@@ -304,30 +304,20 @@ class CaptainsPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
// شارة مصمتة بتدرّج وظل لكل صف تُثقل القائمة بصرياً؛
|
||||||
|
// النمط الموحّد شارة خفيفة بحدّ رفيع.
|
||||||
Container(
|
Container(
|
||||||
width: 52,
|
width: 52,
|
||||||
height: 52,
|
height: 52,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
color: cs.primary.withValues(alpha: 0.12),
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
cs.primary.withValues(alpha: 0.8),
|
|
||||||
cs.primary,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: [
|
border: Border.all(
|
||||||
BoxShadow(
|
color: cs.primary.withValues(alpha: 0.25)),
|
||||||
color: cs.primary.withValues(alpha: 0.2),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.person_rounded,
|
Icons.person_rounded,
|
||||||
color: Colors.white,
|
color: cs.primary,
|
||||||
size: 26,
|
size: 26,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/colors.dart';
|
|
||||||
import '../../../controller/admin/captain_admin_controller.dart';
|
import '../../../controller/admin/captain_admin_controller.dart';
|
||||||
import '../../../main.dart'; // Import main to access myPhone
|
import '../../../main.dart'; // Import main to access myPhone
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
@@ -18,6 +17,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final Map<String, dynamic> data = Get.arguments['data'];
|
final Map<String, dynamic> data = Get.arguments['data'];
|
||||||
final controller = Get.find<CaptainAdminController>();
|
final controller = Get.find<CaptainAdminController>();
|
||||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
@@ -76,7 +76,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_buildDetailTile(Icons.star_rate_rounded, 'Rating',
|
_buildDetailTile(Icons.star_rate_rounded, 'Rating',
|
||||||
'${data['ratingPassenger'] ?? 0.0} / 5.0',
|
'${data['ratingPassenger'] ?? 0.0} / 5.0',
|
||||||
valueColor: cs.warning[700]),
|
valueColor: cs.warning),
|
||||||
_buildDetailTile(Icons.directions_car_filled_outlined,
|
_buildDetailTile(Icons.directions_car_filled_outlined,
|
||||||
'Total Rides', data['countPassengerRide']),
|
'Total Rides', data['countPassengerRide']),
|
||||||
_buildDetailTile(Icons.cancel_outlined,
|
_buildDetailTile(Icons.cancel_outlined,
|
||||||
@@ -101,25 +101,20 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
// --- Header with Gradient Background ---
|
// --- Header with Gradient Background ---
|
||||||
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 25),
|
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||||
|
// كان Colors.white ثابتاً: بطاقة بيضاء فوق خلفية داكنة في الوضع الداكن.
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: cs.surface,
|
||||||
boxShadow: [
|
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||||
BoxShadow(
|
|
||||||
color: cs.onSurfaceVariant.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(30)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 45,
|
radius: 45,
|
||||||
backgroundColor: AppColor.primaryColor.withValues(alpha: 0.1),
|
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||||
child: Text(
|
child: Text(
|
||||||
data['first_name'] != null
|
data['first_name'] != null
|
||||||
? data['first_name'][0].toUpperCase()
|
? data['first_name'][0].toUpperCase()
|
||||||
@@ -127,7 +122,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 35,
|
fontSize: 35,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColor.primaryColor),
|
color: cs.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -147,7 +142,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Active Captain'.tr,
|
'Active Captain'.tr,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: cs.success,
|
color: cs.success,
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600),
|
||||||
@@ -162,25 +157,20 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
{required String title,
|
{required String title,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required List<Widget> children}) {
|
required List<Widget> children}) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: cs.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
border: Border.all(color: cs.outline),
|
||||||
BoxShadow(
|
|
||||||
color: cs.onSurfaceVariant.withValues(alpha: 0.05),
|
|
||||||
spreadRadius: 2,
|
|
||||||
blurRadius: 10)
|
|
||||||
],
|
|
||||||
border: Border.all(color: cs.onSurfaceVariant.withValues(alpha: 0.1)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: AppColor.primaryColor, size: 22),
|
Icon(icon, color: cs.primary, size: 22),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(title.tr,
|
Text(title.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -196,6 +186,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
||||||
{Color? valueColor}) {
|
{Color? valueColor}) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -234,6 +225,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
CaptainAdminController controller,
|
CaptainAdminController controller,
|
||||||
Map<String, dynamic> data,
|
Map<String, dynamic> data,
|
||||||
bool isSuperAdmin) {
|
bool isSuperAdmin) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Driver Scorecard Button
|
// Driver Scorecard Button
|
||||||
@@ -262,12 +254,12 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.notifications_active_outlined,
|
icon: Icon(Icons.notifications_active_outlined,
|
||||||
color: Colors.white),
|
color: cs.onPrimary),
|
||||||
label: Text("Send Notification".tr,
|
label: Text("Send Notification".tr,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 16)),
|
style: TextStyle(color: cs.onPrimary, fontSize: 16)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColor.primaryColor,
|
backgroundColor: cs.primary,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
@@ -285,10 +277,10 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.edit_note_rounded, size: 20),
|
icon: const Icon(Icons.edit_note_rounded, size: 20),
|
||||||
label: Text("Edit".tr),
|
label: Text("Edit".tr),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: cs.warning.withValues(alpha: 0.10),
|
||||||
foregroundColor: AppColor.yellowColor,
|
foregroundColor: cs.warning,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
side: BorderSide(color: AppColor.yellowColor),
|
side: BorderSide(color: cs.warning.withValues(alpha: 0.4)),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
@@ -307,7 +299,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
||||||
label: Text("Delete".tr),
|
label: Text("Delete".tr),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: cs.danger[50],
|
backgroundColor: cs.danger,
|
||||||
foregroundColor: cs.danger,
|
foregroundColor: cs.danger,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -351,6 +343,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
void _showSendNotificationDialog(
|
void _showSendNotificationDialog(
|
||||||
CaptainAdminController controller, Map<String, dynamic> data) {
|
CaptainAdminController controller, Map<String, dynamic> data) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Send Notification'.tr,
|
title: 'Send Notification'.tr,
|
||||||
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
@@ -395,15 +388,16 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
cancel: TextButton(
|
cancel: TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: Text('Cancel'.tr, style: const TextStyle(color: cs.onSurfaceVariant))),
|
child: Text('Cancel'.tr, style: TextStyle(color: cs.onSurfaceVariant))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Confirm Deletion'.tr,
|
title: 'Confirm Deletion'.tr,
|
||||||
titleStyle:
|
titleStyle:
|
||||||
const TextStyle(color: cs.danger, fontWeight: FontWeight.bold),
|
TextStyle(color: cs.danger, fontWeight: FontWeight.bold),
|
||||||
middleText:
|
middleText:
|
||||||
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
||||||
.tr,
|
.tr,
|
||||||
@@ -419,7 +413,7 @@ class CaptainDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
cancel: TextButton(
|
cancel: TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: Text('Cancel'.tr, style: const TextStyle(color: cs.onSurfaceVariant))),
|
child: Text('Cancel'.tr, style: TextStyle(color: cs.onSurfaceVariant))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DriverDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
controller.getDriverDetails(driverId);
|
controller.getDriverDetails(driverId);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class RegisterCaptain extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final controller = Get.put(RegisterCaptainController());
|
final controller = Get.put(RegisterCaptainController());
|
||||||
// String text = '';
|
// String text = '';
|
||||||
controller.driveInit();
|
controller.driveInit();
|
||||||
@@ -532,6 +533,7 @@ Important notes:
|
|||||||
}
|
}
|
||||||
|
|
||||||
GetBuilder<RegisterCaptainController> egyptCarLicenceFront() {
|
GetBuilder<RegisterCaptainController> egyptCarLicenceFront() {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return GetBuilder<RegisterCaptainController>(
|
return GetBuilder<RegisterCaptainController>(
|
||||||
builder: (ai) {
|
builder: (ai) {
|
||||||
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
||||||
@@ -680,6 +682,7 @@ Please fill in the JSON object with the extracted information, following these g
|
|||||||
}
|
}
|
||||||
|
|
||||||
GetBuilder<RegisterCaptainController> egyptCarLicenceBack() {
|
GetBuilder<RegisterCaptainController> egyptCarLicenceBack() {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return GetBuilder<RegisterCaptainController>(
|
return GetBuilder<RegisterCaptainController>(
|
||||||
builder: (ai) {
|
builder: (ai) {
|
||||||
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class DashboardStatCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
// Attempt to use AppStyle.boxDecoration1 properties if it's a BoxDecoration
|
// Attempt to use AppStyle.boxDecoration1 properties if it's a BoxDecoration
|
||||||
BoxDecoration? baseDecoration = AppStyle.boxDecoration1;
|
BoxDecoration? baseDecoration = AppStyle.boxDecoration1;
|
||||||
Color? finalBackgroundColor =
|
Color? finalBackgroundColor =
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class DriverGiftCheckPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = Get.put(DriverGiftCheckerController());
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final controller = Get.put(DriverGiftCheckerController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
|
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -164,7 +165,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.access_time,
|
Icon(Icons.access_time,
|
||||||
color: cs.onSurfaceVariant, size: 12),
|
color: cs.onSurfaceVariant, size: 12),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
@@ -198,7 +199,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete_forever,
|
icon: Icon(Icons.delete_forever,
|
||||||
color: cs.danger),
|
color: cs.danger),
|
||||||
tooltip: "Clear Paid Storage",
|
tooltip: "Clear Paid Storage",
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
@@ -209,7 +210,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
ctrl.fetchData();
|
ctrl.fetchData();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh,
|
icon: Icon(Icons.refresh,
|
||||||
color: cs.info),
|
color: cs.info),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.white10),
|
backgroundColor: Colors.white10),
|
||||||
@@ -267,16 +268,16 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Search by phone number...',
|
hintText: 'Search by phone number...',
|
||||||
prefixIcon:
|
prefixIcon:
|
||||||
const Icon(Icons.search, color: cs.onSurfaceVariant),
|
Icon(Icons.search, color: cs.onSurfaceVariant),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: cs.surfaceContainerHighest,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: cs.onSurfaceVariant.shade200),
|
borderSide: BorderSide(color: cs.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -347,10 +348,11 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(String title, String value, Color color) {
|
Widget _buildStatCard(String title, String value, Color color) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: cs.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border(right: BorderSide(color: color, width: 4)),
|
border: Border(right: BorderSide(color: color, width: 4)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -365,7 +367,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: cs.onSurfaceVariant,
|
color: cs.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
@@ -382,6 +384,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildDriverCard(BuildContext context, Map driver, int index,
|
Widget _buildDriverCard(BuildContext context, Map driver, int index,
|
||||||
DriverCacheController controller) {
|
DriverCacheController controller) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
double hours = _calculateHoursFromStr(driver['active_time']);
|
double hours = _calculateHoursFromStr(driver['active_time']);
|
||||||
String driverId = driver['id']?.toString() ?? 'null';
|
String driverId = driver['id']?.toString() ?? 'null';
|
||||||
bool isPaid = controller.isDriverPaid(driverId);
|
bool isPaid = controller.isDriverPaid(driverId);
|
||||||
@@ -404,7 +407,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Override colors if paid
|
// Override colors if paid
|
||||||
Color cardBackground = isPaid ? cs.tertiary.shade50 : Colors.white;
|
Color cardBackground = isPaid ? cs.tertiary : Colors.white;
|
||||||
Color borderColor = isPaid ? cs.tertiary : Colors.transparent;
|
Color borderColor = isPaid ? cs.tertiary : Colors.transparent;
|
||||||
|
|
||||||
// Calculate progress (max assumed 60 hours for 100% bar)
|
// Calculate progress (max assumed 60 hours for 100% bar)
|
||||||
@@ -469,13 +472,13 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: isPaid
|
color: isPaid
|
||||||
? cs.tertiary.shade900
|
? cs.tertiary
|
||||||
: const Color(0xFF334155)),
|
: const Color(0xFF334155)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
driver['phone'] ?? 'N/A',
|
driver['phone'] ?? 'N/A',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: cs.onSurfaceVariant),
|
color: cs.onSurfaceVariant),
|
||||||
@@ -545,7 +548,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Pay Gift Button (The specific request)
|
// Pay Gift Button (The specific request)
|
||||||
isPaid
|
isPaid
|
||||||
? const Text("Payment Completed",
|
? Text("Payment Completed",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.tertiary, fontWeight: FontWeight.bold))
|
color: cs.tertiary, fontWeight: FontWeight.bold))
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
@@ -571,6 +574,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showPayDialog(Map driver, DriverCacheController controller) {
|
void _showPayDialog(Map driver, DriverCacheController controller) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
// Check for valid ID immediately
|
// Check for valid ID immediately
|
||||||
String driverId = driver['driver_id']?.toString() ?? '';
|
String driverId = driver['driver_id']?.toString() ?? '';
|
||||||
String phone = driver['phone']?.toString() ?? '';
|
String phone = driver['phone']?.toString() ?? '';
|
||||||
@@ -589,7 +593,7 @@ class DriverTheBestRedesigned extends StatelessWidget {
|
|||||||
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
|
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.wallet_giftcard, size: 50, color: cs.info),
|
Icon(Icons.wallet_giftcard, size: 50, color: cs.info),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'Sending gift to ${driver['name_arabic']}',
|
'Sending gift to ${driver['name_arabic']}',
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ class EmployeePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(EmployeeController());
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
Get.put(EmployeeController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
@@ -131,6 +131,7 @@ class _EmployeeCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
bool isExcellent = employee['status'].toString().contains('ممتاز');
|
bool isExcellent = employee['status'].toString().contains('ممتاز');
|
||||||
Color statusColor = isExcellent ? cs.success : cs.warning;
|
Color statusColor = isExcellent ? cs.success : cs.warning;
|
||||||
|
|
||||||
@@ -428,6 +429,7 @@ class _UploadButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onPressed,
|
onTap: onPressed,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ class _ErrorTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final isDriver = item.userType.toLowerCase().contains('driver') ||
|
final isDriver = item.userType.toLowerCase().contains('driver') ||
|
||||||
item.userType.toLowerCase().contains('سائق');
|
item.userType.toLowerCase().contains('سائق');
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildMainFinancialStats(ctrl.stats, cs),
|
_buildMainFinancialStats(ctrl.stats, cs),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
_buildSectionTitle('محفظة سيرو', cs),
|
||||||
|
_buildSiroWalletSection(ctrl.walletSummary, cs),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildSectionTitle('طرق الدفع', cs),
|
_buildSectionTitle('طرق الدفع', cs),
|
||||||
_buildPaymentMethodBreakdown(ctrl.stats, cs),
|
_buildPaymentMethodBreakdown(ctrl.stats, cs),
|
||||||
@@ -78,6 +81,116 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// تنسيق مبلغ بفواصل آلاف وخانتين عشريتين.
|
||||||
|
/// بلا رمز عملة عمداً: هذا النشر أردني بينما بطاقات هذه الشاشة الأخرى
|
||||||
|
/// ما زالت تكتب "ج.م" ثابتة — راجع صحّتها قبل توحيد الرمز.
|
||||||
|
String _money(double v) {
|
||||||
|
final neg = v < 0;
|
||||||
|
final parts = v.abs().toStringAsFixed(2).split('.');
|
||||||
|
final digits = parts[0];
|
||||||
|
final buf = StringBuffer();
|
||||||
|
for (int i = 0; i < digits.length; i++) {
|
||||||
|
if (i > 0 && (digits.length - i) % 3 == 0) buf.write(',');
|
||||||
|
buf.write(digits[i]);
|
||||||
|
}
|
||||||
|
return '${neg ? '-' : ''}$buf.${parts[1]}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// محفظة سيرو من خادم المدفوعات: الشهر الجاري مقابل الشهر السابق،
|
||||||
|
/// والإجمالي التراكمي، مع فصل الإيداع عن الصرف.
|
||||||
|
Widget _buildSiroWalletSection(
|
||||||
|
Map<String, dynamic> summary, ColorScheme cs) {
|
||||||
|
if (summary.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.account_balance_wallet_outlined,
|
||||||
|
color: cs.onSurfaceVariant, size: 18),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'تعذّر جلب ملخّص المحفظة من خادم المدفوعات',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final mtd = (summary['month_to_date'] as Map?) ?? {};
|
||||||
|
final allTime = (summary['all_time'] as Map?) ?? {};
|
||||||
|
final growth = summary['growth_percent'];
|
||||||
|
|
||||||
|
double n(dynamic v) => double.tryParse('${v ?? 0}') ?? 0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildFinancialCard(
|
||||||
|
'صافي هذا الشهر',
|
||||||
|
_money(n(mtd['net'])),
|
||||||
|
Icons.calendar_month_rounded,
|
||||||
|
cs.primary,
|
||||||
|
cs,
|
||||||
|
// المؤشّر يظهر فقط عند وجود شهر سابق يُقارن به
|
||||||
|
subtitle: growth == null
|
||||||
|
? null
|
||||||
|
: '${n(growth) >= 0 ? '▲' : '▼'} ${n(growth).abs().toStringAsFixed(1)}% عن الشهر السابق',
|
||||||
|
subtitleColor: growth == null
|
||||||
|
? null
|
||||||
|
: (n(growth) >= 0 ? cs.success : cs.danger),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _buildFinancialCard(
|
||||||
|
'الإجمالي التراكمي',
|
||||||
|
_money(n(allTime['net'])),
|
||||||
|
Icons.savings_rounded,
|
||||||
|
cs.tertiary,
|
||||||
|
cs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildFinancialCard(
|
||||||
|
'إيداع هذا الشهر',
|
||||||
|
_money(n(mtd['credits'])),
|
||||||
|
Icons.south_west_rounded,
|
||||||
|
cs.success,
|
||||||
|
cs,
|
||||||
|
subtitle: '${mtd['tx_count'] ?? 0} حركة',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _buildFinancialCard(
|
||||||
|
'صرف هذا الشهر',
|
||||||
|
_money(n(mtd['debits'])),
|
||||||
|
Icons.north_east_rounded,
|
||||||
|
cs.warning,
|
||||||
|
cs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
@@ -134,7 +247,7 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildFinancialCard(
|
Widget _buildFinancialCard(
|
||||||
String title, String value, IconData icon, Color color, ColorScheme cs,
|
String title, String value, IconData icon, Color color, ColorScheme cs,
|
||||||
{bool isSmall = false}) {
|
{bool isSmall = false, String? subtitle, Color? subtitleColor}) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(isSmall ? 16 : 24),
|
padding: EdgeInsets.all(isSmall ? 16 : 24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -170,6 +283,14 @@ class FinancialV2Page extends StatelessWidget {
|
|||||||
color: cs.onSurface,
|
color: cs.onSurface,
|
||||||
fontSize: isSmall ? 18 : 24,
|
fontSize: isSmall ? 18 : 24,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
color: subtitleColor ?? cs.onSurfaceVariant,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:siro_admin/constant/theme.dart';
|
||||||
|
import 'package:siro_admin/controller/admin/withdrawal_controller.dart';
|
||||||
|
|
||||||
|
/// شاشة طلبات سحب أرصدة السائقين.
|
||||||
|
/// تتبع لغة تصميم شاشة الرحلات: أسطح محايدة، واللون للمعنى وحده.
|
||||||
|
class WithdrawalRequestsPage extends StatelessWidget {
|
||||||
|
const WithdrawalRequestsPage({super.key});
|
||||||
|
|
||||||
|
static const _filters = <String, String>{
|
||||||
|
'pending': 'معلّقة',
|
||||||
|
'approved': 'معتمدة',
|
||||||
|
'paid': 'مدفوعة',
|
||||||
|
'rejected': 'مرفوضة',
|
||||||
|
'all': 'الكل',
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
body: GetBuilder<WithdrawalController>(
|
||||||
|
init: Get.isRegistered<WithdrawalController>()
|
||||||
|
? Get.find<WithdrawalController>()
|
||||||
|
: WithdrawalController(),
|
||||||
|
builder: (ctrl) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(context, ctrl, cs),
|
||||||
|
_buildTotals(ctrl, cs),
|
||||||
|
_buildFilterBar(ctrl, cs),
|
||||||
|
Expanded(
|
||||||
|
child: ctrl.isLoading
|
||||||
|
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||||
|
: _buildList(ctrl, cs),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(
|
||||||
|
BuildContext context, WithdrawalController ctrl, ColorScheme cs) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surface,
|
||||||
|
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Get.back(),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: cs.onSurfaceVariant, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('طلبات السحب',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh_rounded, color: cs.onSurfaceVariant),
|
||||||
|
onPressed: () => ctrl.fetchRequests(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTotals(WithdrawalController ctrl, ColorScheme cs) {
|
||||||
|
if (ctrl.totals.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
Widget tile(String label, String key, Color color) {
|
||||||
|
final t = ctrl.totals[key] as Map? ?? {};
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.22)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('${t['count'] ?? 0}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w800)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(label,
|
||||||
|
style:
|
||||||
|
TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
tile('معلّقة', 'pending', cs.warning),
|
||||||
|
tile('معتمدة', 'approved', cs.info),
|
||||||
|
tile('مدفوعة', 'paid', cs.success),
|
||||||
|
tile('مرفوضة', 'rejected', cs.danger),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterBar(WithdrawalController ctrl, ColorScheme cs) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
children: _filters.entries.map((e) {
|
||||||
|
final selected = ctrl.filter == e.key;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => ctrl.changeFilter(e.key),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected
|
||||||
|
? cs.primary.withValues(alpha: 0.12)
|
||||||
|
: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? cs.primary : cs.outline),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
e.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected ? cs.primary : cs.onSurfaceVariant,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildList(WithdrawalController ctrl, ColorScheme cs) {
|
||||||
|
if (ctrl.requests.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.inbox_rounded, color: cs.onSurfaceVariant, size: 40),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text('لا توجد طلبات في هذه الحالة',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 24),
|
||||||
|
itemCount: ctrl.requests.length,
|
||||||
|
itemBuilder: (_, i) => _buildCard(ctrl, ctrl.requests[i], cs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(WithdrawalController ctrl, dynamic r, ColorScheme cs) {
|
||||||
|
final status = '${r['status'] ?? 'pending'}';
|
||||||
|
final color = switch (status) {
|
||||||
|
'approved' => cs.info,
|
||||||
|
'paid' => cs.success,
|
||||||
|
'rejected' => cs.danger,
|
||||||
|
_ => cs.warning,
|
||||||
|
};
|
||||||
|
final id = int.tryParse('${r['id']}') ?? 0;
|
||||||
|
final busy = ctrl.busyId == id;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text('${r['driver_name'] ?? 'سائق غير معروف'}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Text(_filters[status] ?? status,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.payments_rounded, size: 14, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text('${r['amount'] ?? 0}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w800)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Icon(Icons.account_balance_wallet_outlined,
|
||||||
|
size: 14, color: cs.onSurfaceVariant),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${r['wallet_type'] ?? ''} — ${r['wallet_number'] ?? ''}',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style:
|
||||||
|
TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text('#$id · ${r['created_at'] ?? ''}',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
|
||||||
|
|
||||||
|
// الإجراءات تظهر بحسب ما يقبله الخادم: من pending إلى أي حالة،
|
||||||
|
// ومن approved إلى paid فقط. أي انتقال آخر يرفضه الخادم بـ 409.
|
||||||
|
if (status == 'pending' || status == 'approved') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (status == 'pending') ...[
|
||||||
|
_action(ctrl, id, 'approved', 'اعتماد', cs.info, cs, busy),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_action(ctrl, id, 'rejected', 'رفض', cs.danger, cs, busy),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
_action(ctrl, id, 'paid', 'تم الدفع', cs.success, cs, busy),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _action(WithdrawalController ctrl, int id, String status, String label,
|
||||||
|
Color color, ColorScheme cs, bool busy) {
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: busy ? null : () => ctrl.updateStatus(id, status),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 9),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: busy ? 0.05 : 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: color.withValues(alpha: busy ? 0.15 : 0.35)),
|
||||||
|
),
|
||||||
|
child: busy
|
||||||
|
? SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: color),
|
||||||
|
)
|
||||||
|
: Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:siro_admin/constant/theme.dart';
|
||||||
|
import 'package:siro_admin/controller/admin/food_merchants_controller.dart';
|
||||||
|
|
||||||
|
/// إدارة المطاعم — قائمة، تصفية بالحالة، واعتماد/رفض الطلبات المعلّقة.
|
||||||
|
/// تتبع لغة تصميم شاشة الرحلات: أسطح محايدة واللون للمعنى وحده.
|
||||||
|
class FoodMerchantsPage extends StatelessWidget {
|
||||||
|
const FoodMerchantsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: cs.surface,
|
||||||
|
body: GetBuilder<FoodMerchantsController>(
|
||||||
|
init: Get.isRegistered<FoodMerchantsController>()
|
||||||
|
? Get.find<FoodMerchantsController>()
|
||||||
|
: FoodMerchantsController(),
|
||||||
|
builder: (ctrl) => Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(ctrl, cs),
|
||||||
|
_buildFilterBar(ctrl, cs),
|
||||||
|
Expanded(
|
||||||
|
child: ctrl.isLoading
|
||||||
|
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||||
|
: _buildList(ctrl, cs),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(FoodMerchantsController ctrl, ColorScheme cs) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surface,
|
||||||
|
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Get.back(),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: cs.onSurfaceVariant, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('إدارة المطاعم',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
const Spacer(),
|
||||||
|
Text('${ctrl.merchants.length}',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.refresh_rounded, color: cs.onSurfaceVariant),
|
||||||
|
onPressed: () => ctrl.fetchMerchants(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterBar(FoodMerchantsController ctrl, ColorScheme cs) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
children: FoodMerchantsController.statuses.entries.map((e) {
|
||||||
|
final selected = ctrl.filter == e.key;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => ctrl.changeFilter(e.key),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected
|
||||||
|
? cs.primary.withValues(alpha: 0.12)
|
||||||
|
: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border:
|
||||||
|
Border.all(color: selected ? cs.primary : cs.outline),
|
||||||
|
),
|
||||||
|
child: Text(e.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected ? cs.primary : cs.onSurfaceVariant,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildList(FoodMerchantsController ctrl, ColorScheme cs) {
|
||||||
|
if (ctrl.merchants.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.storefront_outlined,
|
||||||
|
color: cs.onSurfaceVariant, size: 40),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text('لا توجد مطاعم في هذه الحالة',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 24),
|
||||||
|
itemCount: ctrl.merchants.length,
|
||||||
|
itemBuilder: (_, i) => _buildCard(ctrl, ctrl.merchants[i], cs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(FoodMerchantsController ctrl, dynamic m, ColorScheme cs) {
|
||||||
|
final status = '${m['status'] ?? ''}';
|
||||||
|
final color = switch (status) {
|
||||||
|
'active' => cs.success,
|
||||||
|
'pending_approval' => cs.warning,
|
||||||
|
'rejected' || 'suspended' => cs.danger,
|
||||||
|
_ => cs.onSurfaceVariant,
|
||||||
|
};
|
||||||
|
final id = int.tryParse('${m['id']}') ?? 0;
|
||||||
|
final busy = ctrl.busyId == id;
|
||||||
|
final ratingCount = int.tryParse('${m['rating_count'] ?? 0}') ?? 0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cs.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.storefront_rounded, color: color, size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('${m['name_ar'] ?? m['name_en'] ?? 'بلا اسم'}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${m['city'] ?? ''}${m['category'] != null ? ' · ${m['category']}' : ''}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant, fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
FoodMerchantsController.statuses[status] ?? status,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.percent_rounded,
|
||||||
|
size: 13, color: cs.onSurfaceVariant),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('عمولة ${m['commission_percent'] ?? 0}%',
|
||||||
|
style:
|
||||||
|
TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Icon(Icons.star_rounded, size: 13, color: cs.warning),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// بلا تقييمات نعرض شرطة بدل 0.0 حتى لا يُقرأ كتقييم سيئ
|
||||||
|
Text(
|
||||||
|
ratingCount == 0
|
||||||
|
? 'لا تقييمات'
|
||||||
|
: '${m['rating_avg'] ?? 0} ($ratingCount)',
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (status == 'pending_approval') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_action(ctrl, id, 'approve', 'اعتماد', cs.success, busy),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_action(ctrl, id, 'reject', 'رفض', cs.danger, busy),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _action(FoodMerchantsController ctrl, int id, String action,
|
||||||
|
String label, Color color, bool busy) {
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: busy ? null : () => ctrl.decide(id, action),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 9),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: busy ? 0.05 : 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: color.withValues(alpha: busy ? 0.15 : 0.35)),
|
||||||
|
),
|
||||||
|
child: busy
|
||||||
|
? SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2, color: color),
|
||||||
|
)
|
||||||
|
: Text(label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchHeatmapData() async {
|
Future<void> _fetchHeatmapData() async {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
final queryParams = {
|
final queryParams = {
|
||||||
@@ -364,6 +365,7 @@ class _LegendItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import '../../widgets/elevated_btn.dart';
|
|||||||
import 'passenger_details_page.dart';
|
import 'passenger_details_page.dart';
|
||||||
|
|
||||||
GetBuilder<PassengerAdminController> formSearchPassengers() {
|
GetBuilder<PassengerAdminController> formSearchPassengers() {
|
||||||
// DbSql sql = DbSql.instance;
|
// دالة عليا بلا صنف ولا BuildContext في نطاقها؛ GetMaterialApp يوفّر
|
||||||
|
// context عاماً عبر Get.context.
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return GetBuilder<PassengerAdminController>(
|
return GetBuilder<PassengerAdminController>(
|
||||||
builder: (controller) => Column(
|
builder: (controller) => Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -74,7 +76,7 @@ GetBuilder<PassengerAdminController> formSearchPassengers() {
|
|||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.clear,
|
Icons.clear,
|
||||||
color: cs.danger[300],
|
color: cs.danger,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final Map<String, dynamic> data = Get.arguments['data'];
|
final Map<String, dynamic> data = Get.arguments['data'];
|
||||||
final controller = Get.find<PassengerAdminController>();
|
final controller = Get.find<PassengerAdminController>();
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
Icons.star_rate_rounded,
|
Icons.star_rate_rounded,
|
||||||
'Rating',
|
'Rating',
|
||||||
'${data['ratingPassenger'] ?? 0.0}',
|
'${data['ratingPassenger'] ?? 0.0}',
|
||||||
valueColor: cs.warning[700],
|
valueColor: cs.warning,
|
||||||
),
|
),
|
||||||
_buildDetailTile(
|
_buildDetailTile(
|
||||||
Icons.directions_car_filled_outlined,
|
Icons.directions_car_filled_outlined,
|
||||||
@@ -132,6 +133,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
// --- Header with Gradient/White Background ---
|
// --- Header with Gradient/White Background ---
|
||||||
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
String firstName = data['first_name'] ?? '';
|
String firstName = data['first_name'] ?? '';
|
||||||
String lastName = data['last_name'] ?? '';
|
String lastName = data['last_name'] ?? '';
|
||||||
String fullName = '$firstName $lastName'.trim();
|
String fullName = '$firstName $lastName'.trim();
|
||||||
@@ -181,7 +183,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
data['status'] ?? 'Active',
|
data['status'] ?? 'Active',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: cs.info,
|
color: cs.info,
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600),
|
||||||
@@ -196,6 +198,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
{required String title,
|
{required String title,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required List<Widget> children}) {
|
required List<Widget> children}) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -230,6 +233,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
||||||
{Color? valueColor}) {
|
{Color? valueColor}) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -269,6 +273,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
PassengerAdminController controller,
|
PassengerAdminController controller,
|
||||||
Map<String, dynamic> data,
|
Map<String, dynamic> data,
|
||||||
bool isSuperAdmin) {
|
bool isSuperAdmin) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// --- Send Notification (For All Admins) ---
|
// --- Send Notification (For All Admins) ---
|
||||||
@@ -321,7 +326,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
||||||
label: Text("Delete".tr),
|
label: Text("Delete".tr),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: cs.danger[50],
|
backgroundColor: cs.danger,
|
||||||
foregroundColor: cs.danger,
|
foregroundColor: cs.danger,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -365,6 +370,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
void _showSendNotificationDialog(
|
void _showSendNotificationDialog(
|
||||||
PassengerAdminController controller, Map<String, dynamic> data) {
|
PassengerAdminController controller, Map<String, dynamic> data) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Send Notification'.tr,
|
title: 'Send Notification'.tr,
|
||||||
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
@@ -406,15 +412,16 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
cancel: TextButton(
|
cancel: TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: Text('Cancel'.tr, style: const TextStyle(color: cs.onSurfaceVariant))),
|
child: Text('Cancel'.tr, style: TextStyle(color: cs.onSurfaceVariant))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Confirm Deletion'.tr,
|
title: 'Confirm Deletion'.tr,
|
||||||
titleStyle:
|
titleStyle:
|
||||||
const TextStyle(color: cs.danger, fontWeight: FontWeight.bold),
|
TextStyle(color: cs.danger, fontWeight: FontWeight.bold),
|
||||||
middleText:
|
middleText:
|
||||||
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
||||||
.tr,
|
.tr,
|
||||||
@@ -448,7 +455,7 @@ class PassengerDetailsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
cancel: TextButton(
|
cancel: TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
child: Text('Cancel'.tr, style: const TextStyle(color: cs.onSurfaceVariant)),
|
child: Text('Cancel'.tr, style: TextStyle(color: cs.onSurfaceVariant)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ class KazanEditorPage extends StatelessWidget {
|
|||||||
'speedPrice': {
|
'speedPrice': {
|
||||||
'label': 'Speed ⚡',
|
'label': 'Speed ⚡',
|
||||||
'icon': Icons.flash_on_rounded,
|
'icon': Icons.flash_on_rounded,
|
||||||
'color': cs.warning.shade700
|
'color': cs.warning
|
||||||
},
|
},
|
||||||
'comfortPrice': {
|
'comfortPrice': {
|
||||||
'label': 'Comfort ❄️',
|
'label': 'Comfort ❄️',
|
||||||
'icon': Icons.chair_rounded,
|
'icon': Icons.chair_rounded,
|
||||||
'color': cs.info.shade700
|
'color': cs.info
|
||||||
},
|
},
|
||||||
'ladyPrice': {
|
'ladyPrice': {
|
||||||
'label': 'Lady 👩',
|
'label': 'Lady 👩',
|
||||||
@@ -180,7 +180,7 @@ class KazanEditorPage extends StatelessWidget {
|
|||||||
'electricPrice': {
|
'electricPrice': {
|
||||||
'label': 'Electric 🔋',
|
'label': 'Electric 🔋',
|
||||||
'icon': Icons.electric_car_rounded,
|
'icon': Icons.electric_car_rounded,
|
||||||
'color': cs.success.shade700
|
'color': cs.success
|
||||||
},
|
},
|
||||||
'vanPrice': {
|
'vanPrice': {
|
||||||
'label': 'Van 🚐',
|
'label': 'Van 🚐',
|
||||||
@@ -190,17 +190,17 @@ class KazanEditorPage extends StatelessWidget {
|
|||||||
'deliveryPrice': {
|
'deliveryPrice': {
|
||||||
'label': 'Delivery 📦',
|
'label': 'Delivery 📦',
|
||||||
'icon': Icons.delivery_dining_rounded,
|
'icon': Icons.delivery_dining_rounded,
|
||||||
'color': cs.warning.shade700
|
'color': cs.warning
|
||||||
},
|
},
|
||||||
'mishwarVipPrice': {
|
'mishwarVipPrice': {
|
||||||
'label': 'Mishwar Vip ⭐',
|
'label': 'Mishwar Vip ⭐',
|
||||||
'icon': Icons.star_rounded,
|
'icon': Icons.star_rounded,
|
||||||
'color': cs.warning.shade900
|
'color': cs.warning
|
||||||
},
|
},
|
||||||
'fixedPrice': {
|
'fixedPrice': {
|
||||||
'label': 'Fixed Price 💰',
|
'label': 'Fixed Price 💰',
|
||||||
'icon': Icons.money_rounded,
|
'icon': Icons.money_rounded,
|
||||||
'color': cs.tertiary.shade700
|
'color': cs.tertiary
|
||||||
},
|
},
|
||||||
'awfarPrice': {
|
'awfarPrice': {
|
||||||
'label': 'Awfar Car 🚗',
|
'label': 'Awfar Car 🚗',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
QualityController controller = Get.put(QualityController());
|
QualityController controller = Get.put(QualityController());
|
||||||
// Fetch data when page opens
|
// Fetch data when page opens
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -57,7 +58,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 40,
|
radius: 40,
|
||||||
backgroundColor: cs.onSurfaceVariant.shade300,
|
backgroundColor: cs.onSurfaceVariant,
|
||||||
child: const Icon(Icons.person,
|
child: const Icon(Icons.person,
|
||||||
size: 50, color: Colors.white),
|
size: 50, color: Colors.white),
|
||||||
),
|
),
|
||||||
@@ -67,11 +68,11 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 22, fontWeight: FontWeight.bold)),
|
fontSize: 22, fontWeight: FontWeight.bold)),
|
||||||
Text('هاتف: ${basicInfo['phone']}',
|
Text('هاتف: ${basicInfo['phone']}',
|
||||||
style: const TextStyle(color: cs.onSurfaceVariant)),
|
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||||
const Divider(height: 30),
|
const Divider(height: 30),
|
||||||
Text('التقييم الشامل (Score)',
|
Text('التقييم الشامل (Score)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18, color: cs.onSurfaceVariant.shade700)),
|
fontSize: 18, color: cs.onSurfaceVariant)),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Stack(
|
Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -82,7 +83,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: overallScore / 100,
|
value: overallScore / 100,
|
||||||
strokeWidth: 10,
|
strokeWidth: 10,
|
||||||
backgroundColor: cs.onSurfaceVariant.shade200,
|
backgroundColor: cs.onSurfaceVariant,
|
||||||
color: scoreColor,
|
color: scoreColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -104,7 +105,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
Card(
|
Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.drive_eta, color: cs.info),
|
leading: Icon(Icons.drive_eta, color: cs.info),
|
||||||
title: const Text('نسبة الإنجاز'),
|
title: const Text('نسبة الإنجاز'),
|
||||||
trailing: Text('${ridesStats['completion_rate']}%',
|
trailing: Text('${ridesStats['completion_rate']}%',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -128,14 +129,14 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.star,
|
Icon(Icons.star,
|
||||||
color: cs.warning, size: 30),
|
color: cs.warning, size: 30),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Text('${rating.toString()}/5.0',
|
Text('${rating.toString()}/5.0',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
const Text('متوسط التقييم',
|
Text('متوسط التقييم',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: cs.onSurfaceVariant)),
|
fontSize: 12, color: cs.onSurfaceVariant)),
|
||||||
],
|
],
|
||||||
@@ -150,7 +151,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.warning,
|
Icon(Icons.warning,
|
||||||
color: cs.danger, size: 30),
|
color: cs.danger, size: 30),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Text('${complaints['total_complaints']} شكوى',
|
Text('${complaints['total_complaints']} شكوى',
|
||||||
@@ -158,7 +159,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
Text('${complaints['open_complaints']} مفتوحة',
|
Text('${complaints['open_complaints']} مفتوحة',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: cs.danger)),
|
fontSize: 12, color: cs.danger)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -217,12 +218,13 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBehaviorRow(String title, String value, IconData icon) {
|
Widget _buildBehaviorRow(String title, String value, IconData icon) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: cs.onSurfaceVariant.shade600),
|
Icon(icon, size: 20, color: cs.onSurfaceVariant),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(title, style: const TextStyle(fontSize: 15)),
|
Text(title, style: const TextStyle(fontSize: 15)),
|
||||||
],
|
],
|
||||||
@@ -234,6 +236,7 @@ class DriverScorecardPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color _getScoreColor(num score) {
|
Color _getScoreColor(num score) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
if (score >= 80) return cs.success;
|
if (score >= 80) return cs.success;
|
||||||
if (score >= 60) return cs.warning;
|
if (score >= 60) return cs.warning;
|
||||||
return cs.danger;
|
return cs.danger;
|
||||||
|
|||||||
@@ -985,8 +985,8 @@ class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
|||||||
'تتبع الرحلة #${widget.ride.rideId}',
|
'تتبع الرحلة #${widget.ride.rideId}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: cs.surface,
|
||||||
foregroundColor: const Color(0xFF2B3674),
|
foregroundColor: cs.onSurface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
@@ -1131,8 +1131,8 @@ class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: FloatingActionButton.small(
|
child: FloatingActionButton.small(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: cs.surface,
|
||||||
foregroundColor: const Color(0xFF2B3674),
|
foregroundColor: cs.onSurface,
|
||||||
onPressed: _fitBounds,
|
onPressed: _fitBounds,
|
||||||
child: const Icon(Icons.center_focus_strong_rounded),
|
child: const Icon(Icons.center_focus_strong_rounded),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class ServerMonitorPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = Get.put(ServerMonitorController());
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final controller = Get.put(ServerMonitorController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
@@ -491,7 +491,7 @@ class _TopProcessesCard extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cs.warning.withValues(alpha: 0.1),
|
color: cs.warning.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8)),
|
borderRadius: BorderRadius.circular(8)),
|
||||||
child: const Icon(Icons.analytics_rounded,
|
child: Icon(Icons.analytics_rounded,
|
||||||
color: cs.warning, size: 18),
|
color: cs.warning, size: 18),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -555,7 +555,7 @@ class _TopProcessesCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
process.usage,
|
process.usage,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: cs.warning,
|
color: cs.warning,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold),
|
||||||
@@ -654,7 +654,7 @@ class _ErrorState extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.cloud_off_rounded,
|
Icon(Icons.cloud_off_rounded,
|
||||||
size: 60, color: cs.danger),
|
size: 60, color: cs.danger),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(controller.errorMessage.value,
|
Text(controller.errorMessage.value,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ class AddStaffPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final controller = Get.put(StaffController());
|
final controller = Get.put(StaffController());
|
||||||
controller.selectedRole = role;
|
controller.selectedRole = role;
|
||||||
final cs = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
|
|||||||
@@ -651,6 +651,7 @@ class _LegendDot extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
// نستخدم نفس الكونترولر للوصول لدالة جلب الملاحظات
|
// نستخدم نفس الكونترولر للوصول لدالة جلب الملاحظات
|
||||||
final controller = Get.find<StaticController>();
|
final controller = Get.find<StaticController>();
|
||||||
|
|
||||||
@@ -31,9 +32,9 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: cs.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
iconTheme: const IconThemeData(color: Colors.black87),
|
iconTheme: IconThemeData(color: cs.onSurface),
|
||||||
),
|
),
|
||||||
body: GetBuilder<StaticController>(
|
body: GetBuilder<StaticController>(
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
@@ -47,10 +48,10 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.note_alt_outlined,
|
Icon(Icons.note_alt_outlined,
|
||||||
size: 80, color: cs.onSurfaceVariant.shade300),
|
size: 80, color: cs.onSurfaceVariant),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text("لا توجد سجلات لهذا اليوم",
|
Text("لا توجد سجلات لهذا اليوم",
|
||||||
style: TextStyle(color: cs.onSurfaceVariant.shade600)),
|
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -102,7 +103,7 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
name.toUpperCase(),
|
name.toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: cs.onSurfaceVariant.shade800,
|
color: cs.onSurfaceVariant,
|
||||||
fontSize: 14),
|
fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 100),
|
const SizedBox(width: 100),
|
||||||
@@ -116,7 +117,7 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
phone,
|
phone,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: cs.onSurfaceVariant.shade800,
|
color: cs.onSurfaceVariant,
|
||||||
fontSize: 14),
|
fontSize: 14),
|
||||||
),
|
),
|
||||||
Icon(Icons.phone)
|
Icon(Icons.phone)
|
||||||
@@ -127,7 +128,7 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
time.split(' ').last, // عرض الوقت فقط
|
time.split(' ').last, // عرض الوقت فقط
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: cs.onSurfaceVariant.shade400, fontSize: 12),
|
color: cs.onSurfaceVariant, fontSize: 12),
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -139,7 +140,7 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
content,
|
content,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: cs.onSurfaceVariant.shade700,
|
color: cs.onSurfaceVariant,
|
||||||
height: 1.5),
|
height: 1.5),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -153,9 +154,10 @@ class DailyNotesView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color _getEmployeeColor(String name) {
|
Color _getEmployeeColor(String name) {
|
||||||
|
final cs = Theme.of(Get.context!).colorScheme;
|
||||||
String n = name.toLowerCase().trim();
|
String n = name.toLowerCase().trim();
|
||||||
if (n.contains('shahd')) return cs.danger;
|
if (n.contains('shahd')) return cs.danger;
|
||||||
if (n.contains('mayar')) return cs.warning.shade700;
|
if (n.contains('mayar')) return cs.warning;
|
||||||
if (n.contains('rama2')) return cs.success;
|
if (n.contains('rama2')) return cs.success;
|
||||||
if (n.contains('rama1')) return cs.info;
|
if (n.contains('rama1')) return cs.info;
|
||||||
return Colors.blueGrey;
|
return Colors.blueGrey;
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ class _SliverHeader extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
expandedHeight: 100,
|
expandedHeight: 100,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
@@ -275,6 +276,7 @@ class _DateBadge extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -351,6 +353,7 @@ class _KpiRow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final items = [
|
final items = [
|
||||||
_KpiItem('الركاب', controller.totalMonthlyPassengers,
|
_KpiItem('الركاب', controller.totalMonthlyPassengers,
|
||||||
Icons.groups_rounded, cs.tertiary),
|
Icons.groups_rounded, cs.tertiary),
|
||||||
@@ -477,6 +480,7 @@ class _ChartCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final allSpots = [...spots, ...?compareSpots];
|
final allSpots = [...spots, ...?compareSpots];
|
||||||
final maxY = _maxY(allSpots);
|
final maxY = _maxY(allSpots);
|
||||||
final interval = _interval(maxY);
|
final interval = _interval(maxY);
|
||||||
@@ -605,6 +609,7 @@ class _DynamicMultiLineCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final employees = controller.employeeData.values.toList();
|
final employees = controller.employeeData.values.toList();
|
||||||
employees.sort((a, b) => getTotal(b).compareTo(getTotal(a)));
|
employees.sort((a, b) => getTotal(b).compareTo(getTotal(a)));
|
||||||
|
|
||||||
@@ -762,6 +767,7 @@ class _LegendChip extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -802,6 +808,7 @@ class _EmployeeLeaderboard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final maxCount = stats.isEmpty ? 1 : stats.first.count;
|
final maxCount = stats.isEmpty ? 1 : stats.first.count;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -929,6 +936,7 @@ class _RankBadge extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final isTop3 = rank <= 3;
|
final isTop3 = rank <= 3;
|
||||||
final medalColors = [
|
final medalColors = [
|
||||||
cs.warning,
|
cs.warning,
|
||||||
@@ -974,6 +982,7 @@ class _ControlBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1101,6 +1110,7 @@ class _LoadingState extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -222,7 +223,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: _imageFile != null
|
color: _imageFile != null
|
||||||
? primaryColor
|
? primaryColor
|
||||||
: cs.onSurfaceVariant.shade300,
|
: cs.onSurfaceVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
style: _imageFile != null
|
style: _imageFile != null
|
||||||
? BorderStyle.solid
|
? BorderStyle.solid
|
||||||
@@ -356,6 +357,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
required String hint,
|
required String hint,
|
||||||
bool isNumber = false,
|
bool isNumber = false,
|
||||||
}) {
|
}) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: isNumber
|
keyboardType: isNumber
|
||||||
@@ -379,7 +381,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: cs.onSurfaceVariant.shade200),
|
borderSide: BorderSide(color: cs.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
|||||||
@@ -132,25 +132,15 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: cs.surface,
|
backgroundColor: cs.surface,
|
||||||
floatingActionButton: Container(
|
// الإجراء الأساسي هو الموضع الوحيد الذي يستحق لوناً ممتلئاً في الشاشة.
|
||||||
decoration: BoxDecoration(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
gradient: LinearGradient(colors: [cs.primary, cs.primary.withValues(alpha: 0.7)]),
|
onPressed: () => Get.to(() => AddInvoicePage()),
|
||||||
borderRadius: BorderRadius.circular(16),
|
label: const Text('إضافة فاتورة',
|
||||||
boxShadow: [
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
BoxShadow(
|
icon: const Icon(Icons.add_rounded),
|
||||||
color: cs.primary.withValues(alpha: 0.3),
|
backgroundColor: cs.primary,
|
||||||
blurRadius: 12,
|
foregroundColor: cs.onPrimary,
|
||||||
offset: const Offset(0, 4))
|
elevation: 0,
|
||||||
],
|
|
||||||
),
|
|
||||||
child: FloatingActionButton.extended(
|
|
||||||
onPressed: () => Get.to(() => AddInvoicePage()),
|
|
||||||
label: const Text('إضافة فاتورة',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
icon: const Icon(Icons.add_rounded),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -188,23 +178,11 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
|||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
),
|
),
|
||||||
|
// سطح محايد بحدّ سفلي بدل الشريحة المتدرّجة بلون primary وظلّها —
|
||||||
|
// نفس المبدأ المطبّق في شاشة الرحلات: اللون للمعنى لا للإطار.
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
color: cs.surface,
|
||||||
begin: Alignment.topLeft,
|
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(24),
|
|
||||||
bottomRight: Radius.circular(24),
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: cs.primary.withValues(alpha: 0.2),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -215,18 +193,19 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: cs.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: cs.outline),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.arrow_back_ios_new_rounded,
|
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
color: Colors.white, size: 16),
|
color: cs.onSurfaceVariant, size: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Text(
|
Text(
|
||||||
"سجل الفواتير",
|
"سجل الفواتير",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: cs.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
@@ -246,7 +225,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
|||||||
isMoney: true,
|
isMoney: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(width: 1, height: 40, color: Colors.white24),
|
Container(width: 1, height: 40, color: cs.outline),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildSummaryItem(
|
child: _buildSummaryItem(
|
||||||
title: "عدد الفواتير",
|
title: "عدد الفواتير",
|
||||||
@@ -268,29 +247,37 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
|||||||
required IconData icon,
|
required IconData icon,
|
||||||
required bool isMoney,
|
required bool isMoney,
|
||||||
}) {
|
}) {
|
||||||
|
// بعد تحييد الرأس لم يعد الأبيض مقروءاً. المبلغ يأخذ لون النجاح
|
||||||
|
// والعدّ يأخذ لون النص الأساسي — اللون هنا يحمل معنى لا زينة.
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final accent = isMoney ? cs.success : cs.primary;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: accent.withValues(alpha: 0.12),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: accent.withValues(alpha: 0.25)),
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: Colors.white, size: 20),
|
child: Icon(icon, color: accent, size: 20),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isMoney ? const Color(0xFFD1FAE5) : Colors.white,
|
color: isMoney ? cs.success : cs.onSurface,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white, fontSize: 11, fontWeight: FontWeight.w500),
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ class GlassContainer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
final defaultGradient = isDark
|
final defaultGradient = isDark
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class _SnackContentState extends State<_SnackContent>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
final v = widget.variant;
|
final v = widget.variant;
|
||||||
final accent = v.baseColor;
|
final accent = v.baseColor;
|
||||||
final surface = v.surfaceColor;
|
final surface = v.surfaceColor;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import 'package:siro_admin/views/admin/staff/add_staff_page.dart';
|
|||||||
import 'package:siro_admin/views/admin/staff/pending_admins_page.dart';
|
import 'package:siro_admin/views/admin/staff/pending_admins_page.dart';
|
||||||
import 'package:siro_admin/views/admin/static/advanced_analytics_page.dart';
|
import 'package:siro_admin/views/admin/static/advanced_analytics_page.dart';
|
||||||
import 'package:siro_admin/views/admin/financial/financial_v2_page.dart';
|
import 'package:siro_admin/views/admin/financial/financial_v2_page.dart';
|
||||||
|
import 'package:siro_admin/views/admin/financial/withdrawal_requests_page.dart';
|
||||||
|
import 'package:siro_admin/views/admin/food/food_merchants_page.dart';
|
||||||
import 'package:siro_admin/views/admin/security/audit_logs_page.dart';
|
import 'package:siro_admin/views/admin/security/audit_logs_page.dart';
|
||||||
import 'package:siro_admin/views/admin/analytics/live_analytics_page.dart';
|
import 'package:siro_admin/views/admin/analytics/live_analytics_page.dart';
|
||||||
|
|
||||||
@@ -178,6 +180,20 @@ class WebSidebar extends StatelessWidget {
|
|||||||
index: 108,
|
index: 108,
|
||||||
onTap: () => Get.to(() => const FinancialV2Page()),
|
onTap: () => Get.to(() => const FinancialV2Page()),
|
||||||
),
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context,
|
||||||
|
title: 'طلبات السحب',
|
||||||
|
icon: Icons.request_quote_rounded,
|
||||||
|
index: 112,
|
||||||
|
onTap: () => Get.to(() => const WithdrawalRequestsPage()),
|
||||||
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context,
|
||||||
|
title: 'إدارة المطاعم',
|
||||||
|
icon: Icons.storefront_rounded,
|
||||||
|
index: 113,
|
||||||
|
onTap: () => Get.to(() => const FoodMerchantsPage()),
|
||||||
|
),
|
||||||
_buildNavItem(
|
_buildNavItem(
|
||||||
context,
|
context,
|
||||||
title: 'المحافظ المالية',
|
title: 'المحافظ المالية',
|
||||||
|
|||||||
Reference in New Issue
Block a user