From d8820efa2479bdbea0038468f6b56c6a882ad736 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 7 May 2026 18:56:48 +0300 Subject: [PATCH] Update: 2026-05-07 18:56:48 --- app/modules_app/dashboard/recent_activity.php | 147 +++++++++++++- .../controllers/dashboard_controller.dart | 143 ++++++++++++- .../dashboard/views/dashboard_view.dart | 191 +++++++++++++----- .../controllers/invoices_controller.dart | 14 +- 4 files changed, 438 insertions(+), 57 deletions(-) diff --git a/app/modules_app/dashboard/recent_activity.php b/app/modules_app/dashboard/recent_activity.php index 29ed2cb..fb046e9 100644 --- a/app/modules_app/dashboard/recent_activity.php +++ b/app/modules_app/dashboard/recent_activity.php @@ -6,6 +6,7 @@ declare(strict_types=1); use App\Core\Database; +use App\Core\Encryption; use App\Middleware\AuthMiddleware; $decoded = AuthMiddleware::check(); @@ -19,13 +20,19 @@ try { $where = "WHERE 1=1"; $params = []; } else { - $where = "WHERE tenant_id = ?"; + $where = "WHERE a.tenant_id = ?"; $params = [$tenantId]; } - // Join with users table to get the name of the person who did the action $stmt = $db->prepare(" - SELECT a.id, a.action, a.entity_type, a.created_at, u.name as user_name + SELECT + a.id, + a.action, + a.entity_type, + a.entity_id, + a.new_data, + a.created_at, + u.name AS user_name FROM audit_logs a LEFT JOIN users u ON a.user_id = u.id $where @@ -35,8 +42,142 @@ try { $stmt->execute($params); $activities = $stmt->fetchAll(); + foreach ($activities as &$activity) { + $activity['user_name'] = decryptIfEncrypted($activity['user_name'] ?? null) ?: 'مستخدم مجهول'; + $activity['details'] = decodeAuditData($activity['new_data'] ?? null); + $activity['summary'] = buildActivitySummary($activity); + unset($activity['new_data']); + } + unset($activity); + json_success($activities); } catch (\Exception $e) { + error_log('Dashboard Recent Activity Error: ' . $e->getMessage()); json_error('Failed to fetch recent activity', 500); } + +function decodeAuditData(?string $json): array +{ + if (!$json) { + return []; + } + + $decoded = json_decode($json, true); + if (!is_array($decoded)) { + return []; + } + + return decryptAuditPayload($decoded); +} + +function decryptAuditPayload(array $payload): array +{ + foreach ($payload as $key => $value) { + if (is_array($value)) { + $payload[$key] = decryptAuditPayload($value); + continue; + } + + if (is_string($value)) { + $payload[$key] = decryptIfEncrypted($value); + } + } + + return $payload; +} + +function decryptIfEncrypted(mixed $value): string +{ + if ($value === null) { + return ''; + } + + $text = trim((string)$value); + if ($text === '' || !looksEncrypted($text)) { + return $text; + } + + try { + $decrypted = Encryption::decrypt($text); + return $decrypted !== false ? $decrypted : $text; + } catch (\Throwable $e) { + return $text; + } +} + +function looksEncrypted(string $value): bool +{ + $normalized = str_starts_with($value, '==') ? substr($value, 2) : $value; + + if (strlen($normalized) < 40 || strlen($normalized) % 4 !== 0) { + return false; + } + + return (bool)preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $normalized); +} + +function buildActivitySummary(array $activity): string +{ + $data = is_array($activity['details'] ?? null) ? $activity['details'] : []; + $action = (string)($activity['action'] ?? ''); + + return match ($action) { + 'payment.created' => buildPaymentSummary('تم إنشاء طلب دفع', $data), + 'payment.approved' => buildPaymentSummary('تم اعتماد طلب دفع', $data), + 'payment.rejected' => buildPaymentSummary('تم رفض طلب دفع', $data), + 'subscription.activated' => buildPaymentSummary('تم تفعيل الاشتراك', $data), + 'invoice.approved' => buildEntitySummary('تم اعتماد الفاتورة', $data, ['invoice_number', 'number']), + 'invoice.extracted' => buildEntitySummary('تم استخراج بيانات الفاتورة', $data, ['invoice_number', 'number']), + 'company.created' => buildEntitySummary('تمت إضافة شركة', $data, ['name', 'company_name']), + 'user.created' => buildEntitySummary('تمت إضافة مستخدم', $data, ['name', 'email']), + default => '', + }; +} + +function buildPaymentSummary(string $label, array $data): string +{ + $parts = [$label]; + + $plan = firstTextValue($data, ['plan_name', 'plan_name_ar', 'plan_id']); + if ($plan !== '') { + $parts[] = "الباقة: {$plan}"; + } + + $amount = firstTextValue($data, ['amount', 'amount_jod', 'price_jod']); + if ($amount !== '') { + $parts[] = "القيمة: {$amount} د.أ"; + } + + $reference = firstTextValue($data, ['ref', 'reference', 'internal_reference']); + if ($reference !== '') { + $parts[] = "المرجع: {$reference}"; + } + + return implode(' - ', $parts); +} + +function buildEntitySummary(string $label, array $data, array $keys): string +{ + $value = firstTextValue($data, $keys); + return $value === '' ? $label : "{$label}: {$value}"; +} + +function firstTextValue(array $data, array $keys): string +{ + foreach ($keys as $key) { + if (!array_key_exists($key, $data) || $data[$key] === null) { + continue; + } + + $value = $data[$key]; + if (is_scalar($value)) { + $text = trim((string)$value); + if ($text !== '') { + return $text; + } + } + } + + return ''; +} diff --git a/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart b/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart index 5926018..57a126a 100644 --- a/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart +++ b/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart @@ -401,6 +401,9 @@ class DashboardController extends GetxController { case 'open_scanner': Get.toNamed(AppRoutes.SCANNER); break; + case 'check_quota': + _showQuotaResult(execution); + break; case 'navigate': final screen = params['screen']?.toString().toLowerCase(); if (screen == 'settings') { @@ -411,7 +414,6 @@ class DashboardController extends GetxController { Get.toNamed(AppRoutes.MAIN); } break; - case 'check_quota': case 'check_status': case 'get_report': case 'export_pdf': @@ -455,6 +457,145 @@ class DashboardController extends GetxController { AppSnackbar.showSuccess('الفواتير', 'تم عرض الفواتير $label$suffix'); } + void _showQuotaResult(Map execution) { + final data = _asStringMap(execution['data']); + if (data.isEmpty) { + AppSnackbar.showWarning('الاشتراك', 'لم تصل تفاصيل الاشتراك من الخادم'); + return; + } + + final planName = (data['plan_name'] ?? 'غير معروف').toString(); + final status = _subscriptionStatusLabel(data['status']); + final days = data['days_remaining']; + + Get.bottomSheet( + Container( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + decoration: BoxDecoration( + color: Get.isDarkMode ? const Color(0xFF1E1E2E) : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 18), + Row( + children: [ + const Icon(Icons.workspace_premium_rounded, + color: Color(0xFFD4AF37)), + const SizedBox(width: 10), + Expanded( + child: Text( + 'اشتراكك الحالي', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: Get.isDarkMode + ? Colors.white + : const Color(0xFF0F172A), + ), + ), + ), + ], + ), + const SizedBox(height: 14), + Text( + '$planName • $status${days == null ? '' : ' • $days يوم متبقي'}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: + Get.isDarkMode ? Colors.white70 : const Color(0xFF334155), + ), + ), + const SizedBox(height: 18), + _buildQuotaRow('الفواتير', data['invoices'], Icons.receipt_long), + const SizedBox(height: 12), + _buildQuotaRow('الشركات', data['companies'], Icons.business), + const SizedBox(height: 12), + _buildQuotaRow('المستخدمين', data['users'], Icons.people), + ], + ), + ), + ), + isScrollControlled: true, + ); + } + + Widget _buildQuotaRow(String label, dynamic raw, IconData icon) { + final item = _asStringMap(raw); + final used = item['used'] ?? 0; + final limit = item['limit'] ?? 0; + final percent = ((item['percent'] as num?) ?? 0).clamp(0, 100).toDouble(); + final warning = item['warning'] == true; + final color = warning ? const Color(0xFFDC2626) : const Color(0xFF0F4C81); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: color), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + Text( + '$used / $limit', + style: TextStyle( + fontWeight: FontWeight.w700, + color: color, + fontFamily: 'monospace', + ), + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: percent / 100, + minHeight: 7, + color: color, + backgroundColor: + Get.isDarkMode ? Colors.white10 : const Color(0xFFE2E8F0), + ), + ), + ], + ); + } + + String _subscriptionStatusLabel(dynamic status) { + switch (status?.toString()) { + case 'trial': + return 'تجريبي'; + case 'active': + return 'فعّال'; + case 'past_due': + return 'متأخر الدفع'; + case 'cancelled': + return 'ملغي'; + default: + return 'غير معروف'; + } + } + void _resetVoiceState() { _recordTimer?.cancel(); isVoiceRecording.value = false; diff --git a/musadaq-app/lib/features/dashboard/views/dashboard_view.dart b/musadaq-app/lib/features/dashboard/views/dashboard_view.dart index cb14624..57033f2 100644 --- a/musadaq-app/lib/features/dashboard/views/dashboard_view.dart +++ b/musadaq-app/lib/features/dashboard/views/dashboard_view.dart @@ -14,22 +14,31 @@ class DashboardView extends GetView { children: [ // Custom Top Bar Container( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top, left: 8, right: 8, bottom: 12), + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top, + left: 8, + right: 8, + bottom: 12), color: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81), child: Row( children: [ const SizedBox(width: 48), - Expanded( + const Expanded( child: Center( child: Text( 'مُصادَق', - style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold), ), ), ), IconButton( - icon: const Icon(Icons.notifications_outlined, color: Colors.white), - onPressed: () => Get.snackbar('قريباً', 'الإشعارات ستتوفر قريباً'), + icon: const Icon(Icons.notifications_outlined, + color: Colors.white), + onPressed: () => + Get.snackbar('قريباً', 'الإشعارات ستتوفر قريباً'), ), IconButton( icon: const Icon(Icons.refresh, color: Colors.white), @@ -38,11 +47,12 @@ class DashboardView extends GetView { ], ), ), - + Expanded( child: Obx(() { if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator(color: Color(0xFF0F4C81))); + return const Center( + child: CircularProgressIndicator(color: Color(0xFF0F4C81))); } final stats = controller.stats; @@ -60,12 +70,16 @@ class DashboardView extends GetView { const SizedBox(height: 24), _buildQuickActions(isDark), const SizedBox(height: 32), - const Text('إحصائيات الفواتير', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('إحصائيات الفواتير', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildInvoiceStats(stats, isDark), if (role == 'admin' || role == 'super_admin') ...[ const SizedBox(height: 24), - const Text('نظرة عامة', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('نظرة عامة', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildRoleSpecificStats(stats, role, isDark), ], @@ -74,7 +88,9 @@ class DashboardView extends GetView { _buildQuotaMeter(stats['subscription'], isDark), ], const SizedBox(height: 32), - const Text('أحدث النشاطات', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('أحدث النشاطات', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 12), _buildRecentActivity(isDark), const SizedBox(height: 40), @@ -91,9 +107,15 @@ class DashboardView extends GetView { Widget _buildWelcomeHeader(String role, bool isDark) { String roleName = 'مستخدم'; switch (role) { - case 'super_admin': roleName = 'مدير النظام'; break; - case 'admin': roleName = 'مدير المكتب'; break; - case 'accountant': roleName = 'محاسب'; break; + case 'super_admin': + roleName = 'مدير النظام'; + break; + case 'admin': + roleName = 'مدير المكتب'; + break; + case 'accountant': + roleName = 'محاسب'; + break; } return Row( @@ -107,8 +129,12 @@ class DashboardView extends GetView { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('مرحباً بك في مُصادَق 👋', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - Text('صلاحيات: $roleName', style: TextStyle(color: isDark ? Colors.white38 : Colors.grey, fontSize: 14)), + const Text('مرحباً بك في مُصادَق 👋', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + Text('صلاحيات: $roleName', + style: TextStyle( + color: isDark ? Colors.white38 : Colors.grey, + fontSize: 14)), ], ), ], @@ -121,12 +147,14 @@ class DashboardView extends GetView { Expanded( child: ElevatedButton.icon( icon: const Icon(Icons.document_scanner), - label: const Text('المسح الضوئي', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + label: const Text('المسح الضوئي', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0F4C81), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), ), onPressed: () => Get.toNamed(AppRoutes.SCANNER), ), @@ -135,12 +163,18 @@ class DashboardView extends GetView { Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.mic_rounded), - label: const Text('المساعد الصوتي', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + label: const Text('المساعد الصوتي', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), style: OutlinedButton.styleFrom( - foregroundColor: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81), - side: BorderSide(color: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81)), + foregroundColor: + isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81), + side: BorderSide( + color: isDark + ? const Color(0xFF5EEAD4) + : const Color(0xFF0F4C81)), padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), ), onPressed: () { controller.startVoiceAssistant(); @@ -155,11 +189,14 @@ class DashboardView extends GetView { final inv = stats['invoices'] ?? {'total': 0, 'pending': 0, 'approved': 0}; return Row( children: [ - _buildStatCard('الكل', inv['total'].toString(), Icons.receipt_long, Colors.blue, isDark), + _buildStatCard('الكل', inv['total'].toString(), Icons.receipt_long, + Colors.blue, isDark), const SizedBox(width: 12), - _buildStatCard('قيد المعالجة', inv['pending'].toString(), Icons.hourglass_empty, Colors.orange, isDark), + _buildStatCard('قيد المعالجة', inv['pending'].toString(), + Icons.hourglass_empty, Colors.orange, isDark), const SizedBox(width: 12), - _buildStatCard('معتمدة', inv['approved'].toString(), Icons.check_circle, Colors.green, isDark), + _buildStatCard('معتمدة', inv['approved'].toString(), Icons.check_circle, + Colors.green, isDark), ], ); } @@ -168,38 +205,49 @@ class DashboardView extends GetView { if (role == 'super_admin') { return Row( children: [ - _buildStatCard('المستأجرين', (stats['tenants'] ?? 0).toString(), Icons.business_center, Colors.indigo, isDark), + _buildStatCard('المستأجرين', (stats['tenants'] ?? 0).toString(), + Icons.business_center, Colors.indigo, isDark), const SizedBox(width: 12), - _buildStatCard('المستخدمين', (stats['total_users'] ?? 0).toString(), Icons.people, Colors.purple, isDark), + _buildStatCard('المستخدمين', (stats['total_users'] ?? 0).toString(), + Icons.people, Colors.purple, isDark), ], ); } else { return Row( children: [ - _buildStatCard('الشركات', (stats['companies'] ?? 0).toString(), Icons.business, Colors.indigo, isDark), + _buildStatCard('الشركات', (stats['companies'] ?? 0).toString(), + Icons.business, Colors.indigo, isDark), const SizedBox(width: 12), - _buildStatCard('المستخدمين', (stats['users'] ?? 0).toString(), Icons.people, Colors.purple, isDark), + _buildStatCard('المستخدمين', (stats['users'] ?? 0).toString(), + Icons.people, Colors.purple, isDark), ], ); } } - Widget _buildStatCard(String title, String count, IconData icon, Color color, bool isDark) { + Widget _buildStatCard( + String title, String count, IconData icon, Color color, bool isDark) { return Expanded( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E2E) : Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), + border: + Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 28), const SizedBox(height: 12), - Text(count, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), - Text(title, style: TextStyle(color: isDark ? Colors.white38 : Colors.grey, fontSize: 12)), + Text(count, + style: + const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + Text(title, + style: TextStyle( + color: isDark ? Colors.white38 : Colors.grey, + fontSize: 12)), ], ), ), @@ -210,18 +258,20 @@ class DashboardView extends GetView { int limit = subscription['limit'] ?? 100; int used = subscription['used'] ?? 0; double progress = limit > 0 ? (used / limit) : 0; - + return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E2E) : Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), + border: + Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('استهلاك الباقة الشهرية (AI)', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('استهلاك الباقة الشهرية (AI)', + style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 12), LinearProgressIndicator( value: progress, @@ -234,8 +284,12 @@ class DashboardView extends GetView { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('$used فاتورة', style: const TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF0F4C81))), - Text('من $limit', style: TextStyle(color: isDark ? Colors.white38 : Colors.grey)), + Text('$used فاتورة', + style: const TextStyle( + fontWeight: FontWeight.bold, color: Color(0xFF0F4C81))), + Text('من $limit', + style: + TextStyle(color: isDark ? Colors.white38 : Colors.grey)), ], ) ], @@ -245,7 +299,9 @@ class DashboardView extends GetView { Widget _buildRecentActivity(bool isDark) { if (controller.recentActivities.isEmpty) { - return Center(child: Text('لا توجد نشاطات حديثة', style: TextStyle(color: isDark ? Colors.white38 : Colors.grey))); + return Center( + child: Text('لا توجد نشاطات حديثة', + style: TextStyle(color: isDark ? Colors.white38 : Colors.grey))); } return ListView.builder( @@ -259,16 +315,23 @@ class DashboardView extends GetView { elevation: 0, color: isDark ? const Color(0xFF1E1E2E) : Colors.white, shape: RoundedRectangleBorder( - side: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade200), + side: BorderSide( + color: isDark ? Colors.white10 : Colors.grey.shade200), borderRadius: BorderRadius.circular(12), ), child: ListTile( leading: CircleAvatar( - backgroundColor: isDark ? Colors.white10 : const Color(0xFFF1F5F9), - child: Icon(_getActivityIcon(act['action']), color: const Color(0xFF64748B), size: 18), + backgroundColor: + isDark ? Colors.white10 : const Color(0xFFF1F5F9), + child: Icon(_getActivityIcon(act['action']), + color: const Color(0xFF64748B), size: 18), ), title: Text(_formatAction(act['action'])), - subtitle: Text('بواسطة: ${act['user_name'] ?? 'مستخدم مجهول'}'), + subtitle: Text( + _activitySubtitle(act), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), trailing: Text( _timeAgo(act['created_at']), style: const TextStyle(fontSize: 12, color: Colors.grey), @@ -290,17 +353,45 @@ class DashboardView extends GetView { String _formatAction(String action) { switch (action) { - case 'invoice.approved': return 'اعتماد فاتورة'; - case 'invoice.extracted': return 'استخراج بيانات فاتورة'; - case 'company.created': return 'إضافة شركة'; - case 'company.deleted': return 'حذف شركة'; - case 'user.created': return 'إضافة مستخدم'; - case 'user.deleted': return 'حذف مستخدم'; - case 'user.login': return 'تسجيل دخول'; - default: return action; + case 'invoice.approved': + return 'اعتماد فاتورة'; + case 'invoice.extracted': + return 'استخراج بيانات فاتورة'; + case 'company.created': + return 'إضافة شركة'; + case 'company.deleted': + return 'حذف شركة'; + case 'user.created': + return 'إضافة مستخدم'; + case 'user.deleted': + return 'حذف مستخدم'; + case 'user.login': + return 'تسجيل دخول'; + case 'payment.created': + return 'إنشاء طلب دفع'; + case 'payment.approved': + return 'اعتماد طلب دفع'; + case 'payment.rejected': + return 'رفض طلب دفع'; + case 'subscription.activated': + return 'تفعيل اشتراك'; + default: + return action; } } + String _activitySubtitle(dynamic act) { + final userName = (act['user_name'] ?? '').toString().trim(); + final summary = (act['summary'] ?? '').toString().trim(); + final byUser = 'بواسطة: ${userName.isEmpty ? 'مستخدم مجهول' : userName}'; + + if (summary.isEmpty) { + return byUser; + } + + return '$summary\n$byUser'; + } + String _timeAgo(String datetime) { try { final dt = DateTime.parse(datetime); diff --git a/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart b/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart index f0cd6ac..996bc0e 100644 --- a/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart +++ b/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart @@ -65,16 +65,24 @@ class InvoicesController extends GetxController { String _normalizeStatus(dynamic status) { final value = status?.toString().toLowerCase().trim() ?? ''; + final aliases = { + 'ready': 'extracted', + 'جاهزة': 'extracted', + 'pending': 'uploaded', + 'processing': 'uploaded', + 'قيد المعالجة': 'uploaded', + 'معتمدة': 'approved', + }; + final normalized = aliases[value] ?? value; + const supported = { 'all', 'approved', 'extracted', 'uploaded', - 'processing', - 'pending', 'rejected', }; - return supported.contains(value) ? value : 'all'; + return supported.contains(normalized) ? normalized : 'all'; } }