diff --git a/app/modules_app/tenants/create.php b/app/modules_app/tenants/create.php
index 813dfcf..de708e2 100644
--- a/app/modules_app/tenants/create.php
+++ b/app/modules_app/tenants/create.php
@@ -17,7 +17,10 @@ $data = input();
$errors = Validator::validate($data, [
'name' => 'required',
- 'email' => 'required|email'
+ 'email' => 'required|email',
+ 'manager_name' => 'required',
+ 'manager_email' => 'required|email',
+ 'manager_password' => 'required'
]);
if ($errors) {
@@ -27,14 +30,51 @@ if ($errors) {
$db = Database::getInstance();
try {
- $stmt = $db->prepare("INSERT INTO tenants (name, email, phone, status, created_at) VALUES (?, ?, ?, 'active', NOW())");
+ $db->beginTransaction();
+
+ // Generate Tenant UUID in PHP so we can use it immediately
+ $tenantId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+ mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
+ );
+
+ // 1. Create Tenant
+ $stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())");
$stmt->execute([
+ $tenantId,
$data['name'],
$data['email'],
$data['phone'] ?? null
]);
- json_success(null, 'تم إنشاء المكتب بنجاح');
+ // Generate User UUID
+ $userId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+ mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
+ mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
+ );
+
+ // Encrypt sensitive user data
+ $encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
+ $encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']);
+ $emailHash = hash('sha256', strtolower($data['manager_email']));
+
+ // 2. Create Initial Manager (Admin) for this Tenant
+ $stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())");
+ $stmtUser->execute([
+ $userId,
+ $tenantId,
+ $encryptedName,
+ $encryptedEmail,
+ $emailHash,
+ password_hash($data['manager_password'], PASSWORD_DEFAULT)
+ ]);
+
+ $db->commit();
+ json_success(null, 'تم إنشاء المكتب ومدير المكتب بنجاح');
} catch (\Exception $e) {
- json_error('حدث خطأ أثناء حفظ البيانات', 500);
+ $db->rollBack();
+ json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500);
}
+
diff --git a/app/modules_app/users/create.php b/app/modules_app/users/create.php
index 53e13e6..e973b1b 100644
--- a/app/modules_app/users/create.php
+++ b/app/modules_app/users/create.php
@@ -46,11 +46,22 @@ $encryptedName = Encryption::encrypt($data['name']);
$encryptedEmail = Encryption::encrypt($data['email']);
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
+// 3. Determine Tenant ID
+$tenantId = null;
+if ($decoded['role'] === 'super_admin') {
+ if (empty($data['tenant_id'])) {
+ json_error('يجب اختيار المكتب المحاسبي', 422);
+ }
+ $tenantId = $data['tenant_id'];
+} else {
+ $tenantId = $decoded['tenant_id'];
+}
+
// 4. Save to Database
try {
$stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
- $decoded['tenant_id'],
+ $tenantId,
$encryptedName,
$encryptedEmail,
$emailHash,
diff --git a/public/shell.php b/public/shell.php
index 74e98d6..349f734 100644
--- a/public/shell.php
+++ b/public/shell.php
@@ -153,6 +153,15 @@
+
+
+
+
@@ -178,6 +187,20 @@
+
+ معلومات مدير المكتب (Admin)
+
+
+
+
+
+
+
+
+
+
+
+
@@ -223,17 +246,22 @@
page: 'dashboard',
users: [],
companies: [],
+ tenants: [],
stats: { total: 0, pending: 0, approved: 0 },
showAddModal: false,
showAddCompanyModal: false,
- newUser: { name: '', email: '', password: '', role: 'employee' },
+ newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' },
+ newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' },
init() {
if (!this.user) window.location.href = '/login.php';
this.loadUsers();
this.loadStats();
this.loadCompanies();
+ if (this.user.role === 'super_admin') {
+ this.loadTenants();
+ }
},
title() {
@@ -277,7 +305,7 @@
const json = await res.json();
if (json.success) {
this.showAddModal = false;
- this.newUser = { name: '', email: '', password: '', role: 'employee' };
+ this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' };
this.loadUsers();
alert('تم إضافة المستخدم بنجاح');
} else {
@@ -305,6 +333,34 @@
}
},
+ async loadTenants() {
+ const res = await fetch('/api/v1/tenants', {
+ headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
+ });
+ const json = await res.json();
+ if (json.success) this.tenants = json.data;
+ },
+
+ async createTenant() {
+ const res = await fetch('/api/v1/tenants/create', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + localStorage.getItem('access_token')
+ },
+ body: JSON.stringify(this.newTenant)
+ });
+ const json = await res.json();
+ if (json.success) {
+ this.showAddTenantModal = false;
+ this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' };
+ this.loadTenants();
+ alert('تم إنشاء المكتب وتعيين المدير بنجاح!');
+ } else {
+ alert(json.message);
+ }
+ },
+
logout() {
localStorage.clear();
window.location.href = '/login.php';
diff --git a/scripts/PROJECT_DOCUMENTATION.md b/scripts/PROJECT_DOCUMENTATION.md
new file mode 100644
index 0000000..8653b04
--- /dev/null
+++ b/scripts/PROJECT_DOCUMENTATION.md
@@ -0,0 +1,311 @@
+# Musadaq Project Documentation
+
+This file contains the complete source code of the project (excluding dependencies and sensitive data).
+
+## File: `fix_data.php`
+
+```php
+beginTransaction();
+
+ // 1. Clear corrupted data tables
+ $db->exec("SET FOREIGN_KEY_CHECKS = 0");
+ $db->exec("TRUNCATE TABLE user_company_assignments");
+ $db->exec("TRUNCATE TABLE invoices");
+ $db->exec("TRUNCATE TABLE companies");
+ $db->exec("SET FOREIGN_KEY_CHECKS = 1");
+ echo "[OK] Cleared companies, invoices, and assignments.\n";
+
+ // 2. Ensure Super Admin does not have a tenant_id (if your schema allows NULL, else set to empty string)
+ // Actually, schema.sql says tenant_id CHAR(36) NOT NULL.
+ // This is a flaw in schema.sql for Super Admins. We will leave users alone for now.
+
+ // 3. Fix the admin's tenant_id to match the first available tenant
+ $stmt = $db->query("SELECT id FROM tenants LIMIT 1");
+ $tenantId = $stmt->fetchColumn();
+
+ if ($tenantId) {
+ $db->exec("UPDATE users SET tenant_id = '$tenantId' WHERE role != 'super_admin'");
+ echo "[OK] Linked all non-super-admin users to Tenant ID: $tenantId\n";
+ }
+
+ $db->commit();
+ echo "--- Reset Complete ---\n";
+
+} catch (\Exception $e) {
+ $db->rollBack();
+ echo "[ERROR] Reset failed: " . $e->getMessage() . "\n";
+}
+
+```
+
+## File: `schema.sql`
+
+```sql
+-- ════════════════════════════════════════════════════════════
+-- مُصادَق — Database Schema v1.0
+-- ════════════════════════════════════════════════════════════
+
+-- Tenants (Accounting Offices)
+CREATE TABLE tenants (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ phone VARCHAR(20),
+ status ENUM('active', 'suspended', 'trial') DEFAULT 'trial',
+ trial_ends_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+-- Users
+CREATE TABLE users (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ role ENUM('super_admin','admin','accountant','viewer') NOT NULL,
+ company_id CHAR(36) NULL, -- assigned company for accountant
+ refresh_token_hash VARCHAR(255) NULL,
+ is_active BOOLEAN DEFAULT TRUE,
+ last_login_at DATETIME NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY uq_tenant_email (tenant_id, email),
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- API Keys (for external integrations / mobile scanner)
+CREATE TABLE api_keys (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL,
+ user_id CHAR(36) NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ public_key VARCHAR(64) NOT NULL UNIQUE,
+ secret_hash VARCHAR(255) NOT NULL, -- bcrypt hash of secret
+ last_used_at DATETIME NULL,
+ is_active BOOLEAN DEFAULT TRUE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+-- Companies
+CREATE TABLE companies (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ name_en VARCHAR(255) NULL,
+ tax_identification_number VARCHAR(20) NOT NULL,
+ address TEXT NULL,
+ jofotara_client_id_encrypted TEXT NULL,
+ jofotara_secret_key_encrypted TEXT NULL,
+ jofotara_income_source_sequence VARCHAR(50) NULL,
+ certificate_path VARCHAR(255) NULL,
+ certificate_password_encrypted VARCHAR(500) NULL,
+ is_active BOOLEAN DEFAULT TRUE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_name (name),
+ INDEX idx_tin (tax_identification_number),
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Subscriptions
+CREATE TABLE subscriptions (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL UNIQUE,
+ plan ENUM('basic','office','pro','enterprise') NOT NULL DEFAULT 'basic',
+ max_companies INT NOT NULL DEFAULT 5,
+ max_invoices_per_month INT NOT NULL DEFAULT 100,
+ price_jod DECIMAL(10,2) NOT NULL DEFAULT 0,
+ invoices_used_this_month INT NOT NULL DEFAULT 0,
+ status ENUM('active','past_due','cancelled') DEFAULT 'active',
+ current_period_start DATETIME NULL,
+ current_period_end DATETIME NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Invoices
+CREATE TABLE invoices (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NOT NULL,
+ company_id CHAR(36) NOT NULL,
+ invoice_number VARCHAR(100) NULL,
+ invoice_date DATE NULL,
+ invoice_type ENUM('cash','credit') DEFAULT 'cash',
+ ubl_type_code CHAR(3) DEFAULT '388',
+ payment_method_code CHAR(3) DEFAULT '013',
+ supplier_tin VARCHAR(20) NULL,
+ supplier_name VARCHAR(255) NULL,
+ supplier_address TEXT NULL,
+ buyer_tin VARCHAR(20) NULL,
+ buyer_national_id VARCHAR(20) NULL,
+ buyer_name VARCHAR(255) NULL,
+ subtotal DECIMAL(15,3) DEFAULT 0,
+ discount_total DECIMAL(15,3) DEFAULT 0,
+ tax_amount DECIMAL(15,3) DEFAULT 0,
+ grand_total DECIMAL(15,3) DEFAULT 0,
+ currency_code CHAR(3) DEFAULT 'JOD',
+ status ENUM('uploaded','extracting','extracted','validated','validation_failed','submitting','approved','rejected') DEFAULT 'uploaded',
+ original_file_path TEXT NULL,
+ invoice_category VARCHAR(20) DEFAULT 'simplified',
+ validation_errors JSON NULL,
+ qr_code TEXT NULL,
+ ai_confidence_score DECIMAL(4,3) NULL,
+ ai_prompt_tokens INT DEFAULT 0,
+ ai_completion_tokens INT DEFAULT 0,
+ ai_total_cost DECIMAL(10,6) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_tenant (tenant_id),
+ INDEX idx_company (company_id),
+ INDEX idx_status (status),
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
+ FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
+);
+
+-- Invoice Lines
+CREATE TABLE invoice_lines (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ invoice_id CHAR(36) NOT NULL,
+ line_number INT NOT NULL,
+ description TEXT NOT NULL,
+ quantity DECIMAL(15,3) NOT NULL,
+ unit_price DECIMAL(15,3) NOT NULL,
+ discount DECIMAL(15,3) DEFAULT 0,
+ tax_rate DECIMAL(5,4) NOT NULL,
+ line_total DECIMAL(15,3) NOT NULL,
+ FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
+);
+
+-- Audit Logs
+CREATE TABLE audit_logs (
+ id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
+ tenant_id CHAR(36) NULL,
+ user_id CHAR(36) NULL,
+ action VARCHAR(100) NOT NULL,
+ entity_type VARCHAR(50) NULL,
+ entity_id CHAR(36) NULL,
+ old_data JSON NULL,
+ new_data JSON NULL,
+ ip_address VARCHAR(45) NULL,
+ user_agent VARCHAR(500) NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_tenant (tenant_id),
+ INDEX idx_action (action),
+ INDEX idx_created (created_at)
+);
+```
+
+## File: `migrate.php`
+
+```php
+exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) AFTER email, ADD INDEX (email_hash)");
+ echo "[OK] Added email_hash column and index.\n";
+} catch (\Exception $e) {
+ echo "[SKIP] email_hash column might already exist.\n";
+}
+
+// 2. Fetch all users to encrypt their data
+$stmt = $db->query("SELECT id, name, email FROM users");
+$users = $stmt->fetchAll();
+
+echo "Found " . count($users) . " users. Starting encryption...\n";
+
+$updateStmt = $db->prepare("UPDATE users SET name = ?, email = ?, email_hash = ? WHERE id = ?");
+
+foreach ($users as $user) {
+ // Check if data is already encrypted (to avoid double encryption)
+ $isAlreadyEncrypted = Encryption::decrypt($user['email']) !== false;
+
+ if ($isAlreadyEncrypted) {
+ echo "User ID {$user['id']} is already encrypted. Skipping.\n";
+ continue;
+ }
+
+ // Encrypt Name
+ $encryptedName = Encryption::encrypt($user['name']);
+
+ // Encrypt Email
+ $encryptedEmail = Encryption::encrypt($user['email']);
+
+ // Generate Hash for lookup
+ $emailHash = hash('sha256', strtolower($user['email']));
+
+ $updateStmt->execute([
+ $encryptedName,
+ $encryptedEmail,
+ $emailHash,
+ $user['id']
+ ]);
+
+echo "User ID {$user['id']} migrated successfully.\n";
+}
+
+// 4. Create user_company_assignments table
+try {
+ $db->exec("CREATE TABLE IF NOT EXISTS user_company_assignments (
+ id INT AUTO_INCREMENT,
+ user_id VARCHAR(100) NOT NULL,
+ company_id VARCHAR(100) NOT NULL,
+ assigned_by VARCHAR(100) NOT NULL,
+ assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
+ PRIMARY KEY (id),
+ UNIQUE KEY uq_user_company (user_id, company_id),
+ CONSTRAINT fk_uca_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ CONSTRAINT fk_uca_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
+ CONSTRAINT fk_uca_admin FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE RESTRICT
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+ echo "[OK] User_company_assignments table created.\n";
+} catch (\Exception $e) {
+ echo "[SKIP] user_company_assignments table: " . $e->getMessage() . "\n";
+}
+
+// 5. Update invoices table to include uploaded_by
+try {
+ $db->exec("ALTER TABLE invoices ADD COLUMN uploaded_by VARCHAR(100) NULL AFTER status");
+ $db->exec("ALTER TABLE invoices ADD CONSTRAINT fk_inv_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL");
+ echo "[OK] Updated invoices table with uploaded_by tracker.\n";
+} catch (\Exception $e) {
+ echo "[SKIP] invoices table update: " . $e->getMessage() . "\n";
+}
+
+echo "--- Migration Complete ---\n";
+
+```
+