adjusting sidebar
This commit is contained in:
parent
1f2d599fe1
commit
b2f7e860d8
@ -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
4
check_settings.php
Normal 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
245
index.php
@ -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;
|
||||
|
||||
@ -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}]"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user