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';
+?>
+
+
+
+
+
= h($pageTitle) ?>
+
= h(tr('إدارة تصنيفات المنتجات', 'Manage product categories')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ = h(tr('الاسم (عربي)', 'Name (AR)')) ?> |
+ = h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?> |
+ = h(tr('الوصف', 'Description')) ?> |
+ = h(tr('إجراءات', 'Actions')) ?> |
+
+
+
+
+ | = h(tr('لا توجد بيانات', 'No data found')) ?> |
+
+
+
+ | = h($item['id']) ?> |
+ = h($item['name_ar']) ?> |
+ = h($item['name_en']) ?> |
+ = h($item['description']) ?> |
+
+
+
+ |
+
+
+
+
+
+
+ 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';
+?>
+
+
+
+
+
= h($pageTitle) ?>
+
= h(tr('إدارة حسابات العملاء', 'Manage customer accounts')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ = h(tr('الاسم', 'Name')) ?> |
+ = h(tr('الهاتف', 'Phone')) ?> |
+ = h(tr('البريد', 'Email')) ?> |
+ = h(tr('العنوان', 'Address')) ?> |
+ = h(tr('إجراءات', 'Actions')) ?> |
+
+
+
+
+ | = h(tr('لا توجد بيانات', 'No data found')) ?> |
+
+
+
+ | = h($item['id']) ?> |
+ = h($item['name']) ?> |
+ = h($item['phone']) ?> |
+ = h($item['email']) ?> |
+ = h($item['address']) ?> |
+
+
+
+ |
+
+
+
+
+
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+