diff --git a/app/modules_app/companies/index.php b/app/modules_app/companies/index.php index 36e39b8..16a3208 100644 --- a/app/modules_app/companies/index.php +++ b/app/modules_app/companies/index.php @@ -13,16 +13,26 @@ $db = Database::getInstance(); try { // 1. Super Admin sees ALL companies if ($decoded['role'] === 'super_admin') { - $stmt = $db->prepare("SELECT c.*, t.name as tenant_name + $stmt = $db->prepare(" + SELECT c.*, t.name as tenant_name, + (SELECT COUNT(*) FROM invoices WHERE company_id = c.id AND deleted_at IS NULL) as invoices_count, + (SELECT SUM(grand_total) FROM invoices WHERE company_id = c.id AND deleted_at IS NULL) as total_amount FROM companies c LEFT JOIN tenants t ON c.tenant_id = t.id - WHERE c.deleted_at IS NULL ORDER BY c.created_at DESC"); + WHERE c.deleted_at IS NULL ORDER BY c.created_at DESC + "); $stmt->execute(); $companies = $stmt->fetchAll(); } // 2. Tenant Users (Admin, Accountant, Employee) see all companies in their tenant else { - $stmt = $db->prepare("SELECT * FROM companies WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at DESC"); + $stmt = $db->prepare(" + SELECT *, + (SELECT COUNT(*) FROM invoices WHERE company_id = companies.id AND deleted_at IS NULL) as invoices_count, + (SELECT SUM(grand_total) FROM invoices WHERE company_id = companies.id AND deleted_at IS NULL) as total_amount + FROM companies + WHERE tenant_id = ? AND deleted_at IS NULL ORDER BY created_at DESC + "); $stmt->execute([$decoded['tenant_id']]); $companies = $stmt->fetchAll(); } diff --git a/app/modules_app/tenants/index.php b/app/modules_app/tenants/index.php index d7575a4..ee3bfed 100644 --- a/app/modules_app/tenants/index.php +++ b/app/modules_app/tenants/index.php @@ -15,7 +15,13 @@ if ($decoded['role'] !== 'super_admin') { $db = Database::getInstance(); try { - $stmt = $db->query("SELECT id, name, email, phone, status, created_at FROM tenants ORDER BY created_at DESC"); + $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 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) { diff --git a/app/modules_app/tenants/stats.php b/app/modules_app/tenants/stats.php new file mode 100644 index 0000000..5ea9171 --- /dev/null +++ b/app/modules_app/tenants/stats.php @@ -0,0 +1,60 @@ +prepare(" + SELECT + COUNT(DISTINCT c.id) as total_companies, + COUNT(i.id) as total_invoices, + SUM(i.grand_total) as total_amount, + SUM(i.tax_amount) as total_tax + FROM companies c + LEFT JOIN invoices i ON c.id = i.company_id AND i.deleted_at IS NULL + WHERE c.tenant_id = ? AND c.deleted_at IS NULL + "); + $stmt->execute([$tenantId]); + $summary = $stmt->fetch(); + + // 2. Monthly breakdown + $stmt = $db->prepare(" + SELECT + DATE_FORMAT(i.invoice_date, '%Y-%m') as month, + COUNT(*) as total_invoices, + SUM(i.tax_amount) as total_tax, + SUM(i.grand_total) as total_amount + FROM invoices i + WHERE i.tenant_id = ? AND i.deleted_at IS NULL + GROUP BY month + ORDER BY month DESC + LIMIT 12 + "); + $stmt->execute([$tenantId]); + $monthly = $stmt->fetchAll(); + + json_success([ + 'summary' => $summary, + 'monthly' => $monthly + ]); + +} catch (\Exception $e) { + json_error('Stats Error: ' . $e->getMessage(), 500); +} diff --git a/public/index.php b/public/index.php index 3a78f91..36081db 100644 --- a/public/index.php +++ b/public/index.php @@ -37,6 +37,7 @@ $routes = [ 'v1/tenants' => ['GET', 'tenants/index.php'], 'v1/tenants/create' => ['POST', 'tenants/create.php'], 'v1/tenants/update' => ['POST', 'tenants/update.php'], + 'v1/tenants/stats' => ['GET', 'tenants/stats.php'], ]; if (isset($routes[$route])) { diff --git a/public/shell.php b/public/shell.php index ac80ff7..d701f6b 100644 --- a/public/shell.php +++ b/public/shell.php @@ -128,9 +128,11 @@ الشركة الأرقام الرسمية + الفواتير + الإجمالي الفوترة الحكومية - الإحصائيات - إجراءات + التقارير + إجراءات @@ -145,18 +147,20 @@

TIN:

CRN:

+ +
- + - + @@ -250,9 +254,10 @@ المكتب المحاسبي + الشركات + الفواتير التواصل الحالة - تاريخ الانضمام إجراءات @@ -262,6 +267,13 @@

+

+ + +

+ + +

@@ -272,9 +284,9 @@ :class="t.status==='active'?'bg-emerald-900/40 text-emerald-400':'bg-yellow-900/40 text-yellow-400'" x-text="t.status"> - - + + @@ -282,6 +294,62 @@ + + +
+
+
+
+

+

تقرير أداء المكتب المحاسبي

+
+ +
+ +
+
+

الشركات المدارة

+

+
+
+

إجمالي الفواتير

+

+
+
+

المجموع (JOD)

+

+
+
+

إجمالي الضرائب

+

+
+
+ +

تحليل النشاط الشهري للمكتب

+
+ + + + + + + + + + + + +
الشهرعدد الفواتيرالضريبة المستحقةالإجمالي النهائي
+
+
+
@@ -619,7 +687,7 @@ showAddUserModal: false, showAddCompanyModal: false, showConnectModal: false, showUploadModal: false, showViewModal: false, showCompanyStatsModal: false, - showAddTenantModal: false, showEditTenantModal: false, + showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false, isBusy: false, globalError: '', newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' }, @@ -627,7 +695,8 @@ newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' }, connectData: { client_id: '', secret_key: '', income_source_sequence: '1' }, uploadData: { company_id: '' }, - currentCompany: null, currentInvoice: null, companyStats: null, currentTenant: { name: '', email: '', phone: '', status: '' }, + currentCompany: null, currentInvoice: null, companyStats: null, + currentTenant: { name: '', email: '', phone: '', status: '' }, tenantStats: null, init() { if (!this.user) { window.location.href = '/login.php'; return; } @@ -665,10 +734,7 @@ if (!inv) return ''; if (inv.jofotara?.qr_image_uri) return inv.jofotara.qr_image_uri; if (inv.qr_code) { - // Check if it's already a data URI if (inv.qr_code.startsWith('data:')) return inv.qr_code; - - // Otherwise, it's raw TLV, generate it using QRious try { const qr = new QRious({ value: inv.qr_code, @@ -680,6 +746,46 @@ return ''; }, + async showCompanyStats(companyId) { + this.isBusy = true; + const res = await this.apiRequest('v1/companies/stats&company_id=' + companyId); + if (res) { + this.companyStats = res; + this.showCompanyStatsModal = true; + } + this.isBusy = false; + }, + + async showTenantStats(tenant) { + this.isBusy = true; + const res = await this.apiRequest('v1/tenants/stats&tenant_id=' + tenant.id); + if (res) { + this.tenantStats = res; + this.tenantStats.tenant = tenant; + this.showTenantStatsModal = true; + } + this.isBusy = false; + }, + + async viewInvoice(id) { + this.isBusy = true; + const res = await this.apiRequest('v1/invoices/view&id=' + id); + if (res) { + this.currentInvoice = res; + this.showViewModal = true; + if (!this.currentInvoice.jofotara?.qr_image_uri && !this.currentInvoice.qr_code && this.currentInvoice.status === 'approved') { + try { + const qr = new QRious({ + value: 'Invoice: ' + this.currentInvoice.invoice_number + '\nSeller: ' + this.currentInvoice.supplier_name + '\nTotal: ' + this.currentInvoice.grand_total, + size: 250 + }); + this.currentInvoice.qr_code = qr.toDataURL(); + } catch (e) {} + } + } + this.isBusy = false; + }, + async createUser() { this.isBusy = true; const res = await this.apiRequest('v1/users/create', 'POST', this.newUser); @@ -739,29 +845,6 @@ this.isBusy = false; }, - async showCompanyStats(id) { - this.companyStats = await this.apiRequest('v1/companies/stats&id=' + id); - if (this.companyStats) this.showCompanyStatsModal = true; - }, - - async viewInvoice(id) { - this.currentInvoice = await this.apiRequest('v1/invoices/view&id=' + id); - if (this.currentInvoice) { - this.showViewModal = true; - - // Fallback QR code generation if missing from DB but we have invoice data - if (!this.currentInvoice.jofotara?.qr_image_uri && !this.currentInvoice.qr_code && this.currentInvoice.status === 'approved') { - try { - const qr = new QRious({ - value: 'Invoice: ' + this.currentInvoice.invoice_number + '\nSeller: ' + this.currentInvoice.supplier_name + '\nTotal: ' + this.currentInvoice.grand_total, - size: 250 - }); - this.currentInvoice.qr_code = qr.toDataURL(); - } catch (e) {} - } - } - }, - selectedFile: null, isUploading: false,