Autosave: 20260503-045416
This commit is contained in:
parent
c53e944d52
commit
abc4505db8
279
index.php
279
index.php
@ -197,6 +197,117 @@ if (!function_exists('db_insert_sql_for_existing_columns')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('current_outlet_name')) {
|
||||
function current_outlet_name(): string {
|
||||
static $cache = [];
|
||||
|
||||
$oid = current_outlet_id();
|
||||
if ($oid === -1 || $oid === 0) {
|
||||
return __('All Outlets') ?: 'All Outlets';
|
||||
}
|
||||
|
||||
if (array_key_exists($oid, $cache)) {
|
||||
return $cache[$oid];
|
||||
}
|
||||
|
||||
if (!db_table_exists('outlets')) {
|
||||
$cache[$oid] = 'Outlet ' . $oid;
|
||||
return $cache[$oid];
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("SELECT name FROM outlets WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$oid]);
|
||||
$cache[$oid] = (string)($stmt->fetchColumn() ?: ('Outlet ' . $oid));
|
||||
} catch (Throwable $e) {
|
||||
$cache[$oid] = 'Outlet ' . $oid;
|
||||
}
|
||||
|
||||
return $cache[$oid];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('outlet_scope_sql')) {
|
||||
function outlet_scope_sql(string $tableName, string $qualifiedColumn = 'outlet_id', bool $includeLegacyNull = true): array {
|
||||
if (!db_column_exists($tableName, 'outlet_id')) {
|
||||
return ['sql' => '1=1', 'params' => []];
|
||||
}
|
||||
|
||||
$oid = current_outlet_id();
|
||||
if ($oid === -1) {
|
||||
return ['sql' => '1=1', 'params' => []];
|
||||
}
|
||||
|
||||
$sql = $includeLegacyNull
|
||||
? "({$qualifiedColumn} = ? OR {$qualifiedColumn} IS NULL)"
|
||||
: "{$qualifiedColumn} = ?";
|
||||
|
||||
return ['sql' => $sql, 'params' => [$oid]];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('dashboard_sales_series')) {
|
||||
function dashboard_sales_series(string $period = 'month', int $limit = 12): array {
|
||||
$period = strtolower($period) === 'year' ? 'year' : 'month';
|
||||
$limit = max(1, (int)$limit);
|
||||
$sources = [];
|
||||
$params = [];
|
||||
$db = db();
|
||||
|
||||
if (db_table_exists('invoices')) {
|
||||
$invoiceDateColumn = db_first_existing_column('invoices', ['invoice_date', 'created_at']);
|
||||
$invoiceTotalExpression = db_column_exists('invoices', 'total_with_vat')
|
||||
? 'COALESCE(total_with_vat, 0)'
|
||||
: (db_column_exists('invoices', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
||||
|
||||
if ($invoiceDateColumn !== null) {
|
||||
$invoiceScope = outlet_scope_sql('invoices', 'outlet_id');
|
||||
if ($period === 'year') {
|
||||
$sources[] = "SELECT YEAR(`{$invoiceDateColumn}`) AS period_key, CAST(YEAR(`{$invoiceDateColumn}`) AS CHAR) AS label, {$invoiceTotalExpression} AS total FROM invoices WHERE `{$invoiceDateColumn}` IS NOT NULL AND {$invoiceScope['sql']}";
|
||||
} else {
|
||||
$sources[] = "SELECT DATE_FORMAT(`{$invoiceDateColumn}`, '%Y-%m') AS period_key, DATE_FORMAT(`{$invoiceDateColumn}`, '%b %Y') AS label, {$invoiceTotalExpression} AS total FROM invoices WHERE `{$invoiceDateColumn}` IS NOT NULL AND {$invoiceScope['sql']}";
|
||||
}
|
||||
$params = array_merge($params, $invoiceScope['params']);
|
||||
}
|
||||
}
|
||||
|
||||
if (db_table_exists('pos_transactions')) {
|
||||
$posDateColumn = db_first_existing_column('pos_transactions', ['created_at', 'transaction_date', 'sale_date']);
|
||||
$posTotalExpression = db_column_exists('pos_transactions', 'net_amount')
|
||||
? 'COALESCE(net_amount, 0)'
|
||||
: (db_column_exists('pos_transactions', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
||||
|
||||
if ($posDateColumn !== null) {
|
||||
$posScope = outlet_scope_sql('pos_transactions', 'outlet_id');
|
||||
$posStatusPredicate = db_column_exists('pos_transactions', 'status') ? "status = 'completed' AND " : '';
|
||||
if ($period === 'year') {
|
||||
$sources[] = "SELECT YEAR(`{$posDateColumn}`) AS period_key, CAST(YEAR(`{$posDateColumn}`) AS CHAR) AS label, {$posTotalExpression} AS total FROM pos_transactions WHERE {$posStatusPredicate}`{$posDateColumn}` IS NOT NULL AND {$posScope['sql']}";
|
||||
} else {
|
||||
$sources[] = "SELECT DATE_FORMAT(`{$posDateColumn}`, '%Y-%m') AS period_key, DATE_FORMAT(`{$posDateColumn}`, '%b %Y') AS label, {$posTotalExpression} AS total FROM pos_transactions WHERE {$posStatusPredicate}`{$posDateColumn}` IS NOT NULL AND {$posScope['sql']}";
|
||||
}
|
||||
$params = array_merge($params, $posScope['params']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($sources === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = "SELECT period_key, label, SUM(total) AS total FROM (" . implode(' UNION ALL ', $sources) . ") dashboard_sales_rollup GROUP BY period_key, label ORDER BY period_key DESC LIMIT {$limit}";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['label'] = (string)($row['label'] ?? '');
|
||||
$row['total'] = (float)($row['total'] ?? 0);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('sales_return_reference_column')) {
|
||||
function sales_return_reference_column(): string {
|
||||
return db_first_existing_column('sales_returns', ['invoice_id', 'sale_id']) ?? 'invoice_id';
|
||||
@ -629,26 +740,41 @@ function getPurchaseAlerts() {
|
||||
$db = db();
|
||||
$hasSupplierJoin = db_table_exists('suppliers') && db_column_exists('purchases', 'supplier_id');
|
||||
$dueDateExpression = db_column_exists('purchases', 'due_date') ? 'p.due_date' : 'NULL';
|
||||
$statusPredicate = db_column_exists('purchases', 'status') ? "WHERE p.status != 'paid'" : 'WHERE 1=1';
|
||||
$dueDatePredicate = db_column_exists('purchases', 'due_date')
|
||||
? ' AND p.due_date IS NOT NULL AND p.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)'
|
||||
: '';
|
||||
$totalExpression = db_column_exists('purchases', 'total_with_vat')
|
||||
? 'p.total_with_vat'
|
||||
? 'COALESCE(p.total_with_vat, 0)'
|
||||
: (db_column_exists('purchases', 'total_amount') ? 'COALESCE(p.total_amount, 0)' : '0');
|
||||
$supplierExpression = $hasSupplierJoin ? 's.name' : 'NULL';
|
||||
$joinClause = $hasSupplierJoin ? ' LEFT JOIN suppliers s ON p.supplier_id = s.id' : '';
|
||||
$orderBy = db_column_exists('purchases', 'due_date') ? ' ORDER BY p.due_date ASC' : ' ORDER BY p.id DESC';
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if (db_column_exists('purchases', 'status')) {
|
||||
$where[] = "p.status != 'paid'";
|
||||
}
|
||||
|
||||
if (db_column_exists('purchases', 'due_date')) {
|
||||
$where[] = 'p.due_date IS NOT NULL';
|
||||
$where[] = 'p.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)';
|
||||
}
|
||||
|
||||
$outletScope = outlet_scope_sql('purchases', 'p.outlet_id');
|
||||
if ($outletScope['sql'] !== '1=1') {
|
||||
$where[] = $outletScope['sql'];
|
||||
$params = array_merge($params, $outletScope['params']);
|
||||
}
|
||||
|
||||
$whereSql = $where === [] ? '1=1' : implode(' AND ', $where);
|
||||
$sql = "SELECT p.id, {$dueDateExpression} AS due_date, {$totalExpression} AS total_with_vat, {$supplierExpression} AS supplier_name"
|
||||
. ' FROM purchases p'
|
||||
. $joinClause
|
||||
. ' '
|
||||
. $statusPredicate
|
||||
. $dueDatePredicate
|
||||
. ' WHERE '
|
||||
. $whereSql
|
||||
. $orderBy;
|
||||
|
||||
$stmt = $db->query($sql);
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
@ -3822,6 +3948,7 @@ $data = [
|
||||
'cash_transactions' => [],
|
||||
'monthly_sales' => [],
|
||||
'yearly_sales' => [],
|
||||
'dashboard_scope_label' => '',
|
||||
'opening_balance' => 0,
|
||||
'stats' => [
|
||||
'expired_items' => 0,
|
||||
@ -3831,6 +3958,10 @@ $data = [
|
||||
'total_received' => 0,
|
||||
'total_receivable' => 0,
|
||||
'total_purchases' => 0,
|
||||
'total_paid' => 0,
|
||||
'total_payable' => 0,
|
||||
'total_customers' => 0,
|
||||
'total_items' => 0,
|
||||
],
|
||||
'settings' => [],
|
||||
];
|
||||
@ -4708,39 +4839,99 @@ switch ($page) {
|
||||
break;
|
||||
default:
|
||||
if (can('dashboard_view')) {
|
||||
$data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll();
|
||||
// Statistics with Outlet Filter
|
||||
$current_oid = current_outlet_id();
|
||||
$inv_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
|
||||
$pos_cond = " WHERE status = 'completed' " . (($current_oid > 0) ? " AND outlet_id = $current_oid " : "");
|
||||
$db = db();
|
||||
$scalar = static function (string $sql, array $params = []) use ($db) {
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchColumn();
|
||||
};
|
||||
|
||||
$pay_inv_cond = ($current_oid > 0) ? " WHERE i.outlet_id = $current_oid " : "";
|
||||
$pay_pos_cond = ($current_oid > 0) ? " WHERE t.outlet_id = $current_oid " : "";
|
||||
$data['dashboard_scope_label'] = current_outlet_name();
|
||||
|
||||
$pur_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
|
||||
$pay_pur_cond = ($current_oid > 0) ? " WHERE p.outlet_id = $current_oid " : "";
|
||||
$customerScope = outlet_scope_sql('customers', 'outlet_id');
|
||||
if (db_table_exists('customers')) {
|
||||
$customerStmt = $db->prepare("SELECT * FROM customers WHERE {$customerScope['sql']} ORDER BY id DESC LIMIT 5");
|
||||
$customerStmt->execute($customerScope['params']);
|
||||
$data['customers'] = $customerStmt->fetchAll();
|
||||
$data['stats']['total_customers'] = (int)($scalar("SELECT COUNT(*) FROM customers WHERE {$customerScope['sql']}", $customerScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
$low_stock_query = ($current_oid > 0)
|
||||
? "SELECT COUNT(*) FROM stock_items WHERE outlet_id = $current_oid AND stock_quantity <= min_stock_level"
|
||||
: "SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level";
|
||||
$itemScope = outlet_scope_sql('stock_items', 'outlet_id');
|
||||
if (db_table_exists('stock_items')) {
|
||||
$data['stats']['total_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
||||
|
||||
$data['stats'] = [
|
||||
'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(),
|
||||
'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(),
|
||||
'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices $inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions $pos_cond")->fetchColumn() ?: 0),
|
||||
'total_received' => (db()->query("SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id $pay_inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(pp.amount) FROM pos_payments pp JOIN pos_transactions t ON pp.transaction_id = t.id $pay_pos_cond")->fetchColumn() ?: 0),
|
||||
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases $pur_cond")->fetchColumn() ?: 0,
|
||||
'total_paid' => db()->query("SELECT SUM(pp.amount) FROM purchase_payments pp JOIN purchases p ON pp.purchase_id = p.id $pay_pur_cond")->fetchColumn() ?: 0,
|
||||
'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(),
|
||||
'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(),
|
||||
'low_stock_items_count' => db()->query($low_stock_query)->fetchColumn(),
|
||||
];
|
||||
$data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received'];
|
||||
$data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid'];
|
||||
if (db_column_exists('stock_items', 'expiry_date')) {
|
||||
$data['stats']['expired_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE() AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
||||
$data['stats']['near_expiry_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
// Sales Chart Data
|
||||
$data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (db_column_exists('stock_items', 'stock_quantity') && db_column_exists('stock_items', 'min_stock_level')) {
|
||||
$data['stats']['low_stock_items_count'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (db_table_exists('invoices')) {
|
||||
$invoiceScope = outlet_scope_sql('invoices', 'outlet_id');
|
||||
$invoiceTotalExpression = db_column_exists('invoices', 'total_with_vat') ? 'COALESCE(total_with_vat, 0)' : (db_column_exists('invoices', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
||||
$data['stats']['total_sales'] += (float)($scalar("SELECT COALESCE(SUM({$invoiceTotalExpression}), 0) FROM invoices WHERE {$invoiceScope['sql']}", $invoiceScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
if (db_table_exists('pos_transactions')) {
|
||||
$posScope = outlet_scope_sql('pos_transactions', 'outlet_id');
|
||||
$posTotalExpression = db_column_exists('pos_transactions', 'net_amount') ? 'COALESCE(net_amount, 0)' : (db_column_exists('pos_transactions', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
||||
$posWhere = [];
|
||||
$posParams = [];
|
||||
if (db_column_exists('pos_transactions', 'status')) {
|
||||
$posWhere[] = 'status = ?';
|
||||
$posParams[] = 'completed';
|
||||
}
|
||||
if ($posScope['sql'] !== '1=1') {
|
||||
$posWhere[] = $posScope['sql'];
|
||||
$posParams = array_merge($posParams, $posScope['params']);
|
||||
}
|
||||
$posWhereSql = $posWhere === [] ? '1=1' : implode(' AND ', $posWhere);
|
||||
$data['stats']['total_sales'] += (float)($scalar("SELECT COALESCE(SUM({$posTotalExpression}), 0) FROM pos_transactions WHERE {$posWhereSql}", $posParams) ?: 0);
|
||||
}
|
||||
|
||||
if (db_table_exists('payments') && db_table_exists('invoices') && db_column_exists('payments', 'invoice_id')) {
|
||||
$paymentInvoiceScope = outlet_scope_sql('invoices', 'i.outlet_id');
|
||||
$paymentInvoiceWhere = $paymentInvoiceScope['sql'] === '1=1' ? '1=1' : $paymentInvoiceScope['sql'];
|
||||
$data['stats']['total_received'] += (float)($scalar("SELECT COALESCE(SUM(p.amount), 0) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE {$paymentInvoiceWhere}", $paymentInvoiceScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
if (db_table_exists('pos_payments') && db_table_exists('pos_transactions') && db_column_exists('pos_payments', 'transaction_id')) {
|
||||
$paymentPosScope = outlet_scope_sql('pos_transactions', 't.outlet_id');
|
||||
$paymentPosWhere = [];
|
||||
$paymentPosParams = [];
|
||||
if (db_column_exists('pos_transactions', 'status')) {
|
||||
$paymentPosWhere[] = 't.status = ?';
|
||||
$paymentPosParams[] = 'completed';
|
||||
}
|
||||
if ($paymentPosScope['sql'] !== '1=1') {
|
||||
$paymentPosWhere[] = $paymentPosScope['sql'];
|
||||
$paymentPosParams = array_merge($paymentPosParams, $paymentPosScope['params']);
|
||||
}
|
||||
$paymentPosWhereSql = $paymentPosWhere === [] ? '1=1' : implode(' AND ', $paymentPosWhere);
|
||||
$data['stats']['total_received'] += (float)($scalar("SELECT COALESCE(SUM(pp.amount), 0) FROM pos_payments pp JOIN pos_transactions t ON pp.transaction_id = t.id WHERE {$paymentPosWhereSql}", $paymentPosParams) ?: 0);
|
||||
}
|
||||
|
||||
if (db_table_exists('purchases')) {
|
||||
$purchaseScope = outlet_scope_sql('purchases', 'outlet_id');
|
||||
$purchaseTotalExpression = db_column_exists('purchases', 'total_with_vat') ? 'COALESCE(total_with_vat, 0)' : (db_column_exists('purchases', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
||||
$data['stats']['total_purchases'] = (float)($scalar("SELECT COALESCE(SUM({$purchaseTotalExpression}), 0) FROM purchases WHERE {$purchaseScope['sql']}", $purchaseScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
if (db_table_exists('purchase_payments') && db_table_exists('purchases') && db_column_exists('purchase_payments', 'purchase_id')) {
|
||||
$purchasePaymentScope = outlet_scope_sql('purchases', 'p.outlet_id');
|
||||
$purchasePaymentWhere = $purchasePaymentScope['sql'] === '1=1' ? '1=1' : $purchasePaymentScope['sql'];
|
||||
$data['stats']['total_paid'] = (float)($scalar("SELECT COALESCE(SUM(pp.amount), 0) FROM purchase_payments pp JOIN purchases p ON pp.purchase_id = p.id WHERE {$purchasePaymentWhere}", $purchasePaymentScope['params']) ?: 0);
|
||||
}
|
||||
|
||||
$data['stats']['total_receivable'] = max((float)$data['stats']['total_sales'] - (float)$data['stats']['total_received'], 0);
|
||||
$data['stats']['total_payable'] = max((float)$data['stats']['total_purchases'] - (float)$data['stats']['total_paid'], 0);
|
||||
|
||||
$data['monthly_sales'] = dashboard_sales_series('month', 12);
|
||||
$data['yearly_sales'] = dashboard_sales_series('year', 5);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -5598,7 +5789,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
|
||||
if (count($user_outlets_list) > 1 || $is_admin):
|
||||
$current_oid = current_outlet_id();
|
||||
$current_oname = $current_oid === -1 ? (__('All Outlets') ?: 'All Outlets') : (db()->query("SELECT name FROM outlets WHERE id = $current_oid")->fetchColumn() ?: 'Outlet ' . $current_oid);
|
||||
$current_oname = current_outlet_name();
|
||||
?>
|
||||
<div class="dropdown d-inline-block me-3">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle btn-sm" type="button" data-bs-toggle="dropdown">
|
||||
@ -5833,8 +6024,15 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="m-0" data-en="Sales Performance" data-ar="أداء المبيعات">Sales Performance</h5>
|
||||
<?php $dashboardScopeLabel = (string)($data['dashboard_scope_label'] ?? current_outlet_name()); ?>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
||||
<div>
|
||||
<h5 class="m-0" data-en="Sales Performance" data-ar="أداء المبيعات">Sales Performance</h5>
|
||||
<div class="small text-muted mt-1">
|
||||
<i class="fas fa-store me-1"></i>
|
||||
<span data-en="<?= htmlspecialchars('Scope: ' . $dashboardScopeLabel, ENT_QUOTES) ?>" data-ar="<?= htmlspecialchars('النطاق: ' . $dashboardScopeLabel, ENT_QUOTES) ?>"><?= $lang === 'ar' ? 'النطاق: ' : 'Scope: ' ?><?= htmlspecialchars($dashboardScopeLabel) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-primary active" id="btnMonthly" data-en="Monthly" data-ar="شهري">Monthly</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnYearly" data-en="Yearly" data-ar="سنوي">Yearly</button>
|
||||
@ -5843,6 +6041,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<div style="height: 300px;">
|
||||
<canvas id="salesChart"></canvas>
|
||||
</div>
|
||||
<div id="salesChartEmptyState" class="small text-muted mt-3 d-none" data-en="No completed sales recorded for this outlet yet." data-ar="لا توجد مبيعات مكتملة مسجلة لهذا الفرع حتى الآن.">No completed sales recorded for this outlet yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -58,53 +58,74 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
// -----------------------------
|
||||
<?php if ($page === 'dashboard' && can('dashboard_view')): ?>
|
||||
const monthlyData = <?= json_encode($data['monthly_sales']) ?>;
|
||||
const yearlyData = <?= json_encode($data['yearly_sales']) ?>;
|
||||
const normalizeSalesSeries = (series) => Array.isArray(series)
|
||||
? series.map(point => ({
|
||||
label: String(point.label ?? ''),
|
||||
total: Number(point.total ?? 0)
|
||||
}))
|
||||
: [];
|
||||
|
||||
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||
let salesChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyData.map(d => d.label),
|
||||
datasets: [{
|
||||
label: 'Sales (OMR)',
|
||||
data: monthlyData.map(d => d.total),
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
const monthlyData = normalizeSalesSeries(<?= json_encode($data['monthly_sales']) ?>);
|
||||
const yearlyData = normalizeSalesSeries(<?= json_encode($data['yearly_sales']) ?>);
|
||||
const salesChartCanvas = document.getElementById('salesChart');
|
||||
const salesChartEmptyState = document.getElementById('salesChartEmptyState');
|
||||
|
||||
if (salesChartCanvas) {
|
||||
const ctx = salesChartCanvas.getContext('2d');
|
||||
let salesChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: monthlyData.map(d => d.label),
|
||||
datasets: [{
|
||||
label: 'Sales (OMR)',
|
||||
data: monthlyData.map(d => d.total),
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) { return 'OMR ' + value.toFixed(3); }
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return 'OMR ' + Number(value).toFixed(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btnMonthly').addEventListener('click', function() {
|
||||
this.classList.add('active');
|
||||
document.getElementById('btnYearly').classList.remove('active');
|
||||
salesChart.data.labels = monthlyData.map(d => d.label);
|
||||
salesChart.data.datasets[0].data = monthlyData.map(d => d.total);
|
||||
salesChart.update();
|
||||
});
|
||||
const renderSalesSeries = (series) => {
|
||||
salesChart.data.labels = series.map(d => d.label);
|
||||
salesChart.data.datasets[0].data = series.map(d => d.total);
|
||||
salesChart.update();
|
||||
|
||||
document.getElementById('btnYearly').addEventListener('click', function() {
|
||||
this.classList.add('active');
|
||||
document.getElementById('btnMonthly').classList.remove('active');
|
||||
salesChart.data.labels = yearlyData.map(d => d.label);
|
||||
salesChart.data.datasets[0].data = yearlyData.map(d => d.total);
|
||||
salesChart.update();
|
||||
});
|
||||
if (salesChartEmptyState) {
|
||||
salesChartEmptyState.classList.toggle('d-none', series.length > 0);
|
||||
}
|
||||
};
|
||||
|
||||
renderSalesSeries(monthlyData);
|
||||
|
||||
document.getElementById('btnMonthly')?.addEventListener('click', function() {
|
||||
this.classList.add('active');
|
||||
document.getElementById('btnYearly')?.classList.remove('active');
|
||||
renderSalesSeries(monthlyData);
|
||||
});
|
||||
|
||||
document.getElementById('btnYearly')?.addEventListener('click', function() {
|
||||
this.classList.add('active');
|
||||
document.getElementById('btnMonthly')?.classList.remove('active');
|
||||
renderSalesSeries(yearlyData);
|
||||
});
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user