From 528b3ca247af23abf0e03a41dc40a8e89967ce95 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 7 May 2026 18:41:16 +0300 Subject: [PATCH] Update: 2026-05-07 18:41:16 --- musadaq-app/lib/app/routes/app_pages.dart | 5 +- .../controllers/dashboard_controller.dart | 57 ++++-- .../controllers/invoices_controller.dart | 27 ++- .../controllers/main_shell_controller.dart | 9 + .../main_shell/views/main_shell_view.dart | 167 +++++++++++------- 5 files changed, 186 insertions(+), 79 deletions(-) create mode 100644 musadaq-app/lib/features/main_shell/controllers/main_shell_controller.dart diff --git a/musadaq-app/lib/app/routes/app_pages.dart b/musadaq-app/lib/app/routes/app_pages.dart index ecc5ed6..48b1c2b 100644 --- a/musadaq-app/lib/app/routes/app_pages.dart +++ b/musadaq-app/lib/app/routes/app_pages.dart @@ -4,8 +4,8 @@ import '../../features/auth/views/phone_input_view.dart'; import '../../features/auth/views/otp_verify_view.dart'; import '../../features/auth/views/biometric_setup_view.dart'; import '../../features/auth/views/biometric_auth_view.dart'; +import '../../features/main_shell/controllers/main_shell_controller.dart'; import '../../features/main_shell/views/main_shell_view.dart'; -import '../../features/dashboard/views/dashboard_view.dart'; import '../../features/dashboard/controllers/dashboard_controller.dart'; import '../../features/scanner/views/scanner_view.dart'; import '../../features/scanner/controllers/scanner_controller.dart'; @@ -21,6 +21,7 @@ import '../../core/storage/secure_storage.dart'; part 'app_routes.dart'; class AppPages { + // ignore: constant_identifier_names static const INITIAL = AppRoutes.SPLASH; static final routes = [ @@ -87,6 +88,7 @@ class AppPages { name: AppRoutes.MAIN, page: () => const MainShellView(), binding: BindingsBuilder(() { + Get.put(MainShellController()); Get.put(DashboardController()); Get.put(InvoicesController()); Get.put(SettingsController()); @@ -97,6 +99,7 @@ class AppPages { name: AppRoutes.DASHBOARD, page: () => const MainShellView(), // Now redirects to MainShell binding: BindingsBuilder(() { + Get.put(MainShellController()); Get.put(DashboardController()); Get.put(InvoicesController()); Get.put(SettingsController()); diff --git a/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart b/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart index 9d6842e..5926018 100644 --- a/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart +++ b/musadaq-app/lib/features/dashboard/controllers/dashboard_controller.dart @@ -13,6 +13,8 @@ import '../../../core/services/voice_assistant_service.dart'; import '../../../core/utils/app_snackbar.dart'; import '../../../core/utils/logger.dart'; import '../../../app/routes/app_pages.dart'; +import '../../invoices/controllers/invoices_controller.dart'; +import '../../main_shell/controllers/main_shell_controller.dart'; class DashboardController extends GetxController { final SecureStorage _storage = SecureStorage(); @@ -339,9 +341,7 @@ class DashboardController extends GetxController { ); final action = (intent['action'] ?? '').toString(); - final params = Map.from( - (intent['params'] as Map?) ?? {}, - ); + final params = _asStringMap(intent['params']); final confirmation = (intent['confirmation'] ?? execution['message'] ?? 'تم تنفيذ الأمر الصوتي') @@ -356,7 +356,7 @@ class DashboardController extends GetxController { (execution['message'] ?? 'تعذر تنفيذ الأمر داخليًا').toString(), ); } else { - _executeAction(action, params); + _executeAction(action, params, execution); } } on DioException catch (e) { AppLogger.error('Voice upload error', e.response?.data ?? e.message); @@ -381,20 +381,28 @@ class DashboardController extends GetxController { } } - void _executeAction(String action, dynamic params) { + Map _asStringMap(dynamic value) { + if (value is Map) { + return Map.from(value); + } + return {}; + } + + void _executeAction( + String action, + Map params, + Map execution, + ) { switch (action) { case 'list_invoices': case 'search_invoice': - Get.toNamed(AppRoutes.MAIN); - AppSnackbar.showWarning( - 'معلومة', 'تم فتح الواجهة الرئيسية؛ عرض نتائج تفصيلي قادم'); + _openInvoicesFromVoice(params, execution); break; case 'open_scanner': Get.toNamed(AppRoutes.SCANNER); break; case 'navigate': - final screen = - (params is Map ? params['screen'] : null)?.toString().toLowerCase(); + final screen = params['screen']?.toString().toLowerCase(); if (screen == 'settings') { Get.toNamed(AppRoutes.MAIN); } else if (screen == 'scanner') { @@ -418,6 +426,35 @@ class DashboardController extends GetxController { } } + void _openInvoicesFromVoice( + Map params, + Map execution, + ) { + final invoicesController = Get.isRegistered() + ? Get.find() + : Get.put(InvoicesController()); + + invoicesController.applyVoiceFilters(params); + + if (Get.isRegistered()) { + Get.find().selectTab(1); + } else { + Get.offAllNamed(AppRoutes.MAIN); + } + + final data = execution['data']; + final count = data is Map ? data['count'] : null; + final status = params['status']?.toString(); + final suffix = count == null ? '' : ' ($count)'; + final label = status == 'approved' + ? 'المعتمدة' + : status == 'extracted' + ? 'الجاهزة' + : 'المطابقة'; + + AppSnackbar.showSuccess('الفواتير', 'تم عرض الفواتير $label$suffix'); + } + void _resetVoiceState() { _recordTimer?.cancel(); isVoiceRecording.value = false; diff --git a/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart b/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart index e5385c2..f0cd6ac 100644 --- a/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart +++ b/musadaq-app/lib/features/invoices/controllers/invoices_controller.dart @@ -26,14 +26,24 @@ class InvoicesController extends GetxController { final q = searchQuery.value.toLowerCase(); list = list.where((inv) { final name = (inv['supplier_name'] ?? '').toString().toLowerCase(); + final company = (inv['company_name'] ?? '').toString().toLowerCase(); final num = (inv['invoice_number'] ?? '').toString().toLowerCase(); - return name.contains(q) || num.contains(q); + return name.contains(q) || company.contains(q) || num.contains(q); }).toList(); } return list; } + void applyVoiceFilters(Map params) { + filterStatus.value = _normalizeStatus(params['status']); + + final query = + (params['number'] ?? params['company'] ?? '').toString().trim(); + searchQuery.value = query; + isSearching.value = query.isNotEmpty; + } + void toggleSearch() { isSearching.value = !isSearching.value; if (!isSearching.value) searchQuery.value = ''; @@ -52,4 +62,19 @@ class InvoicesController extends GetxController { isLoading.value = false; } } + + String _normalizeStatus(dynamic status) { + final value = status?.toString().toLowerCase().trim() ?? ''; + const supported = { + 'all', + 'approved', + 'extracted', + 'uploaded', + 'processing', + 'pending', + 'rejected', + }; + + return supported.contains(value) ? value : 'all'; + } } diff --git a/musadaq-app/lib/features/main_shell/controllers/main_shell_controller.dart b/musadaq-app/lib/features/main_shell/controllers/main_shell_controller.dart new file mode 100644 index 0000000..1de09a8 --- /dev/null +++ b/musadaq-app/lib/features/main_shell/controllers/main_shell_controller.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; + +class MainShellController extends GetxController { + final currentIndex = 0.obs; + + void selectTab(int index) { + currentIndex.value = index; + } +} diff --git a/musadaq-app/lib/features/main_shell/views/main_shell_view.dart b/musadaq-app/lib/features/main_shell/views/main_shell_view.dart index 0897e1c..3d22d5f 100644 --- a/musadaq-app/lib/features/main_shell/views/main_shell_view.dart +++ b/musadaq-app/lib/features/main_shell/views/main_shell_view.dart @@ -4,9 +4,9 @@ import '../../dashboard/views/dashboard_view.dart'; import '../../invoices/views/invoices_list_view.dart'; import '../../notifications/views/notifications_view.dart'; import '../../settings/views/settings_view.dart'; +import '../controllers/main_shell_controller.dart'; import '../../../app/routes/app_pages.dart'; import '../../../core/services/upload_progress_service.dart'; -import '../../../core/utils/app_snackbar.dart'; class MainShellView extends StatefulWidget { const MainShellView({super.key}); @@ -16,41 +16,45 @@ class MainShellView extends StatefulWidget { } class _MainShellViewState extends State { - int _currentIndex = 0; - final UploadProgressService _progressService = Get.put(UploadProgressService()); + final MainShellController _shellController = Get.find(); + final UploadProgressService _progressService = + Get.put(UploadProgressService()); // 5 pages: Home(0), Invoices(1), [Scanner FAB](2), Notifications(3), Settings(4) final List _pages = const [ - DashboardView(), // 0 - InvoicesListView(), // 1 - SizedBox(), // 2 - Scanner (FAB placeholder) - NotificationsView(), // 3 - SettingsView(), // 4 + DashboardView(), // 0 + InvoicesListView(), // 1 + SizedBox(), // 2 - Scanner (FAB placeholder) + NotificationsView(), // 3 + SettingsView(), // 4 ]; @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final navBg = isDark ? const Color(0xFF1A1A2E) : Colors.white; - final activeColor = const Color(0xFF0F4C81); + const activeColor = Color(0xFF0F4C81); final inactiveColor = isDark ? Colors.white38 : const Color(0xFF94A3B8); return Scaffold( - backgroundColor: isDark ? const Color(0xFF121212) : const Color(0xFFF5F7FA), + backgroundColor: + isDark ? const Color(0xFF121212) : const Color(0xFFF5F7FA), body: Stack( children: [ - IndexedStack( - index: _getPageIndex(_currentIndex), - children: [ - _pages[0], // Dashboard - _pages[1], // Invoices - _pages[3], // Notifications - _pages[4], // Settings - ], + Obx( + () => IndexedStack( + index: _getPageIndex(_shellController.currentIndex.value), + children: [ + _pages[0], // Dashboard + _pages[1], // Invoices + _pages[3], // Notifications + _pages[4], // Settings + ], + ), ), - + // Global Upload Progress Overlay - Obx(() => _progressService.isUploading.value + Obx(() => _progressService.isUploading.value ? Positioned( bottom: 80, left: 16, @@ -73,13 +77,17 @@ class _MainShellViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // Left side (2 items) - _buildNavItem(0, Icons.home_rounded, 'الرئيسية', activeColor, inactiveColor), - _buildNavItem(1, Icons.receipt_long_rounded, 'الفواتير', activeColor, inactiveColor), + _buildNavItem(0, Icons.home_rounded, 'الرئيسية', activeColor, + inactiveColor), + _buildNavItem(1, Icons.receipt_long_rounded, 'الفواتير', + activeColor, inactiveColor), // Center gap for FAB const SizedBox(width: 48), // Right side (2 items) - _buildNavItem(3, Icons.notifications_rounded, 'الإشعارات', activeColor, inactiveColor), - _buildNavItem(4, Icons.settings_rounded, 'الإعدادات', activeColor, inactiveColor), + _buildNavItem(3, Icons.notifications_rounded, 'الإشعارات', + activeColor, inactiveColor), + _buildNavItem(4, Icons.settings_rounded, 'الإعدادات', activeColor, + inactiveColor), ], ), ), @@ -95,37 +103,45 @@ class _MainShellViewState extends State { return 0; } - Widget _buildNavItem(int index, IconData icon, String label, Color active, Color inactive) { - final isSelected = _currentIndex == index; + Widget _buildNavItem( + int index, IconData icon, String label, Color active, Color inactive) { return Expanded( - child: InkWell( - onTap: () => setState(() => _currentIndex = index), - borderRadius: BorderRadius.circular(12), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), - decoration: isSelected ? BoxDecoration( - color: active.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ) : null, - child: Icon(icon, color: isSelected ? active : inactive, size: 22), - ), - const SizedBox(height: 2), - Text( - label, - style: TextStyle( - fontSize: 10, - fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400, - color: isSelected ? active : inactive, + child: Obx(() { + final isSelected = _shellController.currentIndex.value == index; + + return InkWell( + onTap: () => _shellController.selectTab(index), + borderRadius: BorderRadius.circular(12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 2), + decoration: isSelected + ? BoxDecoration( + color: active.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + ) + : null, + child: + Icon(icon, color: isSelected ? active : inactive, size: 22), ), - ), - ], - ), - ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400, + color: isSelected ? active : inactive, + ), + ), + ], + ), + ); + }), ); } @@ -142,7 +158,7 @@ class _MainShellViewState extends State { ), boxShadow: [ BoxShadow( - color: const Color(0xFFD4AF37).withOpacity(0.4), + color: const Color(0xFFD4AF37).withValues(alpha: 0.4), blurRadius: 12, offset: const Offset(0, 4), ), @@ -153,7 +169,8 @@ class _MainShellViewState extends State { backgroundColor: Colors.transparent, elevation: 0, heroTag: 'scanner_fab', - child: const Icon(Icons.document_scanner_rounded, color: Colors.white, size: 26), + child: const Icon(Icons.document_scanner_rounded, + color: Colors.white, size: 26), ), ); } @@ -161,11 +178,14 @@ class _MainShellViewState extends State { Widget _buildUploadOverlay(bool isDark) { final status = _progressService.status.value; final progress = _progressService.progress.value; - - Color accentColor = status == 'done' ? const Color(0xFF10B981) : const Color(0xFF0F4C81); - String statusText = status == 'uploading' - ? 'جاري رفع الصور...' - : (status == 'processing' ? 'جاري استخراج البيانات...' : 'اكتملت المعالجة ✓'); + + Color accentColor = + status == 'done' ? const Color(0xFF10B981) : const Color(0xFF0F4C81); + String statusText = status == 'uploading' + ? 'جاري رفع الصور...' + : (status == 'processing' + ? 'جاري استخراج البيانات...' + : 'اكتملت المعالجة ✓'); return Card( elevation: 8, @@ -179,25 +199,37 @@ class _MainShellViewState extends State { children: [ Row( children: [ - status == 'done' - ? const Icon(Icons.check_circle, color: Color(0xFF10B981), size: 24) - : const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF0F4C81))), + status == 'done' + ? const Icon(Icons.check_circle, + color: Color(0xFF10B981), size: 24) + : const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, color: Color(0xFF0F4C81))), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(statusText, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + Text(statusText, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 13)), Text( '${_progressService.companyName.value} • ${_progressService.currentImageIndex.value}/${_progressService.totalImages.value}', - style: TextStyle(fontSize: 11, color: isDark ? Colors.white38 : Colors.grey), + style: TextStyle( + fontSize: 11, + color: isDark ? Colors.white38 : Colors.grey), ), ], ), ), Text( '${(progress * 100).toInt()}%', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: accentColor), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: accentColor), ), ], ), @@ -206,7 +238,8 @@ class _MainShellViewState extends State { borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress, - backgroundColor: isDark ? Colors.white10 : const Color(0xFFE2E8F0), + backgroundColor: + isDark ? Colors.white10 : const Color(0xFFE2E8F0), color: accentColor, minHeight: 6, ),