From ccaa56bcffd34ec5d3335e5e5ea3bd9b26d932a1 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 19 Apr 2026 02:30:10 +0000 Subject: [PATCH] Autosave: 20260419-023017 --- assets/css/custom.css | 502 ++++++++++------------------------------- assets/js/main.js | 152 ++++++++++--- categories.php | 226 +++++++++++++++++++ customers.php | 242 ++++++++++++++++++++ healthz.php | 8 + includes/app.php | 476 ++++++++++++++++++++++++++++++++++++++ includes/footer.php | 28 +++ includes/header.php | 157 +++++++++++++ includes/sale_form.php | 180 +++++++++++++++ index.php | 368 ++++++++++++++++++------------ login.php | 110 +++++++++ logout.php | 5 + normal_sale.php | 3 + pos.php | 3 + purchases.php | 140 ++++++++++++ reports.php | 52 +++++ sale.php | 72 ++++++ sales.php | 191 ++++++++++++++++ stock.php | 276 ++++++++++++++++++++++ suppliers.php | 251 +++++++++++++++++++++ users.php | 203 +++++++++++++++++ 21 files changed, 3082 insertions(+), 563 deletions(-) create mode 100644 categories.php create mode 100644 customers.php create mode 100644 healthz.php create mode 100644 includes/app.php create mode 100644 includes/footer.php create mode 100644 includes/header.php create mode 100644 includes/sale_form.php create mode 100644 login.php create mode 100644 logout.php create mode 100644 normal_sale.php create mode 100644 pos.php create mode 100644 purchases.php create mode 100644 reports.php create mode 100644 sale.php create mode 100644 sales.php create mode 100644 stock.php create mode 100644 suppliers.php create mode 100644 users.php diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..4688093 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,139 @@ +:root { + --sidebar-width: 250px; +} body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f4f6f9; } - -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; +#wrapper { + overflow-x: hidden; +} +#sidebar-wrapper { + min-height: 100vh; + margin-left: calc(-1 * var(--sidebar-width)); + transition: margin .25s ease-out; + width: var(--sidebar-width); + position: fixed; + top: 0; + left: 0; + z-index: 1000; + background: #343a40; /* Dark theme sidebar */ +} +[dir="rtl"] #sidebar-wrapper { + margin-left: 0; + margin-right: calc(-1 * var(--sidebar-width)); + left: auto; + right: 0; +} +#wrapper.toggled #sidebar-wrapper { + margin-left: 0; +} +[dir="rtl"] #wrapper.toggled #sidebar-wrapper { + margin-right: 0; +} +#page-content-wrapper { + min-width: 100vw; + transition: margin .25s ease-out; +} +@media (min-width: 768px) { + #sidebar-wrapper { + margin-left: 0; + } + [dir="rtl"] #sidebar-wrapper { + margin-right: 0; + } + #page-content-wrapper { + min-width: 0; width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; + margin-left: var(--sidebar-width); + } + [dir="rtl"] #page-content-wrapper { + margin-left: 0; + margin-right: var(--sidebar-width); + } + #wrapper.toggled #sidebar-wrapper { + margin-left: calc(-1 * var(--sidebar-width)); + } + [dir="rtl"] #wrapper.toggled #sidebar-wrapper { + margin-right: calc(-1 * var(--sidebar-width)); + } + #wrapper.toggled #page-content-wrapper { + margin-left: 0; + } + [dir="rtl"] #wrapper.toggled #page-content-wrapper { + margin-right: 0; + } } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.sidebar-heading { + padding: 1rem 1.25rem; + font-size: 1.25rem; + color: #fff; + background: #212529; +} +.list-group-item { + border: none; + padding: 0.85rem 1.25rem; + background-color: transparent; + color: #c2c7d0; + font-weight: 500; +} +.list-group-item:hover, .list-group-item.active { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; +} +.list-group-item i { + margin-right: 10px; + width: 20px; + text-align: center; +} +[dir="rtl"] .list-group-item i { + margin-right: 0; + margin-left: 10px; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.top-navbar { + background-color: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.card { + box-shadow: 0 0 1px rgba(0,0,0,.125), 0 1px 3px rgba(0,0,0,.2); + margin-bottom: 1rem; + border: 0; + border-radius: 0.25rem; } - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; +.card-header { + background-color: transparent; + border-bottom: 1px solid rgba(0,0,0,.125); + font-weight: 600; } .table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; + font-weight: 600; + color: #495057; + background-color: #f8f9fa; } -.table td { - background: #fff; - padding: 1rem; - border: none; +/* Modal styles */ +.modal-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} +.modal-footer { + border-top: 1px solid #dee2e6; + background-color: #f8f9fa; } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +body.auth-body { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background-color: #e9ecef; } -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; +/* Sidebar Sub-menu */ +[data-bs-toggle="collapse"][aria-expanded="true"] .toggle-icon { + transform: rotate(180deg); } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..b21d2c4 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,125 @@ +// Basic logic for cart interactions document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const saleForm = document.querySelector('[data-sale-form]'); + if (!saleForm) return; - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + let cart = []; + const cartJsonInput = document.getElementById('cart_json'); + const cartLinesContainer = document.getElementById('cart-lines'); + const cartEmptyState = document.getElementById('cart-empty-state'); + const cartCountLabel = document.getElementById('cart-count-label'); + const cartSubtotalLabel = document.getElementById('cart-subtotal'); + const cartTotalLabel = document.getElementById('cart-total'); - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + function renderCart() { + if (cart.length === 0) { + cartEmptyState.style.display = 'block'; + cartLinesContainer.innerHTML = ''; + cartCountLabel.textContent = '0 ' + (window.saleLabels ? window.saleLabels.empty : 'items'); + cartSubtotalLabel.textContent = '0.00'; + cartTotalLabel.textContent = '0.00'; + cartJsonInput.value = '[]'; + return; + } - appendMessage(message, 'visitor'); - chatInput.value = ''; + cartEmptyState.style.display = 'none'; + let html = ''; + let totalItems = 0; + let subtotal = 0; - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + cart.forEach((item, index) => { + const lineTotal = item.qty * item.price; + totalItems += item.qty; + subtotal += lineTotal; + + html += ` +
+
+
${item.name}
+
${item.qty} x ${item.price.toFixed(3)}
+
+
+ ${lineTotal.toFixed(3)} +
+ + + +
+
+
+ `; }); -}); + + cartLinesContainer.innerHTML = html; + cartCountLabel.textContent = totalItems + ' items'; + cartSubtotalLabel.textContent = subtotal.toFixed(3); + cartTotalLabel.textContent = subtotal.toFixed(3); + cartJsonInput.value = JSON.stringify(cart); + } + + window.updateCartQty = function(index, delta) { + if (cart[index]) { + cart[index].qty += delta; + if (cart[index].qty <= 0) { + cart.splice(index, 1); + } + renderCart(); + } + }; + + window.removeCartItem = function(index) { + cart.splice(index, 1); + renderCart(); + }; + + document.querySelectorAll('[data-add-product]').forEach(btn => { + btn.addEventListener('click', () => { + const sku = btn.dataset.sku; + const name = btn.dataset.name; + const price = parseFloat(btn.dataset.price); + + const existing = cart.find(i => i.sku === sku); + if (existing) { + existing.qty++; + } else { + cart.push({ sku, name, price, qty: 1 }); + } + + // SweetAlert2 Toast for adding product + if (typeof Swal !== 'undefined') { + Swal.fire({ + toast: true, + position: 'top-end', + icon: 'success', + title: name + ' added', + showConfirmButton: false, + timer: 1500 + }); + } + + renderCart(); + }); + }); + + const clearBtn = document.querySelector('[data-clear-cart]'); + if (clearBtn) { + clearBtn.addEventListener('click', () => { + cart = []; + renderCart(); + }); + } + + // Handle form submission warning if empty + saleForm.addEventListener('submit', (e) => { + if (cart.length === 0) { + e.preventDefault(); + if (typeof Swal !== 'undefined') { + Swal.fire('Empty Cart', 'Please add items before saving.', 'warning'); + } else { + alert('Cart is empty'); + } + } + }); + + renderCart(); +}); \ No newline at end of file diff --git a/categories.php b/categories.php new file mode 100644 index 0000000..a99ccd1 --- /dev/null +++ b/categories.php @@ -0,0 +1,226 @@ +prepare('INSERT INTO categories (name_ar, name_en, description) VALUES (?, ?, ?)'); + $stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['description'] ?? '']); + set_flash('success', tr('تمت إضافة التصنيف بنجاح', 'Category added successfully')); + redirect_to('categories.php'); + } elseif ($action === 'edit') { + $stmt = $pdo->prepare('UPDATE categories SET name_ar = ?, name_en = ?, description = ? WHERE id = ?'); + $stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['description'] ?? '', $_POST['id']]); + set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); + redirect_to('categories.php'); + } elseif ($action === 'delete') { + $stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?'); + $stmt->execute([$_POST['id']]); + set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully')); + redirect_to('categories.php'); + } +} + +// Pagination & Search +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$offset = ($page - 1) * $limit; +$search = $_GET['q'] ?? ''; + +$where = '1=1'; +$params = []; +if ($search) { + $where .= ' AND (name_ar LIKE ? OR name_en LIKE ?)'; + $params[] = "%$search%"; + $params[] = "%$search%"; +} + +$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM categories WHERE $where"); +$totalStmt->execute($params); +$total = $totalStmt->fetchColumn(); +$totalPages = ceil($total / $limit); + +$queryStmt = $pdo->prepare("SELECT * FROM categories WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset"); +$queryStmt->execute($params); +$items = $queryStmt->fetchAll(); + +require __DIR__ . '/includes/header.php'; +?> + +
+
+
+

+

+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ID
+ + +
+
+ + 1): ?> + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/customers.php b/customers.php new file mode 100644 index 0000000..30db595 --- /dev/null +++ b/customers.php @@ -0,0 +1,242 @@ +prepare('INSERT INTO customers (name, phone, email, address) VALUES (?, ?, ?, ?)'); + $stmt->execute([$_POST['name'], $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '']); + set_flash('success', tr('تمت إضافة العميل بنجاح', 'Customer added successfully')); + redirect_to('customers.php'); + } elseif ($action === 'edit') { + $stmt = $pdo->prepare('UPDATE customers SET name = ?, phone = ?, email = ?, address = ? WHERE id = ?'); + $stmt->execute([$_POST['name'], $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); + set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); + redirect_to('customers.php'); + } elseif ($action === 'delete') { + $stmt = $pdo->prepare('DELETE FROM customers WHERE id = ?'); + $stmt->execute([$_POST['id']]); + set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully')); + redirect_to('customers.php'); + } +} + +// Pagination & Search +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$offset = ($page - 1) * $limit; +$search = $_GET['q'] ?? ''; + +$where = '1=1'; +$params = []; +if ($search) { + $where .= ' AND (name LIKE ? OR phone LIKE ? OR email LIKE ?)'; + $params[] = "%$search%"; + $params[] = "%$search%"; + $params[] = "%$search%"; +} + +$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM customers WHERE $where"); +$totalStmt->execute($params); +$total = $totalStmt->fetchColumn(); +$totalPages = ceil($total / $limit); + +$queryStmt = $pdo->prepare("SELECT * FROM customers WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset"); +$queryStmt->execute($params); +$items = $queryStmt->fetchAll(); + +require __DIR__ . '/includes/header.php'; +?> + +
+
+
+

+

+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ID
+ + +
+
+ + 1): ?> + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/healthz.php b/healthz.php new file mode 100644 index 0000000..15989d4 --- /dev/null +++ b/healthz.php @@ -0,0 +1,8 @@ + 'ok', + 'time' => date(DATE_ATOM), +], JSON_UNESCAPED_UNICODE); diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..e804b31 --- /dev/null +++ b/includes/app.php @@ -0,0 +1,476 @@ + $type, 'message' => $message]; +} + +function pull_flash(): ?array +{ + $flash = $_SESSION['flash'] ?? null; + unset($_SESSION['flash']); + return $flash; +} + +function branches(): array +{ + return [ + 'muscat' => ['code' => 'muscat', 'name_ar' => 'فرع مسقط', 'name_en' => 'Muscat Branch', 'city_ar' => 'مسقط', 'city_en' => 'Muscat'], + 'sohar' => ['code' => 'sohar', 'name_ar' => 'فرع صحار', 'name_en' => 'Sohar Branch', 'city_ar' => 'صحار', 'city_en' => 'Sohar'], + 'nizwa' => ['code' => 'nizwa', 'name_ar' => 'فرع نزوى', 'name_en' => 'Nizwa Branch', 'city_ar' => 'نزوى', 'city_en' => 'Nizwa'], + ]; +} + +function branch_label(string $code): string +{ + $branch = branches()[$code] ?? null; + if (!$branch) { + return $code; + } + + return current_lang() === 'ar' ? $branch['name_ar'] : $branch['name_en']; +} + +function demo_users(): array +{ + return [ + 'owner' => [ + 'username' => 'owner', + 'password' => 'owner123', + 'role' => 'owner', + 'branch_code' => 'muscat', + 'name_ar' => 'مالك النظام', + 'name_en' => 'System Owner', + ], + 'manager_muscat' => [ + 'username' => 'manager_muscat', + 'password' => 'manager123', + 'role' => 'manager', + 'branch_code' => 'muscat', + 'name_ar' => 'مدير فرع مسقط', + 'name_en' => 'Muscat Branch Manager', + ], + 'cashier_sohar' => [ + 'username' => 'cashier_sohar', + 'password' => 'cashier123', + 'role' => 'cashier', + 'branch_code' => 'sohar', + 'name_ar' => 'كاشير فرع صحار', + 'name_en' => 'Sohar Cashier', + ], + ]; +} + +function role_label(string $role): string +{ + return match ($role) { + 'owner' => tr('مالك / مدير عام', 'Owner / Admin'), + 'manager' => tr('مدير فرع', 'Branch Manager'), + 'cashier' => tr('كاشير', 'Cashier'), + default => $role, + }; +} + +function current_user(): ?array +{ + return $_SESSION['auth_user'] ?? null; +} + +function login_attempt(string $username, string $password): bool +{ + $users = demo_users(); + if (!isset($users[$username])) { + return false; + } + + $user = $users[$username]; + if ($user['password'] !== $password) { + return false; + } + + $_SESSION['auth_user'] = $user; + return true; +} + +function logout_user(): void +{ + unset($_SESSION['auth_user']); +} + +function require_auth(): array +{ + $user = current_user(); + if (!$user) { + set_flash('warning', tr('يرجى تسجيل الدخول أولاً.', 'Please sign in first.')); + redirect_to('login.php'); + } + + return $user; +} + +function require_roles(array $roles): array +{ + $user = require_auth(); + if (!in_array($user['role'], $roles, true)) { + set_flash('warning', tr('ليس لديك صلاحية للوصول إلى هذه الصفحة.', 'You do not have permission to access this page.')); + redirect_to('index.php'); + } + + return $user; +} + +function can_access_branch(string $branchCode): bool +{ + $user = current_user(); + if (!$user) { + return false; + } + + if ($user['role'] === 'owner') { + return true; + } + + return $user['branch_code'] === $branchCode; +} + +function catalog(): array +{ + return [ + 'baklava_box' => ['sku' => 'baklava_box', 'name_ar' => 'بقلاوة مشكلة', 'name_en' => 'Mixed Baklava Box', 'price' => 18.50, 'base_stock' => 72, 'unit_ar' => 'علبة', 'unit_en' => 'box'], + 'date_truffles' => ['sku' => 'date_truffles', 'name_ar' => 'ترافل التمر', 'name_en' => 'Date Truffles', 'price' => 9.25, 'base_stock' => 120, 'unit_ar' => 'علبة', 'unit_en' => 'box'], + 'saffron_maamoul' => ['sku' => 'saffron_maamoul', 'name_ar' => 'معمول الزعفران', 'name_en' => 'Saffron Maamoul', 'price' => 7.80, 'base_stock' => 88, 'unit_ar' => 'صندوق', 'unit_en' => 'pack'], + 'pistachio_bites' => ['sku' => 'pistachio_bites', 'name_ar' => 'لقيمات الفستق', 'name_en' => 'Pistachio Bites', 'price' => 11.40, 'base_stock' => 64, 'unit_ar' => 'علبة', 'unit_en' => 'box'], + 'halwa_classic' => ['sku' => 'halwa_classic', 'name_ar' => 'حلوى عمانية كلاسيك', 'name_en' => 'Classic Omani Halwa', 'price' => 6.20, 'base_stock' => 150, 'unit_ar' => 'عبوة', 'unit_en' => 'jar'], + 'gift_tin' => ['sku' => 'gift_tin', 'name_ar' => 'علبة هدايا فاخرة', 'name_en' => 'Premium Gift Tin', 'price' => 24.00, 'base_stock' => 36, 'unit_ar' => 'علبة', 'unit_en' => 'tin'], + ]; +} + +function product_label(string $sku): string +{ + $item = catalog()[$sku] ?? null; + if (!$item) { + return $sku; + } + + return current_lang() === 'ar' ? $item['name_ar'] : $item['name_en']; +} + +function currency(float $amount): string +{ + return number_format($amount, 2) . ' ' . tr('ر.ع', 'OMR'); +} + +function sale_mode_label(string $mode): string +{ + return $mode === 'normal' ? tr('بيع عادي', 'Normal Sale') : tr('بيع نقاط البيع', 'POS Sale'); +} + +function ensure_sales_table(): void +{ + $sql = "CREATE TABLE IF NOT EXISTS sales_orders ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + receipt_no VARCHAR(50) NOT NULL UNIQUE, + sale_mode VARCHAR(20) NOT NULL, + branch_code VARCHAR(30) NOT NULL, + cashier_username VARCHAR(60) NOT NULL, + cashier_name VARCHAR(120) NOT NULL, + role_name VARCHAR(40) NOT NULL, + customer_name VARCHAR(120) DEFAULT NULL, + payment_method VARCHAR(30) NOT NULL, + items_json LONGTEXT NOT NULL, + item_count INT UNSIGNED NOT NULL DEFAULT 0, + subtotal DECIMAL(10,2) NOT NULL DEFAULT 0, + total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, + notes TEXT DEFAULT NULL, + sale_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_sale_mode (sale_mode), + INDEX idx_branch_code (branch_code), + INDEX idx_sale_date (sale_date) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; + + db()->exec($sql); +} + +function create_sale(array $data): int +{ + ensure_sales_table(); + + $stmt = db()->prepare('INSERT INTO sales_orders + (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_name, payment_method, items_json, item_count, subtotal, total_amount, notes, sale_date) + VALUES + (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_name, :payment_method, :items_json, :item_count, :subtotal, :total_amount, :notes, NOW())'); + + $stmt->bindValue(':receipt_no', $data['receipt_no']); + $stmt->bindValue(':sale_mode', $data['sale_mode']); + $stmt->bindValue(':branch_code', $data['branch_code']); + $stmt->bindValue(':cashier_username', $data['cashier_username']); + $stmt->bindValue(':cashier_name', $data['cashier_name']); + $stmt->bindValue(':role_name', $data['role_name']); + $stmt->bindValue(':customer_name', $data['customer_name']); + $stmt->bindValue(':payment_method', $data['payment_method']); + $stmt->bindValue(':items_json', json_encode($data['items'], JSON_UNESCAPED_UNICODE)); + $stmt->bindValue(':item_count', $data['item_count'], PDO::PARAM_INT); + $stmt->bindValue(':subtotal', $data['subtotal']); + $stmt->bindValue(':total_amount', $data['total_amount']); + $stmt->bindValue(':notes', $data['notes']); + $stmt->execute(); + + return (int) db()->lastInsertId(); +} + +function base_sales_query_filters(array &$params, ?string $mode = null, ?string $branch = null): string +{ + $sql = ' WHERE 1=1 '; + if ($mode) { + $sql .= ' AND sale_mode = :sale_mode '; + $params[':sale_mode'] = $mode; + } + if ($branch) { + $sql .= ' AND branch_code = :branch_code '; + $params[':branch_code'] = $branch; + } + + $user = current_user(); + if ($user && $user['role'] !== 'owner') { + $sql .= ' AND branch_code = :viewer_branch '; + $params[':viewer_branch'] = $user['branch_code']; + } + + return $sql; +} + +function fetch_sales(?string $mode = null, ?string $branch = null, int $limit = 50): array +{ + ensure_sales_table(); + $params = []; + $sql = 'SELECT * FROM sales_orders' . base_sales_query_filters($params, $mode, $branch) . ' ORDER BY sale_date DESC LIMIT :limit'; + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value); + } + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + $rows = $stmt->fetchAll(); + + foreach ($rows as &$row) { + $row['items'] = json_decode((string) $row['items_json'], true) ?: []; + } + + return $rows; +} + +function fetch_sale(int $id): ?array +{ + ensure_sales_table(); + $stmt = db()->prepare('SELECT * FROM sales_orders WHERE id = :id LIMIT 1'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $sale = $stmt->fetch(); + + if (!$sale) { + return null; + } + + if (!can_access_branch((string) $sale['branch_code'])) { + return null; + } + + $sale['items'] = json_decode((string) $sale['items_json'], true) ?: []; + return $sale; +} + +function fetch_all_sales_for_scope(): array +{ + ensure_sales_table(); + $params = []; + $sql = 'SELECT * FROM sales_orders' . base_sales_query_filters($params); + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value); + } + $stmt->execute(); + $rows = $stmt->fetchAll(); + foreach ($rows as &$row) { + $row['items'] = json_decode((string) $row['items_json'], true) ?: []; + } + return $rows; +} + +function dashboard_metrics(): array +{ + $sales = fetch_all_sales_for_scope(); + $today = date('Y-m-d'); + $todaySales = 0; + $todayRevenue = 0.0; + $normalCount = 0; + $posCount = 0; + + foreach ($sales as $sale) { + if (str_starts_with((string) $sale['sale_date'], $today)) { + $todaySales++; + $todayRevenue += (float) $sale['total_amount']; + } + if (($sale['sale_mode'] ?? '') === 'normal') { + $normalCount++; + } else { + $posCount++; + } + } + + return [ + 'today_sales' => $todaySales, + 'today_revenue' => $todayRevenue, + 'pos_count' => $posCount, + 'normal_count' => $normalCount, + 'recent' => array_slice(fetch_sales(null, null, 6), 0, 6), + ]; +} + +function report_metrics(): array +{ + $sales = fetch_all_sales_for_scope(); + $branchTotals = []; + $paymentTotals = []; + $productTotals = []; + $gross = 0.0; + + foreach ($sales as $sale) { + $branch = $sale['branch_code']; + $branchTotals[$branch] = ($branchTotals[$branch] ?? 0.0) + (float) $sale['total_amount']; + $payment = $sale['payment_method']; + $paymentTotals[$payment] = ($paymentTotals[$payment] ?? 0.0) + (float) $sale['total_amount']; + $gross += (float) $sale['total_amount']; + foreach ($sale['items'] as $item) { + $sku = (string) ($item['sku'] ?? ''); + $qty = (int) ($item['qty'] ?? 0); + $productTotals[$sku] = ($productTotals[$sku] ?? 0) + $qty; + } + } + + arsort($branchTotals); + arsort($paymentTotals); + arsort($productTotals); + + return [ + 'gross' => $gross, + 'branch_totals' => $branchTotals, + 'payment_totals' => $paymentTotals, + 'product_totals' => $productTotals, + 'sales_count' => count($sales), + ]; +} + +function stock_snapshot(): array +{ + $catalog = catalog(); + $sold = []; + foreach (fetch_all_sales_for_scope() as $sale) { + foreach ($sale['items'] as $item) { + $sku = (string) ($item['sku'] ?? ''); + $sold[$sku] = ($sold[$sku] ?? 0) + (int) ($item['qty'] ?? 0); + } + } + + $rows = []; + foreach ($catalog as $sku => $item) { + $base = (int) $item['base_stock']; + $used = $sold[$sku] ?? 0; + $rows[] = [ + 'sku' => $sku, + 'name' => current_lang() === 'ar' ? $item['name_ar'] : $item['name_en'], + 'base_stock' => $base, + 'sold' => $used, + 'available' => max(0, $base - $used), + 'price' => (float) $item['price'], + ]; + } + + usort($rows, static fn(array $a, array $b): int => $a['available'] <=> $b['available']); + return $rows; +} + +function module_cards(): array +{ + return [ + ['title_ar' => 'نقاط البيع', 'title_en' => 'POS Sale', 'path' => 'pos.php', 'desc_ar' => 'إتمام البيع السريع مع تحديث السجل.', 'desc_en' => 'Fast checkout with instant sales logging.'], + ['title_ar' => 'بيع عادي', 'title_en' => 'Normal Sale', 'path' => 'normal_sale.php', 'desc_ar' => 'فاتورة يدوية مع العميل والملاحظات.', 'desc_en' => 'Manual invoice flow with customer details and notes.'], + ['title_ar' => 'المبيعات', 'title_en' => 'Sales Ledger', 'path' => 'sales.php', 'desc_ar' => 'قائمة الفواتير مع التفاصيل والفرز.', 'desc_en' => 'Invoice list with filters and detail views.'], + ['title_ar' => 'المخزون', 'title_en' => 'Stock', 'path' => 'stock.php', 'desc_ar' => 'قراءة فورية للمخزون الحالي والتنبيهات.', 'desc_en' => 'Live stock snapshot and low-stock indicators.'], + ['title_ar' => 'المشتريات', 'title_en' => 'Purchases', 'path' => 'purchases.php', 'desc_ar' => 'واجهة مبدئية لاستلام الموردين بين الفروع.', 'desc_en' => 'Starter receiving board for suppliers and branches.'], + ['title_ar' => 'التقارير', 'title_en' => 'Reports', 'path' => 'reports.php', 'desc_ar' => 'مبيعات اليوم، الفروع، وأفضل الأصناف.', 'desc_en' => 'Daily sales, branch totals, and best sellers.'], + ['title_ar' => 'المستخدمون والأدوار', 'title_en' => 'Users & Roles', 'path' => 'users.php', 'desc_ar' => 'صلاحيات منفصلة للمالك والمدير والكاشير.', 'desc_en' => 'Separate access for owner, manager, and cashier.'], + ]; +} + +function purchase_pipeline(): array +{ + return [ + ['supplier' => 'Oman Dates Co.', 'reference' => 'PO-24019', 'branch' => 'muscat', 'status' => tr('بانتظار الاستلام', 'Pending Receiving'), 'eta' => '2026-04-20'], + ['supplier' => 'Golden Nuts', 'reference' => 'PO-24023', 'branch' => 'sohar', 'status' => tr('في الطريق', 'In Transit'), 'eta' => '2026-04-21'], + ['supplier' => 'Saffron House', 'reference' => 'PO-24026', 'branch' => 'nizwa', 'status' => tr('جاهز للفحص', 'Ready for QC'), 'eta' => '2026-04-22'], + ]; +} + +function receipt_code(): string +{ + return 'AR-' . date('ymd-His') . '-' . random_int(100, 999); +} diff --git a/includes/footer.php b/includes/footer.php new file mode 100644 index 0000000..abaaa98 --- /dev/null +++ b/includes/footer.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..3010f8a --- /dev/null +++ b/includes/header.php @@ -0,0 +1,157 @@ + + + + + + + <?= h($pageTitle) ?> · <?= h($projectName) ?> + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + +
+ + +
+ + + + + \ No newline at end of file diff --git a/includes/sale_form.php b/includes/sale_form.php new file mode 100644 index 0000000..44b60cb --- /dev/null +++ b/includes/sale_form.php @@ -0,0 +1,180 @@ + $sku, + 'name_ar' => $product['name_ar'], + 'name_en' => $product['name_en'], + 'qty' => $qty, + 'price' => $price, + 'line_total' => $lineTotal, + ]; + $subtotal += $lineTotal; + $itemCount += $qty; + } + + if ($normalized === []) { + $error = tr('السلة غير صالحة بعد التحقق من الأصناف.', 'The cart is invalid after product validation.'); + } else { + $cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']; + $saleId = create_sale([ + 'receipt_no' => receipt_code(), + 'sale_mode' => $saleMode, + 'branch_code' => $branchCode, + 'cashier_username' => $user['username'], + 'cashier_name' => $cashierName, + 'role_name' => $user['role'], + 'customer_name' => $customerName !== '' ? $customerName : null, + 'payment_method' => $paymentMethod, + 'items' => $normalized, + 'item_count' => $itemCount, + 'subtotal' => $subtotal, + 'total_amount' => $subtotal, + 'notes' => $notes !== '' ? $notes : null, + ]); + + set_flash('success', $saleMode === 'normal' + ? tr('تم حفظ البيع العادي بنجاح.', 'Normal sale saved successfully.') + : tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.')); + redirect_to('sale.php', ['id' => $saleId]); + } + } +} + +require __DIR__ . '/header.php'; +?> +
+
+
+
+
+

+
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+

+ +
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+

+ 0 +
+
+

+

+
+
+
+
0.00
+
0.00
+
+
+ + +
+
+ +
+
+
+
+ + diff --git a/index.php b/index.php index 7205f3d..6fd54fc 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,224 @@ 0, 'today_revenue' => 0.0, 'pos_count' => 0, 'normal_count' => 0, 'recent' => []]; +try { + $metrics = dashboard_metrics(); +} catch (Throwable $e) { + $dbError = $e->getMessage(); +} +require __DIR__ . '/includes/header.php'; +?> -$phpVersion = PHP_VERSION; -$now = date('Y-m-d H:i:s'); -?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+
+

-
-
- Page updated: (UTC) -
- - + +
+ + + +
+ + + +
+
+
+
+
+
+ +
+

+
+
+
+ +
+
+
+
+
+ +
+

+
+
+
+ +
+
+
+
+
POS
+ +
+

+
+
+
+ +
+
+
+
+
+ +
+

+
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + +
+ +
+ +
+
+ +
+

+ - +
+
+ +
+

+ +
+ +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..e673e2b --- /dev/null +++ b/login.php @@ -0,0 +1,110 @@ + + + + + + + <?= h(tr('تسجيل الدخول', 'Sign in')) ?> · <?= h($projectName) ?> + + + + + + + + + + + + + + + +
+
+
+
+
+

+

+
+
3
+
3
+
2
+
+
+
+
+
+
+
+
+
+
+

+
+ AR + EN +
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+
+
+ + + +
+
+
+
+
+ + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..e5eea45 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ + $row) { + if ( + str_contains(mb_strtolower($row['supplier']), $lowerSearch) || + str_contains(mb_strtolower($row['reference']), $lowerSearch) + ) { + $filteredPurchases[$key] = $row; + } + } +} else { + $filteredPurchases = $allPurchases; +} + +// Pagination logic +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$total = count($filteredPurchases); +$totalPages = max(1, ceil($total / $limit)); +$offset = ($page - 1) * $limit; +$purchaseRows = array_slice($filteredPurchases, $offset, $limit, true); + +require __DIR__ . '/includes/header.php'; +?> +
+
+
+

+

+
+
+ +
+
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + 1): ?> + + +
+ + + + \ No newline at end of file diff --git a/reports.php b/reports.php new file mode 100644 index 0000000..f728ed6 --- /dev/null +++ b/reports.php @@ -0,0 +1,52 @@ + 0.0, 'branch_totals' => [], 'payment_totals' => [], 'product_totals' => [], 'sales_count' => 0]; +try { + $report = report_metrics(); +} catch (Throwable $e) { + $dbError = $e->getMessage(); +} +require __DIR__ . '/includes/header.php'; +?> +
+
+
+
+
+
+
+
+

+ +
+ +

+ +
+ $amount): ?> +
+ +
+ +
+
+
+
+

+ +

+ +
+ $amount): ?> +
+ +
+ +
+
+
+ diff --git a/sale.php b/sale.php new file mode 100644 index 0000000..7258b95 --- /dev/null +++ b/sale.php @@ -0,0 +1,72 @@ + 0) { + try { + $sale = fetch_sale($id); + } catch (Throwable $e) { + $dbError = $e->getMessage(); + } +} +require __DIR__ . '/includes/header.php'; +?> +
+ +
+ +
+

+

+ +
+ +
+
+
+

+
·
+
+
+ + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ diff --git a/sales.php b/sales.php new file mode 100644 index 0000000..26ea230 --- /dev/null +++ b/sales.php @@ -0,0 +1,191 @@ +prepare($countSql); + foreach ($params as $key => $value) { + $countStmt->bindValue($key, $value); + } + $countStmt->execute(); + $total = $countStmt->fetchColumn(); + $totalPages = max(1, ceil($total / $limit)); + + // Fetch Data + $sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY sale_date DESC LIMIT :limit OFFSET :offset'; + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value); + } + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $sales = $stmt->fetchAll(); + +} catch (Throwable $e) { + $dbError = $e->getMessage(); +} + +require __DIR__ . '/includes/header.php'; +?> +
+
+
+

+
+
+
+ + POS + +
+
+ +
+ + + +
+ + +
+
+ + +
+ +
+

+

+
+ POS + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + +
+
+ + 1): ?> + + + +
+ + + + \ No newline at end of file diff --git a/stock.php b/stock.php new file mode 100644 index 0000000..0ef05f5 --- /dev/null +++ b/stock.php @@ -0,0 +1,276 @@ +getMessage(); +} + +$categories = []; +$suppliers = []; +try { + $pdo = db(); + $categories = $pdo->query('SELECT id, name_ar, name_en FROM categories ORDER BY name_ar ASC')->fetchAll(); + $suppliers = $pdo->query('SELECT id, name FROM suppliers ORDER BY name ASC')->fetchAll(); +} catch (Throwable $e) { + // Ignore if not present +} + +// Search logic +$search = $_GET['q'] ?? ''; +$filteredStock = []; +if ($search && empty($dbError)) { + $lowerSearch = mb_strtolower($search); + foreach ($allStock as $key => $row) { + if ( + str_contains(mb_strtolower($row['sku']), $lowerSearch) || + str_contains(mb_strtolower($row['name']), $lowerSearch) + ) { + $filteredStock[$key] = $row; + } + } +} else { + $filteredStock = $allStock; +} + +// Pagination logic +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$total = count($filteredStock); +$totalPages = max(1, ceil($total / $limit)); +$offset = ($page - 1) * $limit; +$stockRows = array_slice($filteredStock, $offset, $limit, true); + +require __DIR__ . '/includes/header.php'; +?> + +
+
+
+

+

+
+
+ +
+
+ +
+
+ + +
+
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SKU
+ + + + + + + + +
+
+ + 1): ?> + + + +
+ + + + + + + diff --git a/suppliers.php b/suppliers.php new file mode 100644 index 0000000..7330c8c --- /dev/null +++ b/suppliers.php @@ -0,0 +1,251 @@ +prepare('INSERT INTO suppliers (name, contact_person, phone, email, address) VALUES (?, ?, ?, ?, ?)'); + $stmt->execute([$_POST['name'], $_POST['contact_person'] ?? '', $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '']); + set_flash('success', tr('تمت إضافة المورد بنجاح', 'Supplier added successfully')); + redirect_to('suppliers.php'); + } elseif ($action === 'edit') { + $stmt = $pdo->prepare('UPDATE suppliers SET name = ?, contact_person = ?, phone = ?, email = ?, address = ? WHERE id = ?'); + $stmt->execute([$_POST['name'], $_POST['contact_person'] ?? '', $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); + set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); + redirect_to('suppliers.php'); + } elseif ($action === 'delete') { + $stmt = $pdo->prepare('DELETE FROM suppliers WHERE id = ?'); + $stmt->execute([$_POST['id']]); + set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully')); + redirect_to('suppliers.php'); + } +} + +// Pagination & Search +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$offset = ($page - 1) * $limit; +$search = $_GET['q'] ?? ''; + +$where = '1=1'; +$params = []; +if ($search) { + $where .= ' AND (name LIKE ? OR phone LIKE ? OR email LIKE ?)'; + $params[] = "%$search%"; + $params[] = "%$search%"; + $params[] = "%$search%"; +} + +$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM suppliers WHERE $where"); +$totalStmt->execute($params); +$total = $totalStmt->fetchColumn(); +$totalPages = ceil($total / $limit); + +$queryStmt = $pdo->prepare("SELECT * FROM suppliers WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset"); +$queryStmt->execute($params); +$items = $queryStmt->fetchAll(); + +require __DIR__ . '/includes/header.php'; +?> + +
+
+
+

+

+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ID
+ + +
+
+ + 1): ?> + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/users.php b/users.php new file mode 100644 index 0000000..46c116b --- /dev/null +++ b/users.php @@ -0,0 +1,203 @@ + $acc) { + if ( + str_contains(mb_strtolower($acc['name_ar']), $lowerSearch) || + str_contains(mb_strtolower($acc['name_en']), $lowerSearch) || + str_contains(mb_strtolower($acc['username']), $lowerSearch) + ) { + $filteredAccounts[$key] = $acc; + } + } +} else { + $filteredAccounts = $allAccounts; +} + +// Pagination logic +$page = max(1, (int)($_GET['p'] ?? 1)); +$limit = 10; +$total = count($filteredAccounts); +$totalPages = max(1, ceil($total / $limit)); +$offset = ($page - 1) * $limit; +$accounts = array_slice($filteredAccounts, $offset, $limit, true); + +require __DIR__ . '/includes/header.php'; +?> +
+
+
+

+

+
+ +
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + $account): ?> + + + + + + + + + + + +
POS
+
+
+
+ + +
+
+ + 1): ?> + + +
+ + + + + + + \ No newline at end of file