From 4994994ad0d71440c23fd915924ade6f26e13976 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 8 May 2026 00:52:01 +0300 Subject: [PATCH] Update: 2026-05-08 00:52:01 --- app/Core/AI.php | 2 +- app/modules_app/companies/update.php | 80 +++++ app/modules_app/users/update.php | 70 ++++ .../MusadaqLiveActivityLiveActivity.swift | 78 +---- .../companies_management_controller.dart | 17 +- .../views/companies_management_view.dart | 313 +++++++++++------ .../users_management_controller.dart | 31 ++ .../users/views/users_management_view.dart | 314 ++++++++++++++---- public/index.php | 2 + 9 files changed, 671 insertions(+), 236 deletions(-) create mode 100644 app/modules_app/companies/update.php create mode 100644 app/modules_app/users/update.php diff --git a/app/Core/AI.php b/app/Core/AI.php index f081a86..cf1b9cc 100644 --- a/app/Core/AI.php +++ b/app/Core/AI.php @@ -10,7 +10,7 @@ use App\Services\InvoiceExtractionService; */ class AI { - private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; + private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent"; private static int $maxRetries = 3; diff --git a/app/modules_app/companies/update.php b/app/modules_app/companies/update.php new file mode 100644 index 0000000..f8794ac --- /dev/null +++ b/app/modules_app/companies/update.php @@ -0,0 +1,80 @@ +prepare($query); +$stmt->execute($params); +$company = $stmt->fetch(); + +if (!$company) json_error('الشركة غير موجودة', 404); + +$fields = []; +$values = []; + +if (isset($data['name'])) { + $fields[] = 'name = ?'; + $values[] = Encryption::encrypt($data['name']); +} +if (isset($data['name_en'])) { + $fields[] = 'name_en = ?'; + $values[] = !empty($data['name_en']) ? Encryption::encrypt($data['name_en']) : null; +} +if (isset($data['tax_identification_number'])) { + $fields[] = 'tax_identification_number = ?'; + $values[] = $data['tax_identification_number']; +} +if (isset($data['commercial_registration_number'])) { + $fields[] = 'commercial_registration_number = ?'; + $values[] = $data['commercial_registration_number']; +} +if (isset($data['address'])) { + $fields[] = 'address = ?'; + $values[] = $data['address']; +} +if (isset($data['city'])) { + $fields[] = 'city = ?'; + $values[] = $data['city']; +} +if (isset($data['contact_email'])) { + $fields[] = 'contact_email = ?'; + $values[] = $data['contact_email']; +} +if (isset($data['contact_phone'])) { + $fields[] = 'contact_phone = ?'; + $values[] = $data['contact_phone']; +} + +if (empty($fields)) json_error('لا توجد بيانات للتحديث', 422); + +$fields[] = 'updated_at = NOW()'; +$values[] = $id; + +$sql = "UPDATE companies SET " . implode(', ', $fields) . " WHERE id = ?"; +$db->prepare($sql)->execute($values); + +AuditLogger::log('company.updated', 'company', $id, null, ['fields' => array_keys($data)], $decoded); + +json_success(null, 'تم تحديث بيانات الشركة بنجاح'); diff --git a/app/modules_app/users/update.php b/app/modules_app/users/update.php new file mode 100644 index 0000000..bf7fb99 --- /dev/null +++ b/app/modules_app/users/update.php @@ -0,0 +1,70 @@ +prepare($query); +$stmt->execute($params); +$user = $stmt->fetch(); + +if (!$user) json_error('المستخدم غير موجود', 404); + +$fields = []; +$values = []; + +if (isset($data['name'])) { + $fields[] = 'name = ?'; + $values[] = $data['name']; +} +if (isset($data['email'])) { + $fields[] = 'email = ?'; + $values[] = $data['email']; +} +if (isset($data['role'])) { + // Only super_admin can change roles + if ($role !== 'super_admin' && $data['role'] === 'super_admin') { + json_error('لا يمكنك منح صلاحية مدير النظام', 403); + } + $fields[] = 'role = ?'; + $values[] = $data['role']; +} +if (isset($data['phone'])) { + $fields[] = 'phone = ?'; + $values[] = $data['phone']; +} +if (isset($data['is_active'])) { + $fields[] = 'is_active = ?'; + $values[] = (int) $data['is_active']; +} + +if (empty($fields)) json_error('لا توجد بيانات للتحديث', 422); + +$fields[] = 'updated_at = NOW()'; +$values[] = $id; + +$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = ?"; +$db->prepare($sql)->execute($values); + +AuditLogger::log('user.updated', 'user', $id, null, ['fields' => array_keys($data)], $decoded); + +json_success(null, 'تم تحديث بيانات المستخدم بنجاح'); diff --git a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift index 61933cd..34062b2 100644 --- a/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift +++ b/musadaq-app/ios/MusadaqLiveActivity/MusadaqLiveActivityLiveActivity.swift @@ -4,77 +4,9 @@ // // Created by Hamza Aleghwairyeen on 07/05/2026. // +// NOTE: This template file is intentionally left empty. +// The actual Live Activity implementation is in MusadaqLiveActivityBundle.swift +// (InvoiceBatchLiveActivity struct). +// -import ActivityKit -import WidgetKit -import SwiftUI - -struct MusadaqLiveActivityAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - // Dynamic stateful properties about your activity go here! - var emoji: String - } - - // Fixed non-changing properties about your activity go here! - var name: String -} - -struct MusadaqLiveActivityLiveActivity: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: MusadaqLiveActivityAttributes.self) { context in - // Lock screen/banner UI goes here - VStack { - Text("Hello \(context.state.emoji)") - } - .activityBackgroundTint(Color.cyan) - .activitySystemActionForegroundColor(Color.black) - - } dynamicIsland: { context in - DynamicIsland { - // Expanded UI goes here. Compose the expanded UI through - // various regions, like leading/trailing/center/bottom - DynamicIslandExpandedRegion(.leading) { - Text("Leading") - } - DynamicIslandExpandedRegion(.trailing) { - Text("Trailing") - } - DynamicIslandExpandedRegion(.bottom) { - Text("Bottom \(context.state.emoji)") - // more content - } - } compactLeading: { - Text("L") - } compactTrailing: { - Text("T \(context.state.emoji)") - } minimal: { - Text(context.state.emoji) - } - .widgetURL(URL(string: "http://www.apple.com")) - .keylineTint(Color.red) - } - } -} - -extension MusadaqLiveActivityAttributes { - fileprivate static var preview: MusadaqLiveActivityAttributes { - MusadaqLiveActivityAttributes(name: "World") - } -} - -extension MusadaqLiveActivityAttributes.ContentState { - fileprivate static var smiley: MusadaqLiveActivityAttributes.ContentState { - MusadaqLiveActivityAttributes.ContentState(emoji: "😀") - } - - fileprivate static var starEyes: MusadaqLiveActivityAttributes.ContentState { - MusadaqLiveActivityAttributes.ContentState(emoji: "🤩") - } -} - -#Preview("Notification", as: .content, using: MusadaqLiveActivityAttributes.preview) { - MusadaqLiveActivityLiveActivity() -} contentStates: { - MusadaqLiveActivityAttributes.ContentState.smiley - MusadaqLiveActivityAttributes.ContentState.starEyes -} +import Foundation diff --git a/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart b/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart index ad7a4e9..c6e7b02 100644 --- a/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart +++ b/musadaq-app/lib/features/companies/controllers/companies_management_controller.dart @@ -9,7 +9,6 @@ class CompaniesManagementController extends GetxController { var isLoading = true.obs; var companies = >[].obs; - var employees = >[].obs; @override void onInit() { @@ -32,9 +31,23 @@ class CompaniesManagementController extends GetxController { } } + Future updateCompany(String id, Map data) async { + try { + data['id'] = id; + final response = await _dio.post('companies/update', data: data); + if (response.data['success'] == true) { + await fetchCompanies(); + AppSnackbar.showSuccess('نجاح', 'تم تحديث بيانات الشركة'); + } + } catch (e) { + AppLogger.error('Failed to update company', e); + AppSnackbar.showError('خطأ', 'تعذر تحديث الشركة'); + } + } + Future deleteCompany(String id) async { try { - final response = await _dio.delete('companies/$id'); + final response = await _dio.post('companies/delete', data: {'id': id}); if (response.data['success'] == true) { companies.removeWhere((c) => c['id'] == id); AppSnackbar.showSuccess('نجاح', 'تم حذف الشركة بنجاح'); diff --git a/musadaq-app/lib/features/companies/views/companies_management_view.dart b/musadaq-app/lib/features/companies/views/companies_management_view.dart index 75d94a7..cfac187 100644 --- a/musadaq-app/lib/features/companies/views/companies_management_view.dart +++ b/musadaq-app/lib/features/companies/views/companies_management_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/companies_management_controller.dart'; import 'add_company_view.dart'; -import '../../../core/utils/app_snackbar.dart'; import '../../../app/routes/app_pages.dart'; class CompaniesManagementView extends StatelessWidget { @@ -10,7 +9,6 @@ class CompaniesManagementView extends StatelessWidget { @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; @@ -22,10 +20,15 @@ class CompaniesManagementView extends StatelessWidget { foregroundColor: Colors.white, elevation: 0, actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded), + onPressed: () => controller.fetchCompanies(), + ), IconButton( icon: const Icon(Icons.add), - onPressed: () { - Get.to(() => const AddCompanyView()); + onPressed: () async { + await Get.to(() => const AddCompanyView()); + controller.fetchCompanies(); }, ), ], @@ -43,6 +46,16 @@ class CompaniesManagementView extends StatelessWidget { Icon(Icons.business_center_outlined, size: 80, color: Colors.grey.shade400), const SizedBox(height: 16), const Text('لا يوجد شركات مسجلة', style: TextStyle(fontSize: 18, color: Colors.grey)), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () async { + await Get.to(() => const AddCompanyView()); + controller.fetchCompanies(); + }, + icon: const Icon(Icons.add), + label: const Text('إضافة شركة'), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0F4C81)), + ), ], ), ); @@ -55,88 +68,7 @@ class CompaniesManagementView extends StatelessWidget { 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 if (value == 'employees') { - Get.toNamed(AppRoutes.USERS_MANAGEMENT); - } else { - AppSnackbar.showInfo('قريباً', 'سيتم تفعيل هذه الميزة قريباً'); - } - }, - ), - ], - ), - 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(width: 8), - ], - if (company['tenant_name'] != null) - Expanded( - child: _buildDetailChip(Icons.account_balance, company['tenant_name'], isDark, color: Colors.blueAccent), - ), - ], - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: () => AppSnackbar.showInfo('إحصائيات', 'عرض إحصائيات الشركة'), - icon: const Icon(Icons.bar_chart, size: 18), - label: const Text('الإحصائيات'), - ), - ], - ) - ], - ), - ), - ); + return _buildCompanyCard(company, controller, isDark, context); }, ), ); @@ -144,35 +76,218 @@ class CompaniesManagementView extends StatelessWidget { ); } - Widget _buildDetailChip(IconData icon, String text, bool isDark, {Color? color}) { + Widget _buildCompanyCard(Map company, CompaniesManagementController controller, bool isDark, BuildContext context) { + final invoicesCount = company['invoices_count'] ?? 0; + final totalAmount = double.tryParse(company['total_amount']?.toString() ?? '0') ?? 0; + + return Card( + elevation: 0, + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade200), + ), + color: isDark ? const Color(0xFF1E1E2E) : Colors.white, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + company['name'] ?? 'شركة', + style: TextStyle( + fontSize: 17, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : const Color(0xFF0F172A), + ), + ), + const SizedBox(height: 2), + Text( + 'TIN: ${company['tax_identification_number'] ?? '—'}', + style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey, fontFamily: 'monospace'), + ), + ], + ), + ), + PopupMenuButton( + icon: Icon(Icons.more_vert, color: isDark ? Colors.white54 : Colors.grey), + onSelected: (value) { + switch (value) { + case 'edit': + _showEditDialog(context, company, controller); + break; + case 'stats': + Get.toNamed(AppRoutes.COMPANY_STATS, arguments: { + 'company_id': company['id'], + 'company_name': company['name'], + }); + break; + case 'delete': + _confirmDelete(context, controller, company['id'], company['name'] ?? ''); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'edit', child: Row(children: [Icon(Icons.edit, size: 18), SizedBox(width: 8), Text('تعديل البيانات')])), + const PopupMenuItem(value: 'stats', child: Row(children: [Icon(Icons.bar_chart, size: 18), SizedBox(width: 8), Text('الإحصائيات')])), + const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, size: 18, color: Colors.red), SizedBox(width: 8), Text('حذف', style: TextStyle(color: Colors.red))])), + ], + ), + ], + ), + const SizedBox(height: 16), + + // Info chips + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _chip(Icons.receipt_long, '$invoicesCount فاتورة', Colors.blue), + _chip(Icons.payments, '${totalAmount.toStringAsFixed(2)} JOD', const Color(0xFF10B981)), + if (company['address'] != null && company['address'].toString().isNotEmpty) + _chip(Icons.location_on, company['address'], Colors.orange), + if (company['is_jofotara_linked'] == 1) + _chip(Icons.verified, 'جوفتورة', const Color(0xFF6366F1)), + ], + ), + + const SizedBox(height: 12), + + // Action buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => _showEditDialog(context, company, controller), + icon: const Icon(Icons.edit, size: 16), + label: const Text('تعديل', style: TextStyle(fontSize: 13)), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF0F4C81), + side: const BorderSide(color: Color(0xFF0F4C81)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: () => Get.toNamed(AppRoutes.COMPANY_STATS, arguments: { + 'company_id': company['id'], + 'company_name': company['name'], + }), + icon: const Icon(Icons.bar_chart, size: 16), + label: const Text('إحصائيات', style: TextStyle(fontSize: 13)), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + side: const BorderSide(color: Color(0xFF10B981)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _chip(IconData icon, String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: (color ?? Colors.grey).withValues(alpha: 0.1), + color: color.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), - ), + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text(text, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: color)), + ], + ), + ); + } + + void _showEditDialog(BuildContext context, Map company, CompaniesManagementController controller) { + final nameC = TextEditingController(text: company['name'] ?? ''); + final tinC = TextEditingController(text: company['tax_identification_number'] ?? ''); + final addressC = TextEditingController(text: company['address'] ?? ''); + final phoneC = TextEditingController(text: company['contact_phone'] ?? ''); + final emailC = TextEditingController(text: company['contact_email'] ?? ''); + + Get.dialog( + AlertDialog( + title: const Text('تعديل بيانات الشركة', textAlign: TextAlign.center), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _editField('اسم الشركة', nameC, Icons.business), + _editField('الرقم الضريبي', tinC, Icons.numbers), + _editField('العنوان', addressC, Icons.location_on), + _editField('الهاتف', phoneC, Icons.phone), + _editField('البريد', emailC, Icons.email), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Get.back(), child: const Text('إلغاء')), + ElevatedButton( + onPressed: () { + Get.back(); + controller.updateCompany(company['id'], { + 'name': nameC.text, + 'tax_identification_number': tinC.text, + 'address': addressC.text, + 'contact_phone': phoneC.text, + 'contact_email': emailC.text, + }); + }, + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0F4C81)), + child: const Text('حفظ', style: TextStyle(color: Colors.white)), ), ], ), ); } - void _confirmDelete(BuildContext context, CompaniesManagementController controller, String id) { + Widget _editField(String label, TextEditingController controller, IconData icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: controller, + textDirection: TextDirection.rtl, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, size: 20), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ), + ); + } + + void _confirmDelete(BuildContext context, CompaniesManagementController controller, String id, String name) { Get.defaultDialog( title: 'حذف الشركة', - middleText: 'هل أنت متأكد من رغبتك في حذف هذه الشركة نهائياً؟', + middleText: 'هل أنت متأكد من حذف "$name" نهائياً؟\nسيتم حذف جميع الفواتير المرتبطة.', textConfirm: 'حذف', textCancel: 'إلغاء', confirmTextColor: Colors.white, diff --git a/musadaq-app/lib/features/users/controllers/users_management_controller.dart b/musadaq-app/lib/features/users/controllers/users_management_controller.dart index 22740f5..b0379c0 100644 --- a/musadaq-app/lib/features/users/controllers/users_management_controller.dart +++ b/musadaq-app/lib/features/users/controllers/users_management_controller.dart @@ -30,4 +30,35 @@ class UsersManagementController extends GetxController { isLoading.value = false; } } + + Future updateUser(String id, Map data) async { + try { + data['id'] = id; + final response = await _dio.post('users/update', data: data); + if (response.data['success'] == true) { + await fetchUsers(); + AppSnackbar.showSuccess('نجاح', 'تم تحديث بيانات المستخدم'); + } + } catch (e) { + AppLogger.error('Failed to update user', e); + AppSnackbar.showError('خطأ', 'تعذر تحديث المستخدم'); + } + } + + Future deleteUser(String id) async { + try { + final response = await _dio.post('users/delete', data: {'id': id}); + if (response.data['success'] == true) { + users.removeWhere((u) => u['id'] == id); + AppSnackbar.showSuccess('نجاح', 'تم حذف المستخدم بنجاح'); + } + } catch (e) { + AppLogger.error('Failed to delete user', e); + AppSnackbar.showError('خطأ', 'تعذر حذف المستخدم'); + } + } + + Future toggleUserActive(String id, bool isActive) async { + await updateUser(id, {'is_active': isActive}); + } } diff --git a/musadaq-app/lib/features/users/views/users_management_view.dart b/musadaq-app/lib/features/users/views/users_management_view.dart index 60a8f9d..a422d2f 100644 --- a/musadaq-app/lib/features/users/views/users_management_view.dart +++ b/musadaq-app/lib/features/users/views/users_management_view.dart @@ -13,17 +13,21 @@ class UsersManagementView extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('إدارة مستخدمي النظام', - style: TextStyle(fontFamily: 'El Messiri')), + 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 AddUserView()); + icon: const Icon(Icons.refresh_rounded), + onPressed: () => controller.fetchUsers(), + ), + IconButton( + icon: const Icon(Icons.person_add), + onPressed: () async { + await Get.to(() => const AddUserView()); + controller.fetchUsers(); }, ), ], @@ -38,11 +42,19 @@ class UsersManagementView extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.people_outline, - size: 80, color: Colors.grey.shade400), + Icon(Icons.people_outline, size: 80, color: Colors.grey.shade400), const SizedBox(height: 16), - const Text('لا يوجد موظفين مسجلين', - style: TextStyle(fontSize: 18, color: Colors.grey)), + const Text('لا يوجد مستخدمين مسجلين', style: TextStyle(fontSize: 18, color: Colors.grey)), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () async { + await Get.to(() => const AddUserView()); + controller.fetchUsers(); + }, + icon: const Icon(Icons.person_add), + label: const Text('إضافة مستخدم'), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0F4C81)), + ), ], ), ); @@ -55,62 +67,242 @@ class UsersManagementView extends StatelessWidget { itemCount: controller.users.length, itemBuilder: (context, index) { final user = controller.users[index]; - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - child: ListTile( - leading: CircleAvatar( - backgroundColor: - const Color(0xFF0F4C81).withValues(alpha: 0.1), - child: const Icon(Icons.person, color: Color(0xFF0F4C81)), - ), - title: Text( - user['name'] ?? 'مستخدم', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - trailing: Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFF10B981).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - user['role'] ?? '', - style: const TextStyle( - color: Color(0xFF10B981), fontSize: 12), - ), - ), - isThreeLine: user['tenant_name'] != null, - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(user['email'] ?? ''), - if (user['tenant_name'] != null) ...[ - const SizedBox(height: 4), - Row( - children: [ - const Icon(Icons.account_balance, - size: 12, color: Colors.grey), - const SizedBox(width: 4), - Text( - user['tenant_name'], - style: const TextStyle( - fontSize: 12, color: Colors.grey), - ), - ], - ), - ], - ], - ), - ), - ); + return _buildUserCard(user, controller, isDark, context); }, ), ); }), ); } + + Widget _buildUserCard(Map user, UsersManagementController controller, bool isDark, BuildContext context) { + final role = user['role'] ?? ''; + final isActive = user['is_active'] == 1 || user['is_active'] == true; + Color roleColor; + String roleLabel; + + switch (role) { + case 'super_admin': + roleColor = const Color(0xFF6366F1); + roleLabel = 'مدير النظام'; + break; + case 'admin': + roleColor = const Color(0xFF0F4C81); + roleLabel = 'مدير'; + break; + case 'accountant': + roleColor = const Color(0xFF10B981); + roleLabel = 'محاسب'; + break; + default: + roleColor = Colors.grey; + roleLabel = role; + } + + return Card( + elevation: 0, + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade200), + ), + color: isDark ? const Color(0xFF1E1E2E) : Colors.white, + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + children: [ + Row( + children: [ + // Avatar + CircleAvatar( + radius: 24, + backgroundColor: roleColor.withValues(alpha: 0.15), + child: Icon(Icons.person, color: roleColor, size: 24), + ), + const SizedBox(width: 12), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + user['name'] ?? 'مستخدم', + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.bold, + color: isDark ? Colors.white : const Color(0xFF0F172A), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: roleColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text(roleLabel, style: TextStyle(color: roleColor, fontSize: 11, fontWeight: FontWeight.w600)), + ), + ], + ), + const SizedBox(height: 4), + Text( + user['email'] ?? '', + style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey), + ), + if (user['phone'] != null && user['phone'].toString().isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + user['phone'], + style: TextStyle(fontSize: 12, color: isDark ? Colors.white24 : Colors.grey.shade400, fontFamily: 'monospace'), + ), + ], + if (user['tenant_name'] != null) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.account_balance, size: 12, color: isDark ? Colors.white24 : Colors.grey), + const SizedBox(width: 4), + Text(user['tenant_name'], style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)), + ], + ), + ], + ], + ), + ), + ], + ), + const SizedBox(height: 12), + // Actions + Row( + children: [ + // Active/Inactive toggle + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: (isActive ? const Color(0xFF10B981) : Colors.red).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isActive ? Icons.check_circle : Icons.block, size: 14, color: isActive ? const Color(0xFF10B981) : Colors.red), + const SizedBox(width: 4), + Text(isActive ? 'نشط' : 'معطّل', style: TextStyle(fontSize: 11, color: isActive ? const Color(0xFF10B981) : Colors.red, fontWeight: FontWeight.w600)), + ], + ), + ), + const Spacer(), + // Toggle active + IconButton( + icon: Icon(isActive ? Icons.toggle_on : Icons.toggle_off, color: isActive ? const Color(0xFF10B981) : Colors.grey, size: 28), + onPressed: () => controller.toggleUserActive(user['id'], !isActive), + tooltip: isActive ? 'تعطيل' : 'تفعيل', + ), + // Edit + IconButton( + icon: const Icon(Icons.edit, size: 20, color: Color(0xFF0F4C81)), + onPressed: () => _showEditDialog(context, user, controller), + tooltip: 'تعديل', + ), + // Delete + IconButton( + icon: const Icon(Icons.delete_outline, size: 20, color: Colors.red), + onPressed: () => _confirmDelete(context, controller, user['id'], user['name'] ?? ''), + tooltip: 'حذف', + ), + ], + ), + ], + ), + ), + ); + } + + void _showEditDialog(BuildContext context, Map user, UsersManagementController controller) { + final nameC = TextEditingController(text: user['name'] ?? ''); + final emailC = TextEditingController(text: user['email'] ?? ''); + final phoneC = TextEditingController(text: user['phone'] ?? ''); + var selectedRole = user['role'] ?? 'accountant'; + + Get.dialog( + StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: const Text('تعديل بيانات المستخدم', textAlign: TextAlign.center), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _editField('الاسم', nameC, Icons.person), + _editField('البريد', emailC, Icons.email), + _editField('الهاتف', phoneC, Icons.phone), + const SizedBox(height: 4), + DropdownButtonFormField( + value: selectedRole, + decoration: InputDecoration( + labelText: 'الصلاحية', + prefixIcon: const Icon(Icons.security, size: 20), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + ), + items: const [ + DropdownMenuItem(value: 'admin', child: Text('مدير')), + DropdownMenuItem(value: 'accountant', child: Text('محاسب')), + ], + onChanged: (v) => setState(() => selectedRole = v!), + ), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Get.back(), child: const Text('إلغاء')), + ElevatedButton( + onPressed: () { + Get.back(); + controller.updateUser(user['id'], { + 'name': nameC.text, + 'email': emailC.text, + 'phone': phoneC.text, + 'role': selectedRole, + }); + }, + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0F4C81)), + child: const Text('حفظ', style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ); + } + + Widget _editField(String label, TextEditingController controller, IconData icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: controller, + textDirection: TextDirection.rtl, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, size: 20), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ), + ); + } + + void _confirmDelete(BuildContext context, UsersManagementController controller, String id, String name) { + Get.defaultDialog( + title: 'حذف المستخدم', + middleText: 'هل أنت متأكد من حذف "$name" نهائياً؟', + textConfirm: 'حذف', + textCancel: 'إلغاء', + confirmTextColor: Colors.white, + buttonColor: Colors.red, + onConfirm: () { + Get.back(); + controller.deleteUser(id); + }, + ); + } } diff --git a/public/index.php b/public/index.php index 7580e99..6b4c5b1 100644 --- a/public/index.php +++ b/public/index.php @@ -21,9 +21,11 @@ $routes = [ 'v1/auth/logout' => ['POST', 'auth/logout.php'], 'v1/users' => ['GET', 'users/index.php'], 'v1/users/create' => ['POST', 'users/create.php'], + 'v1/users/update' => ['POST', 'users/update.php'], 'v1/users/delete' => ['POST', 'users/delete.php'], 'v1/companies' => ['GET', 'companies/index.php'], 'v1/companies/create' => ['POST', 'companies/create.php'], + 'v1/companies/update' => ['POST', 'companies/update.php'], 'v1/companies/delete' => ['POST', 'companies/delete.php'], 'v1/invoices' => ['GET', 'invoices/index.php'], 'v1/invoices/view' => ['GET', 'invoices/view.php'],