From 7528ec992d4441debe048a5254f6a9fa78a64ba2 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 8 May 2026 01:52:24 +0300 Subject: [PATCH] Update: 2026-05-08 01:52:24 --- app/modules_app/tenants/delete.php | 40 +++ app/modules_app/tenants/index.php | 17 +- app/modules_app/users/index.php | 7 +- .../tenants_management_controller.dart | 27 ++ .../views/tenants_management_view.dart | 267 +++++++++++++++--- public/index.php | 1 + 6 files changed, 317 insertions(+), 42 deletions(-) create mode 100644 app/modules_app/tenants/delete.php diff --git a/app/modules_app/tenants/delete.php b/app/modules_app/tenants/delete.php new file mode 100644 index 0000000..48f8676 --- /dev/null +++ b/app/modules_app/tenants/delete.php @@ -0,0 +1,40 @@ +prepare("SELECT * FROM tenants WHERE id = ?"); +$stmt->execute([$id]); +$tenant = $stmt->fetch(); + +if (!$tenant) json_error('المكتب غير موجود', 404); + +// Check for linked users +$stmtUsers = $db->prepare("SELECT COUNT(*) FROM users WHERE tenant_id = ?"); +$stmtUsers->execute([$id]); +$userCount = $stmtUsers->fetchColumn(); + +if ($userCount > 0) { + json_error("لا يمكن حذف المكتب — يوجد $userCount مستخدم مرتبط به. احذف المستخدمين أولاً.", 422); +} + +// Delete +$db->prepare("DELETE FROM tenants WHERE id = ?")->execute([$id]); + +AuditLogger::log('tenant.deleted', 'tenant', $id, ['name' => $tenant['name']], null, $decoded); + +json_success(null, 'تم حذف المكتب المحاسبي بنجاح'); diff --git a/app/modules_app/tenants/index.php b/app/modules_app/tenants/index.php index ee3bfed..d4e8399 100644 --- a/app/modules_app/tenants/index.php +++ b/app/modules_app/tenants/index.php @@ -18,18 +18,25 @@ try { $stmt = $db->query(" SELECT t.id, t.name, t.email, t.phone, t.status, t.created_at, (SELECT COUNT(*) FROM companies WHERE tenant_id = t.id) as companies_count, + (SELECT COUNT(*) FROM users WHERE tenant_id = t.id) as users_count, (SELECT COUNT(*) FROM invoices WHERE tenant_id = t.id) as invoices_count FROM tenants t ORDER BY t.created_at DESC "); $tenants = $stmt->fetchAll(); - foreach ($tenants as &$t) { - $decName = \App\Core\Encryption::decrypt($t['name']); - $t['name'] = $decName !== false ? $decName : $t['name']; + $dec = function($val) { + if (empty($val)) return ''; + $result = \App\Core\Encryption::decrypt((string)$val); + return ($result !== false && $result !== null) ? $result : (string)$val; + }; - $decEmail = \App\Core\Encryption::decrypt($t['email']); - $t['email'] = $decEmail !== false ? $decEmail : $t['email']; + foreach ($tenants as &$t) { + $t['name'] = $dec($t['name']); + $t['email'] = $dec($t['email']); + if (!empty($t['phone'])) { + $t['phone'] = $dec($t['phone']); + } } json_success($tenants); diff --git a/app/modules_app/users/index.php b/app/modules_app/users/index.php index 16f0bb6..ed870aa 100644 --- a/app/modules_app/users/index.php +++ b/app/modules_app/users/index.php @@ -19,7 +19,7 @@ try { if ($role === 'super_admin') { // Super Admin sees ALL users from ALL tenants $stmt = $db->query(" - SELECT u.id, u.name, u.email, u.role, u.is_active, u.created_at, t.name as tenant_name + SELECT u.id, u.name, u.email, u.phone, u.role, u.is_active, u.created_at, t.name as tenant_name FROM users u LEFT JOIN tenants t ON u.tenant_id = t.id ORDER BY u.created_at DESC @@ -27,7 +27,7 @@ try { } elseif ($role === 'admin') { // Admin sees only users in THEIR tenant (Accounting Office) $stmt = $db->prepare(" - SELECT u.id, u.name, u.email, u.role, u.is_active, u.created_at, t.name as tenant_name + SELECT u.id, u.name, u.email, u.phone, u.role, u.is_active, u.created_at, t.name as tenant_name FROM users u LEFT JOIN tenants t ON u.tenant_id = t.id WHERE u.tenant_id = ? @@ -51,6 +51,9 @@ try { foreach ($users as &$user) { $user['name'] = $dec($user['name']); $user['email'] = $dec($user['email']); + if (!empty($user['phone'])) { + $user['phone'] = $dec($user['phone']); + } if (!empty($user['tenant_name'])) { $user['tenant_name'] = $dec($user['tenant_name']); diff --git a/musadaq-app/lib/features/tenants/controllers/tenants_management_controller.dart b/musadaq-app/lib/features/tenants/controllers/tenants_management_controller.dart index 3bffeb9..08815c1 100644 --- a/musadaq-app/lib/features/tenants/controllers/tenants_management_controller.dart +++ b/musadaq-app/lib/features/tenants/controllers/tenants_management_controller.dart @@ -30,4 +30,31 @@ class TenantsManagementController extends GetxController { isLoading.value = false; } } + + Future updateTenant(String id, Map data) async { + try { + data['id'] = id; + final response = await _dio.post('tenants/update', data: data); + if (response.data['success'] == true) { + await fetchTenants(); + AppSnackbar.showSuccess('نجاح', 'تم تحديث بيانات المكتب'); + } + } catch (e) { + AppLogger.error('Failed to update tenant', e); + AppSnackbar.showError('خطأ', 'تعذر تحديث المكتب'); + } + } + + Future deleteTenant(String id) async { + try { + final response = await _dio.post('tenants/delete', data: {'id': id}); + if (response.data['success'] == true) { + tenants.removeWhere((t) => t['id'] == id); + AppSnackbar.showSuccess('نجاح', 'تم حذف المكتب المحاسبي'); + } + } catch (e) { + AppLogger.error('Failed to delete tenant', e); + AppSnackbar.showError('خطأ', 'تعذر حذف المكتب'); + } + } } diff --git a/musadaq-app/lib/features/tenants/views/tenants_management_view.dart b/musadaq-app/lib/features/tenants/views/tenants_management_view.dart index 891be3a..33c2b19 100644 --- a/musadaq-app/lib/features/tenants/views/tenants_management_view.dart +++ b/musadaq-app/lib/features/tenants/views/tenants_management_view.dart @@ -12,24 +12,30 @@ class TenantsManagementView extends StatelessWidget { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( + backgroundColor: isDark ? const Color(0xFF121212) : const Color(0xFFF5F7FA), appBar: AppBar( - title: const Text('إدارة المكاتب المحاسبية', style: TextStyle(fontFamily: 'El Messiri')), + title: const Text('إدارة المكاتب المحاسبية', style: TextStyle(fontWeight: FontWeight.bold)), centerTitle: true, backgroundColor: const Color(0xFF0F4C81), foregroundColor: Colors.white, elevation: 0, actions: [ IconButton( - icon: const Icon(Icons.add), - onPressed: () { - Get.to(() => const AddTenantView()); + icon: const Icon(Icons.refresh_rounded), + onPressed: () => controller.fetchTenants(), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () async { + await Get.to(() => const AddTenantView()); + controller.fetchTenants(); }, ), ], ), body: Obx(() { if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator(color: Color(0xFF0F4C81))); } if (controller.tenants.isEmpty) { @@ -37,9 +43,18 @@ class TenantsManagementView extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.account_balance_outlined, size: 80, color: Colors.grey.shade400), + Container( + width: 80, height: 80, + decoration: BoxDecoration( + color: const Color(0xFF0F4C81).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon(Icons.account_balance, size: 40, color: Color(0xFF0F4C81)), + ), const SizedBox(height: 16), - const Text('لا يوجد مكاتب محاسبية مسجلة', style: TextStyle(fontSize: 18, color: Colors.grey)), + Text('لا يوجد مكاتب محاسبية', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: isDark ? Colors.white54 : Colors.grey)), + const SizedBox(height: 8), + Text('اضغط + لإضافة مكتب جديد', style: TextStyle(fontSize: 13, color: isDark ? Colors.white24 : Colors.grey.shade400)), ], ), ); @@ -51,38 +66,220 @@ class TenantsManagementView extends StatelessWidget { padding: const EdgeInsets.all(16), itemCount: controller.tenants.length, itemBuilder: (context, index) { - final tenant = controller.tenants[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.account_balance, color: Color(0xFF0F4C81)), - ), - title: Text( - tenant['name'] ?? 'مكتب محاسبي', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text(tenant['email'] ?? ''), - trailing: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFF0F4C81).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - tenant['status'] ?? 'active', - style: const TextStyle(color: Color(0xFF0F4C81), fontSize: 12), - ), - ), - ), - ); + return _buildTenantCard(controller.tenants[index], controller, isDark, context); }, ), ); }), ); } + + Widget _buildTenantCard(Map tenant, TenantsManagementController controller, bool isDark, BuildContext context) { + final status = tenant['status'] ?? 'active'; + final isActive = status == 'active'; + final companiesCount = tenant['companies_count'] ?? tenant['company_count'] ?? 0; + final usersCount = tenant['users_count'] ?? tenant['user_count'] ?? 0; + + return Container( + margin: const EdgeInsets.only(bottom: 14), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E1E2E) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200), + boxShadow: [ + if (!isDark) BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2)), + ], + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isActive + ? [const Color(0xFF0F4C81).withValues(alpha: 0.08), const Color(0xFF0F4C81).withValues(alpha: 0.02)] + : [Colors.red.withValues(alpha: 0.08), Colors.red.withValues(alpha: 0.02)], + begin: Alignment.topLeft, end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + children: [ + Container( + width: 50, height: 50, + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [Color(0xFF0F4C81), Color(0xFF1A6BB5)]), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.account_balance, color: Colors.white, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tenant['name'] ?? 'مكتب محاسبي', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF0F172A)), + ), + const SizedBox(height: 4), + if (tenant['email'] != null && tenant['email'].toString().isNotEmpty) + Text(tenant['email'], style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey)), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: (isActive ? const Color(0xFF10B981) : Colors.red).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + 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: 12, fontWeight: FontWeight.w600, color: isActive ? const Color(0xFF10B981) : Colors.red)), + ], + ), + ), + ], + ), + ), + + // Stats row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + _statChip(Icons.business, '$companiesCount شركة', const Color(0xFF3B82F6), isDark), + const SizedBox(width: 10), + _statChip(Icons.people, '$usersCount مستخدم', const Color(0xFF6366F1), isDark), + const SizedBox(width: 10), + if (tenant['phone'] != null && tenant['phone'].toString().isNotEmpty) + _statChip(Icons.phone, tenant['phone'], const Color(0xFF10B981), isDark), + ], + ), + ), + + // Actions + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade100)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => _showEditDialog(context, tenant, controller), + icon: const Icon(Icons.edit, size: 16, color: Color(0xFF0F4C81)), + label: const Text('تعديل', style: TextStyle(color: Color(0xFF0F4C81), fontSize: 13)), + ), + const SizedBox(width: 4), + TextButton.icon( + onPressed: () => _confirmDelete(context, controller, tenant['id'], tenant['name'] ?? ''), + icon: const Icon(Icons.delete_outline, size: 16, color: Colors.red), + label: const Text('حذف', style: TextStyle(color: Colors.red, fontSize: 13)), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _statChip(IconData icon, String label, Color color, bool isDark) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)), + ], + ), + ); + } + + void _showEditDialog(BuildContext context, Map tenant, TenantsManagementController controller) { + final nameC = TextEditingController(text: tenant['name'] ?? ''); + final emailC = TextEditingController(text: tenant['email'] ?? ''); + final phoneC = TextEditingController(text: tenant['phone'] ?? ''); + final addressC = TextEditingController(text: tenant['address'] ?? ''); + + Get.dialog( + AlertDialog( + title: const Text('تعديل بيانات المكتب', textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold)), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _editField('اسم المكتب', nameC, Icons.account_balance), + _editField('البريد الإلكتروني', emailC, Icons.email), + _editField('رقم الهاتف', phoneC, Icons.phone), + _editField('العنوان', addressC, Icons.location_on), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Get.back(), child: const Text('إلغاء')), + ElevatedButton( + onPressed: () { + Get.back(); + controller.updateTenant(tenant['id'], { + 'name': nameC.text, + 'email': emailC.text, + 'phone': phoneC.text, + 'address': addressC.text, + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0F4C81), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const Text('حفظ', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ], + ), + ); + } + + 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, TenantsManagementController controller, String id, String name) { + Get.defaultDialog( + title: 'حذف المكتب المحاسبي', + middleText: 'هل أنت متأكد من حذف "$name"؟\nسيتم حذف جميع بياناته.', + textConfirm: 'حذف نهائي', + textCancel: 'إلغاء', + confirmTextColor: Colors.white, + buttonColor: Colors.red, + onConfirm: () { + Get.back(); + controller.deleteTenant(id); + }, + ); + } } diff --git a/public/index.php b/public/index.php index b798617..a6d066b 100644 --- a/public/index.php +++ b/public/index.php @@ -49,6 +49,7 @@ $routes = [ 'v1/tenants' => ['GET', 'tenants/index.php'], 'v1/tenants/create' => ['POST', 'tenants/create.php'], 'v1/tenants/update' => ['POST', 'tenants/update.php'], + 'v1/tenants/delete' => ['POST', 'tenants/delete.php'], 'v1/tenants/stats' => ['GET', 'tenants/stats.php'], 'v1/subscriptions/plans' => ['GET', 'subscriptions/plans.php'], 'v1/subscriptions/current' => ['GET', 'subscriptions/current.php'],