diff --git a/musadaq-app/lib/app/routes/app_pages.dart b/musadaq-app/lib/app/routes/app_pages.dart index 48b1c2b..08443fb 100644 --- a/musadaq-app/lib/app/routes/app_pages.dart +++ b/musadaq-app/lib/app/routes/app_pages.dart @@ -17,6 +17,7 @@ import '../../features/invoices/views/invoice_detail_view.dart'; import '../../features/onboarding/views/onboarding_view.dart'; import '../../features/onboarding/controllers/onboarding_controller.dart'; import '../../core/storage/secure_storage.dart'; +import '../../features/companies/views/companies_management_view.dart'; part 'app_routes.dart'; @@ -131,5 +132,9 @@ class AppPages { Get.put(OnboardingController()); }), ), + GetPage( + name: AppRoutes.COMPANIES_MANAGEMENT, + page: () => const CompaniesManagementView(), + ), ]; } diff --git a/musadaq-app/lib/app/routes/app_routes.dart b/musadaq-app/lib/app/routes/app_routes.dart index 7e2d9ae..85c1d1c 100644 --- a/musadaq-app/lib/app/routes/app_routes.dart +++ b/musadaq-app/lib/app/routes/app_routes.dart @@ -17,4 +17,5 @@ abstract class AppRoutes { static const PAYMENT_RECEIPT = '/payment-receipt'; static const INVOICE_DETAIL = '/invoice-detail'; static const ONBOARDING = '/onboarding'; + static const COMPANIES_MANAGEMENT = '/companies-management'; } diff --git a/musadaq-app/lib/features/companies/controllers/add_company_controller.dart b/musadaq-app/lib/features/companies/controllers/add_company_controller.dart new file mode 100644 index 0000000..fdc43cf --- /dev/null +++ b/musadaq-app/lib/features/companies/controllers/add_company_controller.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:dio/dio.dart'; +import '../../../core/network/dio_client.dart'; +import '../../../core/utils/app_snackbar.dart'; +import 'companies_management_controller.dart'; + +class AddCompanyController extends GetxController { + final nameController = TextEditingController(); + final tinController = TextEditingController(); + final crnController = TextEditingController(); + + var isSubmitting = false.obs; + + @override + void onClose() { + nameController.dispose(); + tinController.dispose(); + crnController.dispose(); + super.onClose(); + } + + Future submit() async { + final name = nameController.text.trim(); + final tin = tinController.text.trim(); + + if (name.isEmpty || tin.isEmpty) { + AppSnackbar.showError('خطأ', 'الرجاء إدخال اسم الشركة والرقم الضريبي'); + return; + } + + try { + isSubmitting.value = true; + final dio = DioClient().client; + final response = await dio.post('companies', data: { + 'name': name, + 'tax_identification_number': tin, + 'commercial_registration_number': crnController.text.trim(), + }); + + if (response.data['success'] == true) { + AppSnackbar.showSuccess('نجاح', 'تمت إضافة الشركة بنجاح'); + + // Refresh list if controller exists + if (Get.isRegistered()) { + Get.find().fetchCompanies(); + } + + Get.back(); + } + } on DioException catch (e) { + if (e.response?.statusCode == 403) { + AppSnackbar.showError('خطأ', 'لقد وصلت للحد الأقصى المسموح به للشركات في باقتك'); + } else { + AppSnackbar.showError('خطأ', 'تعذر إضافة الشركة'); + } + } catch (e) { + AppSnackbar.showError('خطأ', 'حدث خطأ غير متوقع'); + } finally { + isSubmitting.value = false; + } + } +} diff --git a/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart b/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart new file mode 100644 index 0000000..ad7a4e9 --- /dev/null +++ b/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart @@ -0,0 +1,47 @@ +import 'package:get/get.dart'; +import 'package:dio/dio.dart'; +import '../../../core/network/dio_client.dart'; +import '../../../core/utils/app_snackbar.dart'; +import '../../../core/utils/logger.dart'; + +class CompaniesManagementController extends GetxController { + final Dio _dio = DioClient().client; + + var isLoading = true.obs; + var companies = >[].obs; + var employees = >[].obs; + + @override + void onInit() { + super.onInit(); + fetchCompanies(); + } + + Future fetchCompanies() async { + try { + isLoading.value = true; + final response = await _dio.get('companies'); + if (response.data['success'] == true) { + companies.value = List>.from(response.data['data']); + } + } catch (e) { + AppLogger.error('Failed to fetch companies', e); + AppSnackbar.showError('خطأ', 'تعذر تحميل قائمة الشركات'); + } finally { + isLoading.value = false; + } + } + + Future deleteCompany(String id) async { + try { + final response = await _dio.delete('companies/$id'); + if (response.data['success'] == true) { + companies.removeWhere((c) => c['id'] == id); + AppSnackbar.showSuccess('نجاح', 'تم حذف الشركة بنجاح'); + } + } catch (e) { + AppLogger.error('Failed to delete company', e); + AppSnackbar.showError('خطأ', 'تعذر حذف الشركة'); + } + } +} diff --git a/musadaq-app/lib/features/companies/views/add_company_view.dart b/musadaq-app/lib/features/companies/views/add_company_view.dart new file mode 100644 index 0000000..1725424 --- /dev/null +++ b/musadaq-app/lib/features/companies/views/add_company_view.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/add_company_controller.dart'; + +class AddCompanyView extends StatelessWidget { + const AddCompanyView({super.key}); + + @override + Widget build(BuildContext context) { + final controller = Get.put(AddCompanyController()); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('إضافة شركة', style: TextStyle(fontFamily: 'El Messiri')), + centerTitle: true, + backgroundColor: const Color(0xFF0F4C81), + foregroundColor: Colors.white, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'بيانات الشركة الأساسية', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 24), + _buildTextField( + controller: controller.nameController, + label: 'اسم الشركة', + icon: Icons.business, + isDark: isDark, + ), + const SizedBox(height: 16), + _buildTextField( + controller: controller.tinController, + label: 'الرقم الضريبي', + icon: Icons.numbers, + keyboardType: TextInputType.number, + isDark: isDark, + ), + const SizedBox(height: 16), + _buildTextField( + controller: controller.crnController, + label: 'رقم السجل التجاري (اختياري)', + icon: Icons.article, + keyboardType: TextInputType.number, + isDark: isDark, + ), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + height: 54, + child: Obx( + () => ElevatedButton( + onPressed: controller.isSubmitting.value ? null : controller.submit, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0F4C81), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + child: controller.isSubmitting.value + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'حفظ وإضافة', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required IconData icon, + TextInputType? keyboardType, + required bool isDark, + }) { + return TextField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: const Color(0xFF0F4C81)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: isDark ? Colors.white24 : Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: isDark ? Colors.white24 : Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF0F4C81), width: 2), + ), + filled: true, + fillColor: isDark ? Colors.white.withValues(alpha: 0.05) : Colors.white, + ), + ); + } +} diff --git a/musadaq-app/lib/features/companies/views/companies_management_view.dart b/musadaq-app/lib/features/companies/views/companies_management_view.dart new file mode 100644 index 0000000..45ce219 --- /dev/null +++ b/musadaq-app/lib/features/companies/views/companies_management_view.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/companies_management_controller.dart'; +import 'add_company_view.dart'; + +class CompaniesManagementView extends StatelessWidget { + const CompaniesManagementView({super.key}); + + @override + Widget build(BuildContext context) { + // Put controller directly so we don't strictly need a binding for this nested route + final controller = Get.put(CompaniesManagementController()); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('إدارة الشركات', style: TextStyle(fontFamily: 'El Messiri')), + centerTitle: true, + backgroundColor: const Color(0xFF0F4C81), + foregroundColor: Colors.white, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + Get.to(() => const AddCompanyView()); + }, + ), + ], + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.companies.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.business_center_outlined, size: 80, color: Colors.grey.shade400), + const SizedBox(height: 16), + const Text('لا يوجد شركات مسجلة', style: TextStyle(fontSize: 18, color: Colors.grey)), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: controller.fetchCompanies, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: controller.companies.length, + itemBuilder: (context, index) { + final company = controller.companies[index]; + return Card( + elevation: 2, + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF0F4C81).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.business, color: Color(0xFF0F4C81)), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + company['name'] ?? 'شركة', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) => [ + const PopupMenuItem(value: 'edit', child: Text('تعديل البيانات')), + const PopupMenuItem(value: 'employees', child: Text('إدارة الموظفين')), + const PopupMenuItem(value: 'delete', child: Text('حذف الشركة', style: TextStyle(color: Colors.red))), + ], + onSelected: (value) { + if (value == 'delete') { + _confirmDelete(context, controller, company['id']); + } else { + Get.snackbar('قريباً', 'الواجهة قيد البرمجة'); + } + }, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _buildDetailChip(Icons.numbers, company['tax_identification_number'] ?? 'غير محدد', isDark), + const SizedBox(width: 8), + if (company['is_jofotara_connected'] == true || company['is_jofotara_connected'] == 1) + _buildDetailChip(Icons.link, 'مرتبطة بجوفوتارا', isDark, color: const Color(0xFF10B981)), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => Get.snackbar('إحصائيات', 'عرض إحصائيات الشركة'), + icon: const Icon(Icons.bar_chart, size: 18), + label: const Text('الإحصائيات'), + ), + ], + ) + ], + ), + ), + ); + }, + ), + ); + }), + ); + } + + Widget _buildDetailChip(IconData icon, String text, bool isDark, {Color? color}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: (color ?? Colors.grey).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color ?? (isDark ? Colors.white70 : Colors.black54)), + const SizedBox(width: 6), + Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color ?? (isDark ? Colors.white70 : Colors.black87), + ), + ), + ], + ), + ); + } + + void _confirmDelete(BuildContext context, CompaniesManagementController controller, String id) { + Get.defaultDialog( + title: 'حذف الشركة', + middleText: 'هل أنت متأكد من رغبتك في حذف هذه الشركة نهائياً؟', + textConfirm: 'حذف', + textCancel: 'إلغاء', + confirmTextColor: Colors.white, + buttonColor: Colors.red, + onConfirm: () { + Get.back(); + controller.deleteCompany(id); + }, + ); + } +} diff --git a/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart b/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart index 8b62b41..771bc01 100644 --- a/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart +++ b/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart @@ -21,6 +21,8 @@ class ScannerController extends GetxController { var processedImagesCount = 0.obs; var totalImagesCount = 0.obs; var isBatchDone = false.obs; + var selectedCompanyId = ''.obs; + var selectedCompanyName = ''.obs; final InvoiceUploadService _uploadService = InvoiceUploadService(); final UploadProgressService _progressService = @@ -90,49 +92,28 @@ class ScannerController extends GetxController { } } - Future uploadBatch(String fallbackCompanyId) async { + Future uploadBatch() async { if (capturedImages.isEmpty) { AppSnackbar.showWarning('تنبيه', 'الرجاء تصوير فاتورة واحدة على الأقل'); return; } + if (selectedCompanyId.isEmpty) { + AppSnackbar.showWarning('تنبيه', 'الرجاء اختيار الشركة أولاً'); + return; + } try { isProcessing.value = true; uploadProgress.value = 0.0; - String companyId = fallbackCompanyId; - String companyName = 'شركة غير محددة'; - - if (companyId == 'mock_company_id_123' || companyId.isEmpty) { - if (companies.isNotEmpty) { - companyId = companies[0]['id']; - companyName = companies[0]['name'] ?? 'شركتي'; - } else { - final res = await DioClient().client.get('companies'); - if (res.data['success'] == true && - res.data['data'] != null && - res.data['data'].isNotEmpty) { - companyId = res.data['data'][0]['id']; - companyName = res.data['data'][0]['name'] ?? 'شركتي'; - } else { - AppSnackbar.showError('خطأ', 'لا توجد شركات مسجلة في حسابك'); - isProcessing.value = false; - return; - } - } - } else { - final comp = companies.firstWhereOrNull((c) => c['id'] == companyId); - if (comp != null) companyName = comp['name'] ?? 'شركتي'; - } - AppLogger.print( - 'Uploading batch of ${capturedImages.length} images to company $companyId...'); + 'Uploading batch of ${capturedImages.length} images to company ${selectedCompanyId.value}...'); // Start global progress - _progressService.startUpload(companyName, capturedImages.length); + _progressService.startUpload(selectedCompanyName.value, capturedImages.length); final batchId = await _uploadService.uploadBatch( - companyId: companyId, + companyId: selectedCompanyId.value, images: capturedImages, onProgress: (current, total) { uploadProgress.value = current / total; @@ -149,6 +130,8 @@ class ScannerController extends GetxController { capturedImages.clear(); uploadProgress.value = 0.0; isProcessing.value = false; + selectedCompanyId.value = ''; + selectedCompanyName.value = ''; _progressService.startProcessing(); Get.back(); // Go back to dashboard, progress will show in overlay @@ -167,6 +150,11 @@ class ScannerController extends GetxController { } } + void selectCompany(String id, String name) { + selectedCompanyId.value = id; + selectedCompanyName.value = name; + } + Future getSavePath() async { final directory = await getTemporaryDirectory(); final fileName = 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg'; diff --git a/musadaq-app/lib/features/scanner/views/scanner_view.dart b/musadaq-app/lib/features/scanner/views/scanner_view.dart index 7b95df2..e9632b4 100644 --- a/musadaq-app/lib/features/scanner/views/scanner_view.dart +++ b/musadaq-app/lib/features/scanner/views/scanner_view.dart @@ -9,6 +9,72 @@ class ScannerView extends GetView { @override Widget build(BuildContext context) { + return Obx(() { + if (controller.selectedCompanyId.value.isEmpty) { + return _buildCompanySelection(context); + } + return _buildScanner(context); + }); + } + + Widget _buildCompanySelection(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('اختر الشركة أولاً', + style: TextStyle(fontFamily: 'El Messiri')), + centerTitle: true, + backgroundColor: const Color(0xFF0F4C81), + foregroundColor: Colors.white, + ), + body: Obx(() { + if (controller.isLoadingCompanies.value) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.companies.isEmpty) { + return const Center( + child: Text('لا توجد شركات مسجلة في حسابك.\nيرجى إضافة شركة أولاً.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16)), + ); + } + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: controller.companies.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final company = controller.companies[index]; + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF0F4C81).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.business, color: Color(0xFF0F4C81)), + ), + title: Text(company['name'] ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16)), + subtitle: Text('الرقم الضريبي: ${company['tax_identification_number'] ?? 'غير محدد'}'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + controller.selectCompany(company['id'], company['name'] ?? ''); + }, + ), + ); + }, + ); + }), + ); + } + + Widget _buildScanner(BuildContext context) { return Scaffold( body: Stack( children: [ @@ -131,27 +197,15 @@ class ScannerView extends GetView { // 3. Upload Button Positioned( - top: 20, - left: 80, - right: 80, + top: 40, + left: 20, + right: 20, child: Obx(() => controller.capturedImages.isEmpty ? const SizedBox() : ElevatedButton.icon( onPressed: controller.isProcessing.value ? null - : () { - if (controller.companies.isEmpty) { - AppSnackbar.showError( - 'خطأ', 'لا توجد شركات مسجلة في حسابك'); - return; - } - if (controller.companies.length == 1) { - controller - .uploadBatch(controller.companies[0]['id']); - return; - } - _showCompanySelectionDialog(context); - }, + : () => controller.uploadBatch(), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0F4C81), padding: const EdgeInsets.symmetric(vertical: 16), @@ -170,9 +224,9 @@ class ScannerView extends GetView { strokeWidth: 2)) : const Icon(Icons.cloud_upload, color: Colors.white), label: Text( - 'رفع ${controller.capturedImages.length} فواتير', + 'رفع ${controller.capturedImages.length} فواتير لـ ${controller.selectedCompanyName.value}', style: const TextStyle( - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white), ), @@ -183,34 +237,5 @@ class ScannerView extends GetView { ); } - void _showCompanySelectionDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('اختر الشركة', - textAlign: TextAlign.right, - style: TextStyle(fontFamily: 'El Messiri')), - content: SizedBox( - width: double.maxFinite, - child: ListView.builder( - shrinkWrap: true, - itemCount: controller.companies.length, - itemBuilder: (context, index) { - final company = controller.companies[index]; - return ListTile( - title: Text(company['name'] ?? '', textAlign: TextAlign.right), - subtitle: Text(company['tax_identification_number'] ?? '', - textAlign: TextAlign.right), - leading: const Icon(Icons.business, color: Color(0xFF0F4C81)), - onTap: () { - Navigator.pop(context); - controller.uploadBatch(company['id']); - }, - ); - }, - ), - ), - ), - ); - } + } diff --git a/musadaq-app/lib/features/settings/views/settings_view.dart b/musadaq-app/lib/features/settings/views/settings_view.dart index 2e95390..64b46c9 100644 --- a/musadaq-app/lib/features/settings/views/settings_view.dart +++ b/musadaq-app/lib/features/settings/views/settings_view.dart @@ -14,104 +14,143 @@ class SettingsView 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( + child: const Row( children: [ - const SizedBox(width: 48), + SizedBox(width: 48), Expanded( child: Center( child: Text( 'الإعدادات', - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold), ), ), ), - const SizedBox(width: 48), + SizedBox(width: 48), ], ), ), - + Expanded( child: Obx(() => ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildProfileCard(isDark), - const SizedBox(height: 24), - _buildSectionTitle('المظهر', Icons.palette_rounded, isDark), - const SizedBox(height: 8), - _buildSettingsCard(isDark, [ - _buildSwitchTile( - icon: Icons.dark_mode_rounded, - title: 'الوضع الداكن', - subtitle: 'تفعيل المظهر الداكن للتطبيق', - value: controller.isDarkMode.value, - onChanged: (v) => controller.toggleTheme(), - isDark: isDark, - ), - ]), - const SizedBox(height: 20), - _buildSectionTitle('الإشعارات', Icons.notifications_rounded, isDark), - const SizedBox(height: 8), - _buildSettingsCard(isDark, [ - _buildSwitchTile( - icon: Icons.notifications_active_rounded, - title: 'إشعارات الدفع', - subtitle: 'استلام إشعارات عند اكتمال المعالجة', - value: controller.pushEnabled.value, - onChanged: (v) => controller.togglePush(), - isDark: isDark, - ), - ]), - const SizedBox(height: 20), - _buildSectionTitle('حول التطبيق', Icons.info_rounded, isDark), - const SizedBox(height: 8), - _buildSettingsCard(isDark, [ - _buildInfoTile( - icon: Icons.verified_rounded, - title: 'الإصدار', - trailing: '1.0.0', - isDark: isDark, - ), - const Divider(height: 1), - _buildInfoTile( - icon: Icons.diamond_rounded, - title: 'الاشتراكات والباقات', - trailing: 'ترقية →', - isDark: isDark, - onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION), - ), - const Divider(height: 1), - _buildInfoTile( - icon: Icons.support_agent_rounded, - title: 'الدعم الفني', - trailing: 'support@musadaq.jo', - isDark: isDark, - ), - const Divider(height: 1), - _buildInfoTile( - icon: Icons.description_rounded, - title: 'سياسة الخصوصية', - trailing: '→', - isDark: isDark, - onTap: () {}, - ), - ]), - const SizedBox(height: 32), - _buildLogoutButton(), - const SizedBox(height: 16), - Center( - child: TextButton( - onPressed: () => _confirmDeleteAccount(context), - child: const Text( - 'حذف الحساب', - style: TextStyle(color: Colors.red, fontSize: 13, decoration: TextDecoration.underline), + padding: const EdgeInsets.all(16), + children: [ + _buildProfileCard(isDark), + const SizedBox(height: 24), + _buildSectionTitle('المظهر', Icons.palette_rounded, isDark), + const SizedBox(height: 8), + _buildSettingsCard(isDark, [ + _buildSwitchTile( + icon: Icons.dark_mode_rounded, + title: 'الوضع الداكن', + subtitle: 'تفعيل المظهر الداكن للتطبيق', + value: controller.isDarkMode.value, + onChanged: (v) => controller.toggleTheme(), + isDark: isDark, + ), + ]), + const SizedBox(height: 20), + _buildSectionTitle( + 'الإشعارات', Icons.notifications_rounded, isDark), + const SizedBox(height: 8), + _buildSettingsCard(isDark, [ + _buildSwitchTile( + icon: Icons.notifications_active_rounded, + title: 'إشعارات الدفع', + subtitle: 'استلام إشعارات عند اكتمال المعالجة', + value: controller.pushEnabled.value, + onChanged: (v) => controller.togglePush(), + isDark: isDark, + ), + ]), + const SizedBox(height: 20), + + // Admin Section + Obx(() { + if (controller.userRole.value == 'admin' || + controller.userRole.value == 'super_admin') { + return Column( + children: [ + _buildSectionTitle('إدارة المكتب', + Icons.admin_panel_settings_rounded, isDark), + const SizedBox(height: 8), + _buildSettingsCard(isDark, [ + _buildInfoTile( + icon: Icons.business_rounded, + title: 'الشركات والموظفين', + trailing: 'إدارة →', + isDark: isDark, + onTap: () { + Get.toNamed(AppRoutes.COMPANIES_MANAGEMENT); + }, + ), + ]), + const SizedBox(height: 20), + ], + ); + } + return const SizedBox.shrink(); + }), + + _buildSectionTitle('حول التطبيق', Icons.info_rounded, isDark), + const SizedBox(height: 8), + _buildSettingsCard(isDark, [ + _buildInfoTile( + icon: Icons.verified_rounded, + title: 'الإصدار', + trailing: '1.0.0', + isDark: isDark, + ), + const Divider(height: 1), + _buildInfoTile( + icon: Icons.diamond_rounded, + title: 'الاشتراكات والباقات', + trailing: 'ترقية →', + isDark: isDark, + onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION), + ), + const Divider(height: 1), + _buildInfoTile( + icon: Icons.support_agent_rounded, + title: 'الدعم الفني', + trailing: 'support@musadaq.jo', + isDark: isDark, + ), + const Divider(height: 1), + _buildInfoTile( + icon: Icons.description_rounded, + title: 'سياسة الخصوصية', + trailing: '→', + isDark: isDark, + onTap: () {}, + ), + ]), + const SizedBox(height: 32), + _buildLogoutButton(), + const SizedBox(height: 16), + Center( + child: TextButton( + onPressed: () => _confirmDeleteAccount(context), + child: const Text( + 'حذف الحساب', + style: TextStyle( + color: Colors.red, + fontSize: 13, + decoration: TextDecoration.underline), + ), + ), ), - ), - ), - const SizedBox(height: 40), - ], - )), + const SizedBox(height: 40), + ], + )), ), ], ); @@ -129,52 +168,71 @@ class SettingsView extends GetView { borderRadius: BorderRadius.circular(16), ), child: Obx(() => Row( - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(14), - ), - child: Center( - child: Text( - (controller.userName.value.isNotEmpty ? controller.userName.value[0] : 'م').toUpperCase(), - style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: Text( + (controller.userName.value.isNotEmpty + ? controller.userName.value[0] + : 'م') + .toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold), + ), + ), ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - controller.userName.value.isNotEmpty ? controller.userName.value : 'مستخدم مُصادَق', - style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.userName.value.isNotEmpty + ? controller.userName.value + : 'مستخدم مُصادَق', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + controller.userPhone.value, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 13, + fontFamily: 'monospace'), + ), + ], ), - const SizedBox(height: 4), - Text( - controller.userPhone.value, - style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 13, fontFamily: 'monospace'), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFD4AF37).withOpacity(0.5)), ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: const Color(0xFFD4AF37).withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFD4AF37).withOpacity(0.5)), - ), - child: Text( - controller.roleName, - style: const TextStyle(color: Color(0xFFF0D060), fontSize: 11, fontWeight: FontWeight.w600), - ), - ), - ], - )), + child: Text( + controller.roleName, + style: const TextStyle( + color: Color(0xFFF0D060), + fontSize: 11, + fontWeight: FontWeight.w600), + ), + ), + ], + )), ); } @@ -183,7 +241,11 @@ class SettingsView extends GetView { children: [ Icon(icon, size: 18, color: const Color(0xFF0F4C81)), const SizedBox(width: 8), - Text(title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: isDark ? Colors.white70 : const Color(0xFF0F4C81))), + Text(title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: isDark ? Colors.white70 : const Color(0xFF0F4C81))), ], ); } @@ -193,7 +255,8 @@ class SettingsView extends GetView { decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E2E) : Colors.white, borderRadius: BorderRadius.circular(14), - border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), + border: + Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), ), child: Column(children: children), ); @@ -218,8 +281,13 @@ class SettingsView extends GetView { ), child: Icon(icon, color: const Color(0xFF0F4C81), size: 20), ), - title: Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87)), - subtitle: Text(subtitle, style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)), + title: Text(title, + style: TextStyle( + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87)), + subtitle: Text(subtitle, + style: TextStyle( + fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)), trailing: Switch.adaptive( value: value, onChanged: onChanged, @@ -247,8 +315,13 @@ class SettingsView extends GetView { ), child: Icon(icon, color: const Color(0xFF0F4C81), size: 20), ), - title: Text(title, style: TextStyle(fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87)), - trailing: Text(trailing, style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey)), + title: Text(title, + style: TextStyle( + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87)), + trailing: Text(trailing, + style: TextStyle( + fontSize: 13, color: isDark ? Colors.white38 : Colors.grey)), ); } @@ -262,10 +335,12 @@ class SettingsView extends GetView { backgroundColor: const Color(0xFFFEE2E2), foregroundColor: const Color(0xFFDC2626), elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), icon: const Icon(Icons.logout_rounded), - label: const Text('تسجيل الخروج', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)), + label: const Text('تسجيل الخروج', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16)), ), ); } @@ -287,7 +362,8 @@ class SettingsView extends GetView { void _confirmDeleteAccount(BuildContext context) { Get.defaultDialog( title: '⚠️ حذف الحساب', - middleText: 'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.', + middleText: + 'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.', textConfirm: 'حذف نهائي', textCancel: 'إلغاء', confirmTextColor: Colors.white, diff --git a/musadaq-app/lib/features/voice/controllers/voice_controller.dart b/musadaq-app/lib/features/voice/controllers/voice_controller.dart new file mode 100644 index 0000000..0b597f4 --- /dev/null +++ b/musadaq-app/lib/features/voice/controllers/voice_controller.dart @@ -0,0 +1,32 @@ +import 'package:get/get.dart'; + +class VoiceResult { + final String action; + final Map params; + final String confirmation; + + VoiceResult({ + required this.action, + required this.params, + required this.confirmation, + }); +} + +class VoiceController extends GetxController { + var isListening = false.obs; + var isProcessing = false.obs; + var hasPermission = true.obs; + var recognizedText = ''.obs; + var errorMessage = ''.obs; + Rx lastResult = Rx(null); + + void startListening() { + isListening.value = true; + errorMessage.value = ''; + // Mock implementation + } + + void stopListening() { + isListening.value = false; + } +}