import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import '../../../core/storage/secure_storage.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/services/voice_assistant_service.dart'; import '../../../core/utils/app_snackbar.dart'; import '../../../core/utils/logger.dart'; import '../../../app/routes/app_pages.dart'; class DashboardController extends GetxController { final SecureStorage _storage = SecureStorage(); final Dio _dio = DioClient().client; final VoiceAssistantService _voiceAssistantService = VoiceAssistantService(); final AudioRecorder _audioRecorder = AudioRecorder(); Timer? _recordTimer; var isLoading = true.obs; var stats = {}.obs; var recentActivities = [].obs; var userRole = ''.obs; final isVoiceRecording = false.obs; final isVoiceUploading = false.obs; final voiceRemainingSeconds = 15.obs; final voiceStatusText = 'اضغط "ابدأ التسجيل" ثم تحدّث'.obs; @override void onInit() { super.onInit(); _loadDashboardData(); } @override void onClose() { _recordTimer?.cancel(); _audioRecorder.dispose(); super.onClose(); } Future _loadDashboardData() async { try { isLoading.value = true; final token = await _storage.getToken(); if (token == null || token.isEmpty) { Get.offAllNamed(AppRoutes.PHONE_INPUT); return; } // Fetch Stats final statsResponse = await _dio.get('dashboard/stats'); if (statsResponse.data['success'] == true) { stats.value = statsResponse.data['data']; userRole.value = statsResponse.data['data']['role'] ?? ''; } // Fetch Recent Activity final activityResponse = await _dio.get('dashboard/recent-activity'); if (activityResponse.data['success'] == true) { recentActivities.value = activityResponse.data['data']; } } on DioException catch (e) { AppLogger.error('Dashboard Data Fetch Error', e); if (e.response?.statusCode == 401 || e.response?.statusCode == 403) { await logout(); } else { AppSnackbar.showError( 'خطأ', 'فشل في جلب البيانات. الرجاء التحقق من اتصالك بالإنترنت.'); } } catch (e) { AppLogger.error('Unexpected error fetching dashboard', e); } finally { isLoading.value = false; } } Future logout() async { await _storage.clearAll(); Get.offAllNamed(AppRoutes.PHONE_INPUT); } void startVoiceAssistant() { _resetVoiceState(); Get.bottomSheet( Obx( () => Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Get.isDarkMode ? const Color(0xFF1E1E2E) : Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), Row( children: [ const Icon(Icons.auto_awesome, color: Color(0xFF5EEAD4)), const SizedBox(width: 12), Text( 'المساعد الصوتي (Gemini)', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Get.isDarkMode ? Colors.white : const Color(0xFF0F4C81), ), ), ], ), const SizedBox(height: 18), Text( voiceStatusText.value, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: Get.isDarkMode ? Colors.white70 : Colors.black87, ), ), const SizedBox(height: 14), Text( '${voiceRemainingSeconds.value}s', style: TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.bold, fontSize: 18, color: isVoiceRecording.value ? const Color(0xFFDC2626) : const Color(0xFF0F4C81), ), ), const SizedBox(height: 12), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: (15 - voiceRemainingSeconds.value) / 15, minHeight: 8, color: isVoiceRecording.value ? const Color(0xFFDC2626) : const Color(0xFF0F4C81), backgroundColor: Get.isDarkMode ? Colors.white10 : const Color(0xFFE2E8F0), ), ), const SizedBox(height: 20), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: isVoiceUploading.value ? null : () async { if (isVoiceRecording.value) { await _stopAndUploadVoice(autoStopped: false); } else { await _startVoiceRecording(); } }, icon: Icon( isVoiceRecording.value ? Icons.stop_circle_outlined : Icons.mic_none_rounded, ), label: Text( isVoiceRecording.value ? 'إيقاف وإرسال' : 'ابدأ التسجيل', ), style: OutlinedButton.styleFrom( foregroundColor: isVoiceRecording.value ? const Color(0xFFDC2626) : const Color(0xFF0F4C81), side: BorderSide( color: isVoiceRecording.value ? const Color(0xFFDC2626) : const Color(0xFF0F4C81), ), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: isVoiceRecording.value || isVoiceUploading.value ? null : () => Get.back(), icon: const Icon(Icons.close), label: const Text('إغلاق'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0F4C81), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), if (isVoiceUploading.value) ...[ const SizedBox(height: 14), const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ], ], ), ), ), isScrollControlled: true, isDismissible: false, ); } Future _startVoiceRecording() async { if (isVoiceUploading.value) return; final hasPermission = await _audioRecorder.hasPermission(); if (!hasPermission) { AppSnackbar.showError('صلاحيات', 'الرجاء السماح بالوصول للميكروفون'); return; } final tempDir = await getTemporaryDirectory(); final path = '${tempDir.path}/voice_${DateTime.now().millisecondsSinceEpoch}.m4a'; await _audioRecorder.start( const RecordConfig( encoder: AudioEncoder.aacLc, sampleRate: 16000, numChannels: 1, bitRate: 64000, ), path: path, ); isVoiceRecording.value = true; voiceRemainingSeconds.value = 15; voiceStatusText.value = 'جاري التسجيل... تكلم الآن'; _startVoiceCountdown(); } void _startVoiceCountdown() { _recordTimer?.cancel(); _recordTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final next = voiceRemainingSeconds.value - 1; voiceRemainingSeconds.value = next; if (next <= 0) { timer.cancel(); _stopAndUploadVoice(autoStopped: true); } }); } Future _stopAndUploadVoice({required bool autoStopped}) async { if (!isVoiceRecording.value) return; _recordTimer?.cancel(); isVoiceRecording.value = false; voiceStatusText.value = autoStopped ? 'تم الإيقاف التلقائي، جاري الإرسال...' : 'جاري الإرسال...'; final recordPath = await _audioRecorder.stop(); if (recordPath == null || recordPath.isEmpty) { voiceStatusText.value = 'فشل حفظ التسجيل الصوتي'; AppSnackbar.showError('خطأ', 'فشل حفظ التسجيل الصوتي'); return; } await _uploadVoiceCommand(File(recordPath)); } Future _uploadVoiceCommand(File audioFile) async { isVoiceUploading.value = true; try { final result = await _voiceAssistantService.processVoiceCommand(audioFile); final intent = Map.from( (result['intent'] as Map?) ?? {}, ); final execution = Map.from( (result['execution'] as Map?) ?? {}, ); final action = (intent['action'] ?? '').toString(); final params = Map.from( (intent['params'] as Map?) ?? {}, ); final confirmation = (intent['confirmation'] ?? execution['message'] ?? 'تم تنفيذ الأمر الصوتي') .toString(); final executionStatus = (execution['status'] ?? 'unknown').toString(); AppSnackbar.showSuccess('المساعد الصوتي', confirmation); if (executionStatus == 'failed') { AppSnackbar.showError( 'فشل التنفيذ', (execution['message'] ?? 'تعذر تنفيذ الأمر داخليًا').toString(), ); } else { _executeAction(action, params); } } on DioException catch (e) { AppLogger.error('Voice upload error', e.response?.data ?? e.message); String errorMsg = 'تعذر إرسال التسجيل الصوتي'; if (e.response != null && e.response?.data != null) { if (e.response?.data is Map && e.response?.data['message'] != null) { errorMsg = e.response?.data['message']; } } AppSnackbar.showError('خطأ', errorMsg); } catch (e, stack) { AppLogger.error('Voice Assistant Error', e, stack); AppSnackbar.showError('خطأ', 'حدث خطأ غير متوقع'); } finally { isVoiceUploading.value = false; voiceStatusText.value = 'جاهز لأمر صوتي جديد'; // Close voice sheet if still open. if (Get.isBottomSheetOpen == true) { Get.back(); } } } void _executeAction(String action, dynamic params) { switch (action) { case 'list_invoices': case 'search_invoice': Get.toNamed(AppRoutes.MAIN); AppSnackbar.showWarning( 'معلومة', 'تم فتح الواجهة الرئيسية؛ عرض نتائج تفصيلي قادم'); break; case 'open_scanner': Get.toNamed(AppRoutes.SCANNER); break; case 'navigate': final screen = (params is Map ? params['screen'] : null)?.toString().toLowerCase(); if (screen == 'settings') { Get.toNamed(AppRoutes.MAIN); } else if (screen == 'scanner') { Get.toNamed(AppRoutes.SCANNER); } else { Get.toNamed(AppRoutes.MAIN); } break; case 'check_quota': case 'check_status': case 'get_report': case 'export_pdf': AppSnackbar.showWarning( 'تم التنفيذ', 'تم تنفيذ الأمر على الخادم وسيتم تحسين عرض النتائج داخل التطبيق قريباً', ); break; default: AppSnackbar.showWarning( 'تنبيه', 'الأمر مفهوم ولكن لم يتم ربط الأكشن برمجياً بعد.'); } } void _resetVoiceState() { _recordTimer?.cancel(); isVoiceRecording.value = false; isVoiceUploading.value = false; voiceRemainingSeconds.value = 15; voiceStatusText.value = 'اضغط "ابدأ التسجيل" ثم تحدّث'; } void refreshData() { _loadDashboardData(); } }