adjusting sidebar

This commit is contained in:
Flatlogic Bot 2026-02-18 13:38:32 +00:00
parent 1f2d599fe1
commit b2f7e860d8
4 changed files with 192 additions and 73 deletions

View File

@ -30,6 +30,19 @@ document.addEventListener('DOMContentLoaded', function() {
sidebar.classList.remove('show');
overlay.classList.remove('show');
});
// Sidebar Accordion logic: Close other collapses when one is opened
const sidebarCollapses = document.querySelectorAll('.sidebar .collapse');
sidebarCollapses.forEach(collapseEl => {
collapseEl.addEventListener('show.bs.collapse', function () {
sidebarCollapses.forEach(otherCollapse => {
if (otherCollapse !== collapseEl && otherCollapse.classList.contains('show')) {
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(otherCollapse);
bsCollapse.hide();
}
});
});
});
});
function setLanguage(lang) {

4
check_settings.php Normal file
View File

@ -0,0 +1,4 @@
<?php
require_once 'db/config.php';
$settings = db()->query("SELECT * FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
echo json_encode($settings, JSON_PRETTY_PRINT);

245
index.php
View File

@ -106,6 +106,27 @@ function can(string $permission): bool {
return is_array($perms) && in_array($permission, $perms);
}
// Missing helper functions
function getLoyaltyMultiplier($tier) {
switch (strtolower((string)$tier)) {
case 'gold': return 2.0;
case 'silver': return 1.5;
default: return 1.0;
}
}
function numberToWords($num) {
$num = (int)$num;
if ($num == 0) return "zero";
$ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
$tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
if ($num < 20) return $ones[$num];
if ($num < 100) return $tens[(int)($num / 10)] . ($num % 10 ? "-" . $ones[$num % 10] : "");
if ($num < 1000) return $ones[(int)($num / 100)] . " hundred" . ($num % 100 ? " and " . numberToWords($num % 100) : "");
if ($num < 1000000) return numberToWords((int)($num / 1000)) . " thousand" . ($num % 1000 ? " " . numberToWords($num % 1000) : "");
return (string)$num;
}
// Login Logic
$login_error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
@ -719,6 +740,39 @@ if (isset($_POST['add_hr_department'])) {
}
}
if (isset($_POST['update_settings'])) {
if (can('settings_view')) {
$db = db();
if (isset($_POST['settings']) && is_array($_POST['settings'])) {
foreach ($_POST['settings'] as $key => $value) {
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
$stmt->execute([$key, $value, $value]);
}
}
// Handle file uploads
$files = ['company_logo', 'favicon', 'manager_signature'];
foreach ($files as $file_key) {
if (isset($_FILES[$file_key]) && $_FILES[$file_key]['error'] === 0) {
$ext = pathinfo($_FILES[$file_key]['name'], PATHINFO_EXTENSION);
$filename = 'uploads/' . $file_key . '_' . time() . '.' . $ext;
if (!is_dir('uploads')) mkdir('uploads', 0777, true);
if (move_uploaded_file($_FILES[$file_key]['tmp_name'], $filename)) {
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
$stmt->execute([$file_key, $filename, $filename]);
}
}
}
$message = "Settings updated successfully!";
// Reload settings for current request
$settings_raw = db()->query("SELECT * FROM settings")->fetchAll();
foreach ($settings_raw as $s) {
$data['settings'][$s['key']] = $s['value'];
}
}
}
// --- Backup Handlers ---
if (isset($_POST['create_backup'])) {
if (can('users_view')) { // Admin check
@ -1825,10 +1879,13 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
</header>
<script>
const companySettings = <?= json_encode($data['settings']) ?>;
</script>
<?php if ($message): ?>
<script>
const companySettings = <?= json_encode($data['settings']) ?>;
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function() {
let msg = <?= json_encode($message) ?>;
let type = 'success';
let title = 'Success';
@ -2735,7 +2792,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span>
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Search products by name or SKU..." data-en="Search products..." data-ar="بحث عن منتجات...">
</div>
<div class="input-group" style="width: 200px;">
<div class="input-group flex-grow-1">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-upc-scan"></i></span>
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="Scan barcode..." data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
</div>
@ -2746,7 +2803,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
<div class="product-grid" id="productGrid">
<?php foreach ($products as $p): ?>
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>">
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>">
<?php if ($p['image_path']): ?>
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
<?php else: ?>
@ -2872,37 +2929,52 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
echo json_encode($custData);
?>,
add(product) {
const allowZeroStock = companySettings.allow_zero_stock_sell === '1';
if (!this.items) this.items = [];
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(product.stock_quantity) || 0;
const existing = this.items.find(item => item.id === product.id);
if (existing) {
if (!allowZeroStock && (existing.qty + 1) > currentStock) {
alert('Insufficient stock!');
Swal.fire('Error', 'Insufficient stock!', 'error');
return;
}
existing.qty++;
} else {
if (!allowZeroStock && currentStock <= 0) {
alert('Insufficient stock!');
Swal.fire('Error', 'Insufficient stock!', 'error');
return;
}
this.items.push({...product, qty: 1});
}
this.render();
// Add visual feedback
const lang = document.documentElement.lang || 'en';
const displayName = lang === 'ar' ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr);
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: (lang === 'ar' ? 'تم إضافة: ' : 'Added: ') + displayName,
showConfirmButton: false,
timer: 800
});
},
remove(id) {
this.items = this.items.filter(item => item.id !== id);
this.render();
},
updateQty(id, delta) {
if (!this.items) return;
const item = this.items.find(i => i.id === id);
if (item) {
const allowZeroStock = companySettings.allow_zero_stock_sell === '1';
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(item.stock_quantity) || 0;
if (delta > 0 && !allowZeroStock && (item.qty + delta) > currentStock) {
alert('Insufficient stock!');
Swal.fire('Error', 'Insufficient stock!', 'error');
return;
}
@ -3091,70 +3163,94 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
if (!silent) this.openHeldCartsModal();
},
render() {
const container = document.getElementById('cartItems');
const lang = document.documentElement.lang || 'en';
if (this.items.length === 0) {
container.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-cart-x" style="font-size: 3rem;"></i><p data-en="Cart is empty" data-ar="السلة فارغة">${lang === 'ar' ? 'السلة فارغة' : 'Cart is empty'}</p></div>`;
document.getElementById('posSubtotal').innerText = 'OMR 0.000';
document.getElementById('posTotal').innerText = 'OMR 0.000';
document.getElementById('checkoutBtn').disabled = true;
return;
}
let subtotal = 0;
container.innerHTML = this.items.map(item => {
subtotal += item.price * item.qty;
const displayName = lang === 'ar' ? item.nameAr : item.nameEn;
return `
<div class="cart-item">
<div class="flex-grow-1">
<div class="fw-bold small">${displayName}</div>
<div class="text-muted smaller">OMR ${parseFloat(item.price).toFixed(3)}</div>
</div>
<div class="qty-controls mx-3">
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
<span class="small fw-bold">${item.qty}</span>
<button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button>
</div>
<div class="fw-bold small">OMR ${(item.price * item.qty).toFixed(3)}</div>
</div>
`;
}).join('');
let discountAmount = 0;
if (this.discount) {
if (this.discount.type === 'percentage') {
discountAmount = subtotal * (parseFloat(this.discount.value) / 100);
} else {
discountAmount = parseFloat(this.discount.value);
try {
const container = document.getElementById('cartItems');
if (!container) return;
const lang = document.documentElement.lang || 'en';
const items = Array.isArray(this.items) ? this.items : [];
if (items.length === 0) {
container.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-cart-x" style="font-size: 3rem;"></i><p data-en="Cart is empty" data-ar="السلة فارغة">${lang === 'ar' ? 'السلة فارغة' : 'Cart is empty'}</p></div>`;
const subtotalEl = document.getElementById('posSubtotal');
const totalEl = document.getElementById('posTotal');
const checkoutBtn = document.getElementById('checkoutBtn');
if (subtotalEl) subtotalEl.innerText = 'OMR 0.000';
if (totalEl) totalEl.innerText = 'OMR 0.000';
if (checkoutBtn) checkoutBtn.disabled = true;
return;
}
let subtotal = 0;
container.innerHTML = items.map(item => {
const price = parseFloat(item.price) || 0;
const qty = parseFloat(item.qty) || 0;
subtotal += price * qty;
const displayName = (lang === 'ar' ? (item.nameAr || item.nameEn) : (item.nameEn || item.nameAr)) || 'Unknown Item';
return `
<div class="cart-item">
<div class="flex-grow-1">
<div class="fw-bold small">${displayName}</div>
<div class="text-muted smaller">OMR ${price.toFixed(3)}</div>
</div>
<div class="qty-controls mx-3">
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
<span class="small fw-bold">${qty}</span>
<button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button>
</div>
<div class="fw-bold small">OMR ${(price * qty).toFixed(3)}</div>
</div>
`;
}).join('');
let discountAmount = 0;
if (this.discount) {
if (this.discount.type === 'percentage') {
discountAmount = subtotal * (parseFloat(this.discount.value) / 100);
} else {
discountAmount = parseFloat(this.discount.value);
}
}
}
let loyaltyRedeemedValue = 0;
const redeemSwitch = document.getElementById('redeemLoyalty');
if (redeemSwitch && redeemSwitch.checked) {
const maxRedeemValue = subtotal - discountAmount;
const availableRedeemValue = this.customerPoints / 100;
loyaltyRedeemedValue = Math.min(maxRedeemValue, availableRedeemValue);
}
let loyaltyRedeemedValue = 0;
const redeemSwitch = document.getElementById('redeemLoyalty');
if (redeemSwitch && redeemSwitch.checked) {
const maxRedeemValue = subtotal - discountAmount;
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
const availableRedeemValue = (parseFloat(this.customerPoints) || 0) / redeemRate;
loyaltyRedeemedValue = Math.min(Math.max(0, maxRedeemValue), availableRedeemValue);
}
const total = subtotal - discountAmount - loyaltyRedeemedValue;
const pointsToEarn = Math.floor(total * (this.customerMultiplier || 1.0));
document.getElementById('posSubtotal').innerText = 'OMR ' + subtotal.toFixed(3);
let totalHtml = '';
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- Disc: OMR ${discountAmount.toFixed(3)}</div>`;
if (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- Loyalty: OMR ${loyaltyRedeemedValue.toFixed(3)}</div>`;
if (document.getElementById('posCustomer').value) {
totalHtml += `<div class="smaller text-info">+ Earn: ${pointsToEarn} pts</div>`;
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemedValue);
const multiplier = parseFloat(this.customerMultiplier) || 1.0;
const pointsToEarn = Math.floor(total * multiplier);
const subtotalDisplay = document.getElementById('posSubtotal');
if (subtotalDisplay) subtotalDisplay.innerText = 'OMR ' + subtotal.toFixed(3);
let totalHtml = '';
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- Disc: OMR ${discountAmount.toFixed(3)}</div>`;
if (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- Loyalty: OMR ${loyaltyRedeemedValue.toFixed(3)}</div>`;
const customerId = document.getElementById('posCustomer') ? document.getElementById('posCustomer').value : '';
if (customerId) {
totalHtml += `<div class="smaller text-info">+ Earn: ${pointsToEarn} pts</div>`;
}
totalHtml += 'OMR ' + total.toFixed(3);
const totalDisplay = document.getElementById('posTotal');
if (totalDisplay) {
totalDisplay.innerHTML = totalHtml;
}
const checkoutBtn = document.getElementById('checkoutBtn');
if (checkoutBtn) checkoutBtn.disabled = false;
} catch (e) {
console.error('Cart render error:', e);
}
totalHtml += 'OMR ' + total.toFixed(3);
document.getElementById('posTotal').innerHTML = totalHtml;
document.getElementById('checkoutBtn').disabled = false;
},
async checkout() {
if (this.items.length === 0) return;
@ -3482,7 +3578,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
id: parseInt(card.dataset.id),
nameEn: card.dataset.nameEn,
nameAr: card.dataset.nameAr,
price: parseFloat(card.dataset.price)
price: parseFloat(card.dataset.price),
stock_quantity: parseFloat(card.dataset.stockQuantity)
};
cart.add(product);
});
@ -3512,7 +3609,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
id: parseInt(card.dataset.id),
nameEn: card.dataset.nameEn,
nameAr: card.dataset.nameAr,
price: parseFloat(card.dataset.price)
price: parseFloat(card.dataset.price),
sku: card.dataset.sku,
stock_quantity: parseFloat(card.dataset.stockQuantity)
};
cart.add(product);
e.target.value = '';
@ -7373,7 +7472,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (suggestions) suggestions.style.display = 'none';
if (searchInput) searchInput.value = '';
const allowZeroStock = companySettings.allow_zero_stock_sell === '1';
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(item.stock_quantity) || 0;
if (invoiceType === 'sale' && !allowZeroStock && !customData) {
@ -7448,7 +7547,7 @@ document.addEventListener('DOMContentLoaded', function() {
function attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
row.querySelector('.item-qty').addEventListener('input', function() {
const allowZeroStock = companySettings.allow_zero_stock_sell === '1';
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
if (invoiceType === 'sale' && !allowZeroStock) {
const stock = parseFloat(row.querySelector('.item-row-stock').value) || 0;
const qty = parseFloat(this.value) || 0;

View File

@ -14,3 +14,6 @@
2026-02-18 11:43:14 - POST: {"register_id":"1","opening_balance":"0.000","open_register":""}
2026-02-18 11:50:03 - POST: {"register_id":"1","opening_balance":"0.000","open_register":""}
2026-02-18 12:01:49 - POST: {"session_id":"1","cash_in_hand":"0","notes":"","close_register":""}
2026-02-18 13:05:39 - POST: {"settings":{"company_name":"Bahjet Al-Safa Trading","company_phone":"99359472","company_email":"aalabry@gmail.com","vat_number":"OM25418","company_address":"AL Hamra\r\nOman","allow_zero_stock_sell":"0","loyalty_enabled":"0","loyalty_points_per_unit":"1","loyalty_redeem_points_per_unit":"100"},"update_settings":""}
2026-02-18 13:06:21 - POST: {"settings":{"company_name":"Bahjet Al-Safa Trading","company_phone":"99359472","company_email":"aalabry@gmail.com","vat_number":"OM25418","company_address":"AL Hamra\r\nOman","allow_zero_stock_sell":"1","loyalty_enabled":"0","loyalty_points_per_unit":"1","loyalty_redeem_points_per_unit":"100"},"update_settings":""}
2026-02-18 13:33:14 - POST: {"action":"save_pos_transaction","customer_id":"1","payments":"[{\"method\":\"credit\",\"amount\":1.19}]","total_amount":"1.19","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":1,\"qty\":2,\"price\":0.3825},{\"id\":2,\"qty\":2,\"price\":0.2125}]"}