Update: 2026-05-07 22:19:17

This commit is contained in:
Hamza-Ayed
2026-05-07 22:19:18 +03:00
parent d8820efa24
commit e04229dfbe
10 changed files with 733 additions and 212 deletions

View File

@@ -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(),
),
];
}

View File

@@ -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';
}

View File

@@ -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<void> 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<CompaniesManagementController>()) {
Get.find<CompaniesManagementController>().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;
}
}
}

View File

@@ -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 = <Map<String, dynamic>>[].obs;
var employees = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
fetchCompanies();
}
Future<void> fetchCompanies() async {
try {
isLoading.value = true;
final response = await _dio.get('companies');
if (response.data['success'] == true) {
companies.value = List<Map<String, dynamic>>.from(response.data['data']);
}
} catch (e) {
AppLogger.error('Failed to fetch companies', e);
AppSnackbar.showError('خطأ', 'تعذر تحميل قائمة الشركات');
} finally {
isLoading.value = false;
}
}
Future<void> 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('خطأ', 'تعذر حذف الشركة');
}
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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);
},
);
}
}

View File

@@ -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<void> uploadBatch(String fallbackCompanyId) async {
Future<void> 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<String> getSavePath() async {
final directory = await getTemporaryDirectory();
final fileName = 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg';

View File

@@ -9,6 +9,72 @@ class ScannerView extends GetView<ScannerController> {
@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<ScannerController> {
// 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<ScannerController> {
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<ScannerController> {
);
}
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']);
},
);
},
),
),
),
);
}
}

View File

@@ -14,104 +14,143 @@ class SettingsView extends GetView<SettingsController> {
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<SettingsController> {
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<SettingsController> {
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<SettingsController> {
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<SettingsController> {
),
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<SettingsController> {
),
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<SettingsController> {
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<SettingsController> {
void _confirmDeleteAccount(BuildContext context) {
Get.defaultDialog(
title: '⚠️ حذف الحساب',
middleText: 'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.',
middleText:
'سيتم حذف جميع بياناتك نهائياً. هذا الإجراء لا يمكن التراجع عنه.',
textConfirm: 'حذف نهائي',
textCancel: 'إلغاء',
confirmTextColor: Colors.white,

View File

@@ -0,0 +1,32 @@
import 'package:get/get.dart';
class VoiceResult {
final String action;
final Map<String, dynamic> 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<VoiceResult?> lastResult = Rx<VoiceResult?>(null);
void startListening() {
isListening.value = true;
errorMessage.value = '';
// Mock implementation
}
void stopListening() {
isListening.value = false;
}
}