From cf68007ef104221cbca5396374dbc221936f9d31 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 3 May 2026 03:15:18 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20=D9=85=D9=8F=D8=B5=D8=A7=D8=AF?= =?UTF-8?q?=D9=8E=D9=82:=20=D8=AA=D8=AD=D8=AF=D9=8A=D8=AB=20=D8=A8=D8=B1?= =?UTF-8?q?=D9=85=D8=AC=D9=8A=20=D8=AC=D8=AF=D9=8A=D8=AF=202026-05-03=2003?= =?UTF-8?q?:15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Modules/Auth/AuthService.php | 6 ++-- app/Modules/Companies/CompanyController.php | 15 +++++++++- app/Modules/Dashboard/DashboardController.php | 22 ++++++++++---- app/Modules/Invoices/InvoiceController.php | 24 +++++++++++---- app/Modules/Users/UsersController.php | 20 +++++++++---- public/shell.php | 30 +++++++++++++++---- 6 files changed, 90 insertions(+), 27 deletions(-) diff --git a/app/Modules/Auth/AuthService.php b/app/Modules/Auth/AuthService.php index 6a8fc11..73385eb 100644 --- a/app/Modules/Auth/AuthService.php +++ b/app/Modules/Auth/AuthService.php @@ -30,7 +30,8 @@ final class AuthService $accessToken = $this->jwtService->issueAccessToken([ 'user_id' => $user['id'], 'tenant_id' => $user['tenant_id'], - 'role' => $user['role'] + 'role' => $user['role'], + 'assigned_company_id' => $user['assigned_company_id'] ]); $refreshToken = $this->jwtService->issueRefreshToken($user['id']); @@ -49,7 +50,8 @@ final class AuthService 'id' => $user['id'], 'name' => $user['name'], 'email' => $user['email'], - 'role' => $user['role'] + 'role' => $user['role'], + 'assigned_company_id' => $user['assigned_company_id'] ] ]; } diff --git a/app/Modules/Companies/CompanyController.php b/app/Modules/Companies/CompanyController.php index 52d95e4..919ffe7 100644 --- a/app/Modules/Companies/CompanyController.php +++ b/app/Modules/Companies/CompanyController.php @@ -17,7 +17,20 @@ final class CompanyController public function list(Request $request): void { - $companies = $this->companyModel->findByTenant($request->tenantId); + $tenantId = $request->tenantId; + $role = $request->user->role ?? 'viewer'; + $assignedCompanyId = $request->user->assigned_company_id ?? null; + + if ($role === 'super_admin') { + $companies = $this->companyModel->findByTenant($tenantId); + } else { + // Filter by assigned company + $db = \App\Core\Database::getInstance(); + $stmt = $db->prepare("SELECT * FROM companies WHERE tenant_id = ? AND id = ? AND deleted_at IS NULL"); + $stmt->execute([$tenantId, $assignedCompanyId]); + $companies = $stmt->fetchAll(); + } + Response::json([ 'success' => true, 'data' => $companies diff --git a/app/Modules/Dashboard/DashboardController.php b/app/Modules/Dashboard/DashboardController.php index 2307870..3149102 100644 --- a/app/Modules/Dashboard/DashboardController.php +++ b/app/Modules/Dashboard/DashboardController.php @@ -11,21 +11,31 @@ final class DashboardController public function getStats(Request $request): void { $tenantId = $request->tenantId; + $role = $request->user->role ?? 'viewer'; + $assignedCompanyId = $request->user->assigned_company_id ?? null; $db = Database::getInstance(); + $where = "WHERE tenant_id = ?"; + $params = [$tenantId]; + + if ($role !== 'super_admin') { + $where .= " AND company_id = ?"; + $params[] = $assignedCompanyId; + } + // 1. Total Invoices this month - $stmt = $db->prepare("SELECT COUNT(*) as count FROM invoices WHERE tenant_id = ? AND MONTH(created_at) = MONTH(CURRENT_DATE)"); - $stmt->execute([$tenantId]); + $stmt = $db->prepare("SELECT COUNT(*) as count FROM invoices {$where} AND MONTH(created_at) = MONTH(CURRENT_DATE)"); + $stmt->execute($params); $thisMonth = $stmt->fetch()['count']; // 2. Approved vs Rejected - $stmt = $db->prepare("SELECT status, COUNT(*) as count FROM invoices WHERE tenant_id = ? GROUP BY status"); - $stmt->execute([$tenantId]); + $stmt = $db->prepare("SELECT status, COUNT(*) as count FROM invoices {$where} GROUP BY status"); + $stmt->execute($params); $statusCounts = $stmt->fetchAll(); // 3. Recent Activity - $stmt = $db->prepare("SELECT i.*, c.name as company_name FROM invoices i JOIN companies c ON i.company_id = c.id WHERE i.tenant_id = ? ORDER BY i.created_at DESC LIMIT 5"); - $stmt->execute([$tenantId]); + $stmt = $db->prepare("SELECT i.*, c.name as company_name FROM invoices i JOIN companies c ON i.company_id = c.id {$where} ORDER BY i.created_at DESC LIMIT 5"); + $stmt->execute($params); $recent = $stmt->fetchAll(); Response::json([ diff --git a/app/Modules/Invoices/InvoiceController.php b/app/Modules/Invoices/InvoiceController.php index 4c453b5..a8abc73 100644 --- a/app/Modules/Invoices/InvoiceController.php +++ b/app/Modules/Invoices/InvoiceController.php @@ -20,7 +20,20 @@ final class InvoiceController public function list(Request $request): void { - $invoices = $this->invoiceModel->findByTenant($request->tenantId); + $tenantId = $request->tenantId; + $role = $request->user->role ?? 'viewer'; + $assignedCompanyId = $request->user->assigned_company_id ?? null; + + if ($role === 'super_admin') { + $invoices = $this->invoiceModel->findByTenant($tenantId); + } else { + // Filter by assigned company for admin, accountant, etc. + $db = \App\Core\Database::getInstance(); + $stmt = $db->prepare("SELECT * FROM invoices WHERE tenant_id = ? AND company_id = ? AND deleted_at IS NULL ORDER BY created_at DESC"); + $stmt->execute([$tenantId, $assignedCompanyId]); + $invoices = $stmt->fetchAll(); + } + Response::json([ 'success' => true, 'data' => $invoices @@ -50,11 +63,10 @@ final class InvoiceController $invoiceId = \Ramsey\Uuid\Uuid::uuid4()->toString(); $this->invoiceModel->create([ 'id' => $invoiceId, - 'invoice_uuid' => \Ramsey\Uuid\Uuid::uuid4()->toString(), 'tenant_id' => $tenantId, 'company_id' => $companyId, 'uploaded_by' => $request->user->user_id, - 'status' => 'PROCESSING', + 'status' => 'uploaded', // Match schema ENUM 'original_file_path' => $filePath, 'original_file_hash' => $fileHash, 'idempotency_key' => bin2hex(random_bytes(16)) @@ -67,8 +79,8 @@ final class InvoiceController // Update Invoice with extracted data $this->invoiceModel->update($invoiceId, [ - 'status' => 'EXTRACTED', - 'extracted_data' => json_encode($extractedData, JSON_UNESCAPED_UNICODE) + 'status' => 'extracted', // Match schema ENUM + 'ai_raw_response' => json_encode($extractedData, JSON_UNESCAPED_UNICODE) ]); Response::json([ @@ -83,7 +95,7 @@ final class InvoiceController } catch (Throwable $aiError) { // Keep it uploaded, maybe manual retry later $this->invoiceModel->update($invoiceId, [ - 'status' => 'AI_FAILED' + 'status' => 'validation_failed' // Match schema fallback ]); Response::json([ diff --git a/app/Modules/Users/UsersController.php b/app/Modules/Users/UsersController.php index afeb199..fbc984b 100644 --- a/app/Modules/Users/UsersController.php +++ b/app/Modules/Users/UsersController.php @@ -38,6 +38,8 @@ final class UsersController public function create(Request $request): void { $currentUserRole = $request->user->role ?? 'viewer'; + $currentAssignedCompanyId = $request->user->assigned_company_id ?? null; + if (!in_array($currentUserRole, ['super_admin', 'admin'])) { Response::error('ليس لديك صلاحية لإضافة مستخدمين', 'FORBIDDEN', 403); return; @@ -47,11 +49,16 @@ final class UsersController $email = $request->input('email'); $password = $request->input('password'); $role = $request->input('role', 'accountant'); + $assignedCompanyId = $request->input('assigned_company_id'); // Admin can only create accountants and employees. Only super_admin can create admins. - if ($currentUserRole === 'admin' && in_array($role, ['admin', 'super_admin'])) { - Response::error('لا تملك الصلاحية لإضافة مدراء', 'FORBIDDEN', 403); - return; + if ($currentUserRole === 'admin') { + if (in_array($role, ['admin', 'super_admin'])) { + Response::error('لا تملك الصلاحية لإضافة مدراء', 'FORBIDDEN', 403); + return; + } + // Admin automatically assigns their own company to the new user + $assignedCompanyId = $currentAssignedCompanyId; } // Validate valid roles @@ -62,14 +69,14 @@ final class UsersController } if (!$name || !$email || !$password) { - Response::error('Name, email, and password are required', 'VALIDATION_ERROR', 422); + Response::error('الاسم والبريد وكلمة المرور مطلوبة', 'VALIDATION_ERROR', 422); return; } try { // Check if email exists if ($this->userModel->findByEmail($email)) { - Response::error('Email already in use', 'EMAIL_EXISTS', 409); + Response::error('البريد الإلكتروني مستخدم بالفعل', 'EMAIL_EXISTS', 409); return; } @@ -81,12 +88,13 @@ final class UsersController 'email' => $email, 'password_hash' => password_hash($password, PASSWORD_BCRYPT), 'role' => $role, + 'assigned_company_id' => $assignedCompanyId, 'is_active' => 1 ]); Response::json([ 'success' => true, - 'message' => 'User created successfully', + 'message' => 'تم إنشاء المستخدم بنجاح', 'data' => ['id' => $userId] ]); } catch (Throwable $e) { diff --git a/public/shell.php b/public/shell.php index 472b321..918fb0a 100644 --- a/public/shell.php +++ b/public/shell.php @@ -200,7 +200,7 @@ let roleLabel = 'مستخدم'; if (user.role === 'super_admin') { roleColor = 'text-primary'; roleLabel = 'سوبر أدمن'; } - else if (user.role === 'admin') { roleColor = 'text-blue-400'; roleLabel = 'مدير النظام'; } + else if (user.role === 'admin') { roleColor = 'text-blue-400'; roleLabel = 'مدير شركة'; } else if (user.role === 'accountant') { roleColor = 'text-purple-400'; roleLabel = 'محاسب'; } else if (user.role === 'employee') { roleColor = 'text-orange-400'; roleLabel = 'موظف'; } @@ -237,14 +237,30 @@ } } - function showAddUserModal() { + async function showAddUserModal() { const currentRole = localStorage.getItem('user_role'); + let companies = []; + let companySelectHtml = ''; + + if (currentRole === 'super_admin') { + try { + const res = await API.get('/companies'); + companies = res.data; + companySelectHtml = ` + + `; + } catch (err) { console.error('Failed to fetch companies'); } + } + let optionsHtml = ` `; if (currentRole === 'super_admin') { - optionsHtml += ``; + optionsHtml += ``; } const modals = document.getElementById('modals'); @@ -259,6 +275,7 @@ + ${companySelectHtml}
@@ -276,7 +293,8 @@ name: document.getElementById('usr-name').value, email: document.getElementById('usr-email').value, password: document.getElementById('usr-password').value, - role: document.getElementById('usr-role').value + role: document.getElementById('usr-role').value, + assigned_company_id: document.getElementById('usr-company')?.value || null }; await API.post('/users', data); document.getElementById('user-modal').remove(); @@ -390,7 +408,7 @@
-

${inv.invoice_uuid.substring(0,8)}...

+

${inv.id ? inv.id.substring(0,8) : ''}...

${inv.company_name}

@@ -520,7 +538,7 @@ const statusColor = inv.status === 'APPROVED' ? 'text-primary' : (inv.status === 'REJECTED' ? 'text-red-400' : 'text-yellow-400'); html += ` - ${inv.invoice_uuid} + ${inv.id} ${inv.company_id} ${new Date(inv.created_at).toLocaleDateString('ar-JO')} ${inv.status}