Autosave: 20260419-032550

This commit is contained in:
Flatlogic Bot 2026-04-19 03:25:43 +00:00
parent 033b73cd60
commit f6212d4e47
10 changed files with 1112 additions and 149 deletions

View File

@ -74,18 +74,18 @@ require __DIR__ . '/includes/header.php';
</section>
<section class="surface-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th><?= h(tr('الوصف', 'Description')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent">ID</th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الوصف', 'Description')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="5" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
@ -96,10 +96,10 @@ require __DIR__ . '/includes/header.php';
<td><?= h($item['name_en']) ?></td>
<td><?= h($item['description']) ?></td>
<td>
<button class="btn btn-sm btn-light text-primary border" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>

View File

@ -75,19 +75,19 @@ require __DIR__ . '/includes/header.php';
</section>
<section class="surface-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم', 'Name')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('البريد', 'Email')) ?></th>
<th><?= h(tr('العنوان', 'Address')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent">ID</th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم', 'Name')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الهاتف', 'Phone')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('البريد', 'Email')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('العنوان', 'Address')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
@ -99,10 +99,10 @@ require __DIR__ . '/includes/header.php';
<td><?= h($item['email']) ?></td>
<td><?= h($item['address']) ?></td>
<td>
<button class="btn btn-sm btn-light text-primary border" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>

View File

@ -95,18 +95,18 @@ require __DIR__ . '/includes/header.php';
<a class="btn btn-primary" href="<?= h(url_for('pos.php')) ?>"><?= h(tr('إنشاء بيع POS', 'Create POS sale')) ?></a>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th class="ps-3"><?= h(tr('رقم الإيصال', 'Receipt No')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('النوع', 'Type')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th class="text-end pe-3"><?= h(tr('الإجمالي', 'Total')) ?></th>
<th class="ps-3 text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('رقم الإيصال', 'Receipt No')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('النوع', 'Type')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التاريخ', 'Date')) ?></th>
<th class="text-end pe-3 text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Total')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php foreach ($metrics['recent'] as $sale): ?>
<tr>
<td class="ps-3">
@ -122,7 +122,7 @@ require __DIR__ . '/includes/header.php';
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
</div>

774
pos.php
View File

@ -1,3 +1,775 @@
<?php
$saleMode = 'pos';
require_once __DIR__ . '/includes/sale_form.php';
require_once __DIR__ . '/includes/app.php';
$user = require_roles(['owner', 'manager', 'cashier']);
$pageTitle = tr('نقاط البيع', 'Smart POS');
$activeNav = 'pos';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
try {
$pdo = db();
$categories = $pdo->query('SELECT id, name_ar, name_en FROM categories ORDER BY name_ar ASC')->fetchAll();
} catch (Throwable $e) {
$categories = [];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$branchCode = trim((string) ($_POST['branch_code'] ?? ''));
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$notes = trim((string) ($_POST['notes'] ?? ''));
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
if (!in_array($branchCode, $allowedBranches, true)) {
$error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.');
} elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer'], true)) {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى السلة.', 'Add at least one item to the cart.');
} else {
$normalized = [];
$subtotal = 0.0;
$itemCount = 0;
foreach ($items as $item) {
$sku = (string) ($item['sku'] ?? '');
$qty = (int) ($item['qty'] ?? 0);
if (!isset($catalog[$sku]) || $qty < 1) {
continue;
}
$product = $catalog[$sku];
$price = (float) $product['price'];
$lineTotal = $price * $qty;
$normalized[] = [
'sku' => $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', tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.'));
redirect_to('sale.php', ['id' => $saleId]);
}
}
}
require __DIR__ . '/includes/header.php';
?>
<style>
/* Modern POS Styles */
.pos-wrapper {
height: calc(100vh - 140px); /* Adjust based on your header */
min-height: 600px;
display: flex;
gap: 1.5rem;
}
.pos-products-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pos-cart-area {
width: 380px;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
/* Categories Scroll */
.cat-scroll-container {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: none; /* Firefox */
}
.cat-scroll-container::-webkit-scrollbar {
display: none; /* Chrome */
}
.cat-btn {
white-space: nowrap;
border-radius: 20px;
padding: 0.5rem 1.25rem;
font-weight: 500;
transition: all 0.2s;
background: #fff;
border: 1px solid #e9ecef;
color: #495057;
cursor: pointer;
}
.cat-btn:hover {
background: #f8f9fa;
}
.cat-btn.active {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
/* Products Grid */
.products-grid {
flex: 1;
overflow-y: auto;
padding-right: 0.5rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.25rem;
align-content: start;
}
.product-card {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
position: relative;
user-select: none;
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
border-color: #0dcaf0;
}
.product-card:active {
transform: translateY(0);
}
.product-img-wrapper {
height: 120px;
width: 100%;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.product-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-placeholder {
font-size: 2rem;
color: #dee2e6;
}
.product-info {
padding: 0.75rem;
text-align: center;
}
.product-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: #212529;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.2;
height: 2.4em;
}
.product-price {
font-weight: 700;
color: #0d6efd;
font-size: 1.05rem;
}
/* Cart Styles */
.cart-header {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
padding: 1rem 1.25rem;
}
.cart-items {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px dashed #e9ecef;
}
.cart-item-info {
flex: 1;
padding-right: 1rem;
}
.cart-item-title {
font-weight: 600;
font-size: 0.9rem;
color: #343a40;
}
.cart-item-price {
color: #6c757d;
font-size: 0.85rem;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f8f9fa;
border-radius: 20px;
padding: 0.25rem;
}
.cart-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: none;
background: #fff;
color: #495057;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.cart-btn:hover {
background: #e9ecef;
}
.cart-qty {
font-weight: 600;
font-size: 0.9rem;
min-width: 20px;
text-align: center;
}
.cart-footer {
padding: 1.25rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.95rem;
color: #495057;
}
.summary-total {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: 700;
color: #212529;
padding-top: 0.5rem;
border-top: 1px solid #dee2e6;
}
/* Pay / Action Buttons */
.btn-pay {
background: linear-gradient(90deg, #198754, #20c997);
border: none;
color: white;
font-size: 1.1rem;
font-weight: 600;
padding: 0.75rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.2);
transition: all 0.2s;
}
.btn-pay:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(25, 135, 84, 0.3);
}
.btn-pay:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
[dir="ltr"] .cart-item-info {
padding-right: 0;
padding-left: 1rem;
}
</style>
<?php if ($error !== ''): ?>
<div class="alert alert-danger shadow-sm border-0 rounded-3 mb-4"><?= h($error) ?></div>
<?php endif; ?>
<div class="pos-wrapper">
<!-- Left Area: Products -->
<div class="pos-products-area">
<!-- Top Bar: Search & Hold -->
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="position-relative" style="width: 300px;">
<input type="text" id="posSearch" class="form-control rounded-pill ps-4" placeholder="<?= h(tr('بحث عن صنف...', 'Search item...')) ?>" autocomplete="off">
<i class="bi bi-search position-absolute top-50 translate-middle-y text-muted" style="left: 15px;"></i>
</div>
<div>
<button class="btn btn-outline-primary rounded-pill px-4 shadow-sm" onclick="openHeldOrdersModal()">
<i class="bi bi-clock-history me-1"></i> <span id="heldOrdersCount">0</span> <?= h(tr('قيد الانتظار', 'Held Orders')) ?>
</button>
</div>
</div>
<!-- Categories -->
<div class="cat-scroll-container mb-4" id="catContainer">
<button class="cat-btn active" data-cat="all" onclick="filterCat('all')"><?= h(tr('الكل', 'All')) ?></button>
<?php foreach($categories as $cat):
$catId = h($cat['id']);
$catName = h(current_lang() === 'ar' ? $cat['name_ar'] : $cat['name_en']);
?>
<button class="cat-btn" data-cat="<?= $catId ?>" onclick="filterCat('<?= $catId ?>')"><?= $catName ?></button>
<?php endforeach; ?>
</div>
<!-- Grid -->
<div class="products-grid" id="productsGrid">
<?php foreach ($catalog as $sku => $item):
$itemSku = h($sku);
$itemName = h(current_lang() === 'ar' ? $item['name_ar'] : $item['name_en']);
$itemPrice = h($item['price']);
$itemCat = h($item['category_id'] ?? '');
$imageUrl = !empty($item['image_url']) ? h($item['image_url']) : '';
?>
<div class="product-card" data-sku="<?= $itemSku ?>" data-name="<?= $itemName ?>" data-price="<?= $itemPrice ?>" data-cat="<?= $itemCat ?>" onclick="addToCart('<?= $itemSku ?>')">
<div class="product-img-wrapper">
<?php if (!empty($imageUrl)):
$imgAlt = h(tr('صورة المنتج', 'Product Image'));
?>
<img src="<?= $imageUrl ?>" alt="<?= $imgAlt ?>" class="product-img" loading="lazy">
<?php else:
$placeholderText = h(tr('لا توجد صورة', 'No Image'));
?>
<i class="bi bi-image product-placeholder"></i>
<?php endif; ?>
</div>
<div class="product-info">
<div class="product-title"><?= $itemName ?></div>
<div class="product-price"><?= h(currency($itemPrice)) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Right Area: Smart Cart -->
<div class="pos-cart-area">
<div class="cart-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><i class="bi bi-cart3 me-2"></i><?= h(tr('سلة المشتريات', 'Shopping Cart')) ?></h5>
<span class="badge bg-light text-primary rounded-pill px-2" id="cartBadgeCount">0</span>
</div>
<div class="p-3 bg-light border-bottom">
<select class="form-select border-0 shadow-sm rounded-3 mb-2" id="posBranch" <?= count($allowedBranches) === 1 ? 'disabled' : '' ?> required>
<?php foreach ($allowedBranches as $branchCode):
$branchLabel = h(branch_label($branchCode));
?>
<option value="<?= h($branchCode) ?>"><?= $branchLabel ?></option>
<?php endforeach; ?>
</select>
<input type="text" id="posCustomer" class="form-control border-0 shadow-sm rounded-3" placeholder="<?= h(tr('اسم العميل (اختياري)', 'Customer Name (Optional)')) ?>">
</div>
<div class="cart-items" id="cartItemsList">
<!-- Items rendered via JS -->
<div class="text-center text-muted mt-5 pt-4">
<i class="bi bi-basket2 fs-1 d-block mb-3 opacity-50"></i>
<p><?= h(tr('السلة فارغة، اختر بعض الأصناف.', 'Cart is empty, pick some items.')) ?></p>
</div>
</div>
<div class="cart-footer">
<div class="summary-row">
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
<span id="cartSubtotalVal">0.000</span>
</div>
<div class="summary-total">
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
<span class="text-primary" id="cartTotalVal">0.000</span>
</div>
<div class="action-grid">
<button class="btn btn-outline-warning rounded-3 fw-semibold" onclick="holdCart()" id="btnHold" disabled>
<i class="bi bi-pause-circle"></i> <?= h(tr('تعليق', 'Hold')) ?>
</button>
<button class="btn btn-outline-danger rounded-3 fw-semibold" onclick="clearCart()" id="btnClear" disabled>
<i class="bi bi-trash"></i> <?= h(tr('إلغاء', 'Clear')) ?>
</button>
</div>
<button class="btn w-100 btn-pay" id="btnPay" onclick="openPaymentModal()" disabled>
<i class="bi bi-wallet2 me-2"></i> <?= h(tr('دفع وإصدار', 'Pay & Checkout')) ?>
</button>
</div>
</div>
</div>
<!-- Hidden Form for Submission -->
<form method="POST" id="checkoutForm" style="display: none;">
<input type="hidden" name="branch_code" id="inputBranch">
<input type="hidden" name="customer_name" id="inputCustomer">
<input type="hidden" name="payment_method" id="inputPayment">
<input type="hidden" name="notes" id="inputNotes">
<input type="hidden" name="cart_json" id="inputCart">
</form>
<!-- Payment Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= h(tr('إتمام الدفع', 'Complete Payment')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center pt-2">
<h2 class="text-primary fw-bold mb-4" id="modalTotalAmount">0.000</h2>
<div class="d-grid gap-3">
<button class="btn btn-lg btn-outline-success rounded-pill fw-semibold" onclick="submitSale('cash')">
<i class="bi bi-cash-coin me-2"></i> <?= h(tr('نقداً', 'Cash')) ?>
</button>
<button class="btn btn-lg btn-outline-primary rounded-pill fw-semibold" onclick="submitSale('card')">
<i class="bi bi-credit-card me-2"></i> <?= h(tr('بطاقة', 'Card')) ?>
</button>
<button class="btn btn-lg btn-outline-secondary rounded-pill fw-semibold" onclick="submitSale('transfer')">
<i class="bi bi-phone me-2"></i> <?= h(tr('تحويل بنكي', 'Transfer')) ?>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Held Orders Modal -->
<div class="modal fade" id="heldOrdersModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header bg-light border-0">
<h5 class="modal-title fw-bold"><i class="bi bi-clock-history me-2 text-primary"></i><?= h(tr('الطلبات المعلقة', 'Held Orders')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div class="list-group list-group-flush" id="heldOrdersList">
<!-- Rendered by JS -->
</div>
</div>
</div>
</div>
</div>
<script>
let cart = {};
let catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
let currencyLabel = '<?= h(tr('ر.ع', 'OMR')) ?>';
// Product Grid Filtering & Searching
function filterCat(catId) {
document.querySelectorAll('.cat-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector(`.cat-btn[data-cat="${catId}"]`).classList.add('active');
applyFilters();
}
document.getElementById('posSearch').addEventListener('input', applyFilters);
function applyFilters() {
const q = document.getElementById('posSearch').value.toLowerCase();
const activeCat = document.querySelector('.cat-btn.active').dataset.cat;
document.querySelectorAll('.product-card').forEach(card => {
const name = card.dataset.name.toLowerCase();
const cat = card.dataset.cat;
const matchesSearch = name.includes(q);
const matchesCat = (activeCat === 'all' || cat === activeCat);
if (matchesSearch && matchesCat) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
}
// Cart Logic
function addToCart(sku) {
if (!catalogData[sku]) return;
if (cart[sku]) {
cart[sku].qty += 1;
} else {
cart[sku] = {
sku: sku,
name: document.querySelector(`.product-card[data-sku="${sku}"]`).dataset.name,
price: parseFloat(document.querySelector(`.product-card[data-sku="${sku}"]`).dataset.price),
qty: 1
};
}
renderCart();
}
function updateQty(sku, delta) {
if (!cart[sku]) return;
cart[sku].qty += delta;
if (cart[sku].qty <= 0) {
delete cart[sku];
}
renderCart();
}
function renderCart() {
const list = document.getElementById('cartItemsList');
const badge = document.getElementById('cartBadgeCount');
const subVal = document.getElementById('cartSubtotalVal');
const totalVal = document.getElementById('cartTotalVal');
const btnPay = document.getElementById('btnPay');
const btnHold = document.getElementById('btnHold');
const btnClear = document.getElementById('btnClear');
list.innerHTML = '';
let total = 0;
let count = 0;
const skus = Object.keys(cart);
if (skus.length === 0) {
list.innerHTML = `
<div class="text-center text-muted mt-5 pt-4">
<i class="bi bi-basket2 fs-1 d-block mb-3 opacity-50"></i>
<p><?= h(tr('السلة فارغة، اختر بعض الأصناف.', 'Cart is empty, pick some items.')) ?></p>
</div>
`;
badge.innerText = '0';
subVal.innerText = `0.000 ${currencyLabel}`;
totalVal.innerText = `0.000 ${currencyLabel}`;
btnPay.disabled = true;
btnHold.disabled = true;
btnClear.disabled = true;
return;
}
skus.forEach(sku => {
const item = cart[sku];
const lineTotal = item.price * item.qty;
total += lineTotal;
count += item.qty;
list.innerHTML += `
<div class="cart-item">
<div class="cart-item-info">
<div class="cart-item-title">
${item.name}
</div>
<div class="cart-item-price">
${item.price.toFixed(3)} ${currencyLabel}
</div>
</div>
<div class="cart-item-controls">
<button class="cart-btn" onclick="updateQty('${sku}', -1)"><i class="bi bi-dash"></i></button>
<span class="cart-qty">${item.qty}</span>
<button class="cart-btn" onclick="updateQty('${sku}', 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`;
});
badge.innerText = count;
const totalStr = `${total.toFixed(3)} ${currencyLabel}`;
subVal.innerText = totalStr;
totalVal.innerText = totalStr;
document.getElementById('modalTotalAmount').innerText = totalStr;
btnPay.disabled = false;
btnHold.disabled = false;
btnClear.disabled = false;
}
function clearCart() {
cart = {};
document.getElementById('posCustomer').value = '';
renderCart();
}
// Payment & Checkout
let paymentModalObj = null;
document.addEventListener('DOMContentLoaded', () => {
paymentModalObj = new bootstrap.Modal(document.getElementById('paymentModal'));
updateHeldOrdersCount();
});
function openPaymentModal() {
if (Object.keys(cart).length === 0) return;
paymentModalObj.show();
}
function submitSale(method) {
const branch = document.getElementById('posBranch').value || '<?= h($allowedBranches[0] ?? '') ?>';
const customer = document.getElementById('posCustomer').value;
const itemsArr = Object.values(cart).map(item => ({
sku: item.sku,
qty: item.qty
}));
document.getElementById('inputBranch').value = branch;
document.getElementById('inputCustomer').value = customer;
document.getElementById('inputPayment').value = method;
document.getElementById('inputCart').value = JSON.stringify(itemsArr);
document.getElementById('checkoutForm').submit();
}
// Held Orders Logic (localStorage)
function getHeldOrders() {
const raw = localStorage.getItem('posHeldOrders');
return raw ? JSON.parse(raw) : [];
}
function saveHeldOrders(orders) {
localStorage.setItem('posHeldOrders', JSON.stringify(orders));
updateHeldOrdersCount();
}
function updateHeldOrdersCount() {
const orders = getHeldOrders();
document.getElementById('heldOrdersCount').innerText = orders.length;
}
function holdCart() {
if (Object.keys(cart).length === 0) return;
const orders = getHeldOrders();
const orderName = document.getElementById('posCustomer').value || `<?= h(tr('عميل غير معروف', 'Unknown')) ?> #${Math.floor(Math.random()*1000)}`;
const branch = document.getElementById('posBranch').value;
const newOrder = {
id: Date.now(),
name: orderName,
branch: branch,
cart: JSON.parse(JSON.stringify(cart)),
time: new Date().toLocaleTimeString()
};
orders.push(newOrder);
saveHeldOrders(orders);
// Clear current cart via toast
clearCart();
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2000
});
Toast.fire({
icon: 'success',
title: '<?= h(tr('تم تعليق الطلب', 'Order held successfully')) ?>'
});
}
let heldOrdersModalObj = null;
function openHeldOrdersModal() {
if (!heldOrdersModalObj) {
heldOrdersModalObj = new bootstrap.Modal(document.getElementById('heldOrdersModal'));
}
const list = document.getElementById('heldOrdersList');
const orders = getHeldOrders();
list.innerHTML = '';
if (orders.length === 0) {
list.innerHTML = `<div class="p-4 text-center text-muted"><?= h(tr('لا توجد طلبات معلقة', 'No held orders')) ?></div>`;
} else {
orders.forEach((order, index) => {
const count = Object.values(order.cart).reduce((sum, i) => sum + i.qty, 0);
list.innerHTML += `
<div class="list-group-item d-flex justify-content-between align-items-center p-3">
<div>
<h6 class="mb-1 fw-bold">
${order.name}
</h6>
<small class="text-muted"><i class="bi bi-clock me-1"></i>${order.time} &bull; ${count} <?= h(tr('قطعة', 'items')) ?></small>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary rounded-pill px-3" onclick="resumeOrder(${index})"><?= h(tr('استعادة', 'Resume')) ?></button>
<button class="btn btn-sm btn-outline-danger rounded-pill" onclick="deleteHeldOrder(${index})"><i class="bi bi-trash"></i></button>
</div>
</div>
`;
});
}
heldOrdersModalObj.show();
}
function resumeOrder(index) {
const orders = getHeldOrders();
if (!orders[index]) return;
const order = orders[index];
// Warn if cart is not empty
if (Object.keys(cart).length > 0) {
if (!confirm('<?= h(tr('السلة الحالية غير فارغة، هل تريد استبدالها؟', 'Current cart is not empty, replace it?')) ?>')) {
return;
}
}
cart = order.cart;
document.getElementById('posCustomer').value = order.name.includes('#') ? '' : order.name;
if (document.getElementById('posBranch').querySelector(`option[value="${order.branch}"]`)) {
document.getElementById('posBranch').value = order.branch;
}
orders.splice(index, 1);
saveHeldOrders(orders);
renderCart();
heldOrdersModalObj.hide();
}
function deleteHeldOrder(index) {
const orders = getHeldOrders();
orders.splice(index, 1);
saveHeldOrders(orders);
openHeldOrdersModal(); // re-render
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -55,19 +55,19 @@ require __DIR__ . '/includes/header.php';
</section>
<section class="surface-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th><?= h(tr('المورد', 'Supplier')) ?></th>
<th><?= h(tr('المرجع', 'Reference')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th><?= h(tr('الوصول المتوقع', 'ETA')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المورد', 'Supplier')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المرجع', 'Reference')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الحالة', 'Status')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الوصول المتوقع', 'ETA')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($purchaseRows)): ?>
<tr><td colspan="6" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
@ -79,10 +79,10 @@ require __DIR__ . '/includes/header.php';
<td><span class="badge text-bg-light border"><?= h($row['status']) ?></span></td>
<td><?= h($row['eta']) ?></td>
<td>
<button class="btn btn-sm btn-light text-primary border" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>

View File

@ -42,17 +42,17 @@ require __DIR__ . '/includes/header.php';
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('الكاشير', 'Cashier')) ?></span><strong><?= h((string) $sale['cashier_name']) ?></strong></div></div>
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('الإجمالي', 'Total')) ?></span><strong><?= h(currency((float) $sale['total_amount'])) ?></strong></div></div>
</div>
<div class="table-responsive mb-4">
<table class="table app-table align-middle mb-0">
<thead>
<tr>
<th><?= h(tr('الصنف', 'Item')) ?></th>
<th><?= h(tr('الكمية', 'Qty')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th><?= h(tr('الإجمالي', 'Line total')) ?></th>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الصنف', 'Item')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الكمية', 'Qty')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('السعر', 'Price')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Line total')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php foreach ($sale['items'] as $item): ?>
<tr>
<td><?= h(current_lang() === 'ar' ? ($item['name_ar'] ?? $item['sku']) : ($item['name_en'] ?? $item['sku'])) ?></td>
@ -63,7 +63,7 @@ require __DIR__ . '/includes/header.php';
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (!empty($sale['notes'])): ?>
<div class="alert alert-light border mb-0"><strong><?= h(tr('ملاحظات:', 'Notes:')) ?></strong> <?= h((string) $sale['notes']) ?></div>
<?php endif; ?>

View File

@ -100,20 +100,20 @@ require __DIR__ . '/includes/header.php';
</div>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<tr>
<th><?= h(tr('الإيصال', 'Receipt')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('النوع', 'Type')) ?></th>
<th><?= h(tr('الكاشير', 'Cashier')) ?></th>
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإيصال', 'Receipt')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('النوع', 'Type')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الكاشير', 'Cashier')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Total')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التاريخ', 'Date')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php foreach ($sales as $sale): ?>
<tr>
<td>
@ -129,10 +129,10 @@ require __DIR__ . '/includes/header.php';
<a class="btn btn-sm btn-light text-primary border me-1" href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>">
<i class="bi bi-eye"></i>
</a>
<button class="btn btn-sm btn-light text-secondary border me-1" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-secondary rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
@ -140,7 +140,7 @@ require __DIR__ . '/includes/header.php';
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4">

297
stock.php
View File

@ -5,6 +5,93 @@ $pageTitle = tr('المخزون', 'Stock');
$activeNav = 'stock';
$dbError = null;
// Handle AJAX actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
header('Content-Type: application/json');
$pdo = db();
if ($_POST['action'] === 'save') {
try {
$sku = $_POST['sku'] ?? '';
$name = $_POST['name'] ?? '';
$price = (float)($_POST['price'] ?? 0);
$base_stock = (int)($_POST['base_stock'] ?? 0);
$vat = (float)($_POST['vat'] ?? 5);
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
$supplier_id = !empty($_POST['supplier_id']) ? (int)$_POST['supplier_id'] : null;
if (!$sku || !$name) {
echo json_encode(['success' => false, 'error' => 'Missing SKU or Name']);
exit;
}
$image_url = $_POST['existing_image_url'] ?? null;
if (isset($_FILES['picture']) && $_FILES['picture']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/assets/images/items/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$ext = pathinfo($_FILES['picture']['name'], PATHINFO_EXTENSION);
$filename = time() . '_' . rand(1000, 9999) . '.' . $ext;
if (move_uploaded_file($_FILES['picture']['tmp_name'], $uploadDir . $filename)) {
$image_url = 'assets/images/items/' . $filename;
}
}
$stmt = $pdo->prepare('SELECT id FROM items WHERE sku = ?');
$stmt->execute([$sku]);
$existing = $stmt->fetch();
if (isset($_POST['original_sku']) && $_POST['original_sku'] !== '') {
$orig_sku = $_POST['original_sku'];
if ($existing && $existing['id'] != ($pdo->query("SELECT id FROM items WHERE sku = " . $pdo->quote($orig_sku))->fetchColumn() ?: -1)) {
echo json_encode(['success' => false, 'error' => 'SKU already exists']);
exit;
}
$sql = "UPDATE items SET sku=?, name=?, price=?, base_stock=?, vat=?, category_id=?, supplier_id=? " . ($image_url ? ", image_url=?" : "") . " WHERE sku=?";
$params = [$sku, $name, $price, $base_stock, $vat, $category_id, $supplier_id];
if ($image_url) {
$params[] = $image_url;
}
$params[] = $orig_sku;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} else {
if ($existing) {
echo json_encode(['success' => false, 'error' => 'SKU already exists']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO items (sku, name, price, base_stock, vat, category_id, supplier_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$sku, $name, $price, $base_stock, $vat, $category_id, $supplier_id, $image_url]);
}
echo json_encode(['success' => true]);
exit;
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
}
if ($_POST['action'] === 'delete') {
try {
$sku = $_POST['sku'] ?? '';
if (!$sku) {
echo json_encode(['success' => false, 'error' => 'Missing SKU']);
exit;
}
$stmt = $pdo->prepare('DELETE FROM items WHERE sku = ?');
$stmt->execute([$sku]);
echo json_encode(['success' => true]);
exit;
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
}
}
$allStock = [];
try {
$allStock = stock_snapshot();
@ -56,10 +143,10 @@ require __DIR__ . '/includes/header.php';
<div class="row g-4 align-items-center mb-3">
<div class="col-lg-8">
<h3 class="h5 mb-1"><i class="bi bi-box-seam me-2"></i><?= h(tr('قائمة الأصناف والمخزون', 'Items & Stock List')) ?></h3>
<p class="text-muted mb-0"><?= h(tr('احتساب مبسط = الرصيد الابتدائي - الكميات المباعة في هذا الإصدار الأول.', 'Starter calculation = opening stock minus sold quantities in this first version.')) ?></p>
<p class="text-muted mb-0"><?= h(tr('إدارة الأصناف وجرد المخزون.', 'Manage items and inventory.')) ?></p>
</div>
<div class="col-lg-4 text-lg-end">
<button type="button" class="btn btn-primary" onclick="openItemModal()">
<button type="button" class="btn btn-primary shadow-sm" onclick="openItemModal()">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة صنف', 'Add Item')) ?>
</button>
</div>
@ -68,7 +155,12 @@ require __DIR__ . '/includes/header.php';
<form class="mb-4" method="GET" action="stock.php">
<div class="row g-2">
<div class="col-md-4">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث برمز الصنف أو الاسم...', 'Search by SKU or name...')) ?>" value="<?= h($search) ?>">
<div class="input-group">
<input type="text" name="q" id="searchInput" class="form-control" placeholder="<?= h(tr('بحث برمز الصنف أو الاسم...', 'Search by SKU or name...')) ?>" value="<?= h($search) ?>" autocomplete="off">
<button class="btn btn-outline-secondary bg-white" type="button" id="clearSearchBtn" style="<?= empty($search) ? 'display: none;' : '' ?>" title="<?= h(tr('مسح البحث', 'Clear Search')) ?>">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="col-md-3">
<select name="category" class="form-select">
@ -87,7 +179,7 @@ require __DIR__ . '/includes/header.php';
</select>
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" type="submit">
<button class="btn btn-primary w-100 shadow-sm" type="submit">
<i class="bi bi-funnel"></i> <?= h(tr('تصفية', 'Filter')) ?>
</button>
</div>
@ -99,45 +191,54 @@ require __DIR__ . '/includes/header.php';
<?php if ($dbError): ?>
<div class="alert alert-warning"><?= h($dbError) ?></div>
<?php else: ?>
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead class="table-primary">
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th>SKU</th>
<th width="60"><?= h(tr('صورة', 'Pic')) ?></th>
<th><?= h(tr('الصنف', 'Product')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th><?= h(tr('افتتاحي', 'Opening')) ?></th>
<th><?= h(tr('مباع', 'Sold')) ?></th>
<th><?= h(tr('متاح', 'Available')) ?></th>
<th><?= h(tr('التنبيه', 'Signal')) ?></th>
<th class="text-end"><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent">SKU</th>
<th width="70" class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('صورة', 'Pic')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent text-start"><?= h(tr('الصنف', 'Product')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('السعر', 'Price')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('افتتاحي', 'Opening')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('مباع', 'Sold')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('متاح', 'Available')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التنبيه', 'Signal')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent text-end pe-4"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($stockRows)): ?>
<tr><td colspan="8" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<tr><td colspan="9" class="text-center text-muted py-5"><i class="bi bi-inbox fs-1 d-block text-black-50 mb-2"></i> <?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($stockRows as $row): ?>
<tr>
<td><?= h($row['sku']) ?></td>
<td><?= h($row['name']) ?></td>
<td><?= h(currency($row['price'])) ?></td>
<td><?= h((string) $row['base_stock']) ?></td>
<td><?= h((string) $row['sold']) ?></td>
<td class="fw-semibold"><?= h((string) $row['available']) ?></td>
<tr style="transition: all 0.2s ease;">
<td class="text-secondary"><?= h($row['sku']) ?></td>
<td>
<?php if ($row['available'] <= 12): ?>
<span class="badge text-bg-warning"><?= h(tr('منخفض', 'Low')) ?></span>
<?php if (!empty($row['image_url'])): ?>
<img src="<?= h($row['image_url']) ?>" alt="pic" class="img-thumbnail p-0 shadow-sm" style="width: 45px; height: 45px; object-fit: cover; border-radius: 10px;">
<?php else: ?>
<span class="badge text-bg-light border"><?= h(tr('مستقر', 'Stable')) ?></span>
<div class="bg-light text-muted d-flex align-items-center justify-content-center shadow-sm mx-auto" style="width: 45px; height: 45px; border-radius: 10px; font-size: 1rem;">
<i class="bi bi-box-seam"></i>
</div>
<?php endif; ?>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary border" onclick="openItemModal('<?= h($row['sku']) ?>', '<?= h(addslashes($row['name'])) ?>', '<?= h($row['price']) ?>', '<?= h($row['base_stock']) ?>', '<?= h($row['vat'] ?? 5) ?>', '<?= h($row['category_id'] ?? '') ?>', '<?= h($row['supplier_id'] ?? '') ?>', '<?= h($row['image_url'] ?? '') ?>')" data-bs-toggle="tooltip" title="<?= h(tr('تعديل', 'Edit')) ?>">
<td class="fw-bold text-start text-dark"><?= h($row['name']) ?></td>
<td><span class="badge bg-light text-dark border px-2 py-1"><?= h(currency($row['price'])) ?></span></td>
<td class="text-secondary"><?= h((string) $row['base_stock']) ?></td>
<td class="text-secondary"><?= h((string) $row['sold']) ?></td>
<td class="fw-bold text-primary fs-6"><?= h((string) $row['available']) ?></td>
<td>
<?php if ($row['available'] <= 12): ?>
<span class="badge bg-warning bg-opacity-25 text-dark px-3 py-2 rounded-pill"><i class="bi bi-exclamation-triangle me-1"></i> <?= h(tr('منخفض', 'Low')) ?></span>
<?php else: ?>
<span class="badge bg-success bg-opacity-10 text-success px-3 py-2 rounded-pill border border-success border-opacity-25"><i class="bi bi-check2-circle me-1"></i> <?= h(tr('مستقر', 'Stable')) ?></span>
<?php endif; ?>
</td>
<td class="text-end pe-3">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="openItemModal('<?= h($row['sku']) ?>', '<?= h(addslashes($row['name'])) ?>', '<?= h($row['price']) ?>', '<?= h($row['base_stock']) ?>', '<?= h($row['vat'] ?? 5) ?>', '<?= h($row['category_id'] ?? '') ?>', '<?= h($row['supplier_id'] ?? '') ?>', '<?= h($row['image_url'] ?? '') ?>')" data-bs-toggle="tooltip" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="mockDelete()" data-bs-toggle="tooltip" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem('<?= h(addslashes($row['sku'])) ?>')" data-bs-toggle="tooltip" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
@ -165,12 +266,14 @@ require __DIR__ . '/includes/header.php';
<div class="modal fade" id="itemModal" tabindex="-1" aria-labelledby="itemModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form onsubmit="handleItemSubmit(event)">
<form onsubmit="handleItemSubmit(event)" id="itemForm">
<div class="modal-header bg-primary text-white ">
<h5 class="modal-title" id="itemModalLabel"><?= h(tr('إضافة / تعديل صنف', 'Add / Edit Item')) ?></h5>
<button type="button" class="btn-close btn-close-white " data-bs-dismiss="modal" aria-label="Close" ></button>
</div>
<div class="modal-body">
<input type="hidden" id="item_original_sku">
<input type="hidden" id="item_existing_image_url">
<div class="mb-3 text-center">
<label class="form-label d-block text-start"><?= h(tr('صورة الصنف', 'Item Picture')) ?></label>
<input type="file" class="form-control" id="item_picture" accept="image/*">
@ -178,7 +281,7 @@ require __DIR__ . '/includes/header.php';
<div class="mb-3">
<label class="form-label"><?= h(tr('رمز الصنف (SKU)', 'SKU')) ?></label>
<div class="input-group">
<input type="text" class="form-control" id="item_sku" required maxlength="8">
<input type="text" class="form-control" id="item_sku" required maxlength="15">
<button type="button" class="btn btn-outline-secondary" onclick="suggestSKU()" title="<?= h(tr('اقتراح رمز', 'Suggest SKU')) ?>">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -204,7 +307,7 @@ require __DIR__ . '/includes/header.php';
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?></label>
<select class="form-select" id="item_category" required>
<select class="form-select" id="item_category">
<option value=""><?= h(tr('-- اختر التصنيف --', '-- Select Category --')) ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= h($cat['id']) ?>"><?= h(current_lang() === 'ar' ? $cat['name_ar'] : $cat['name_en']) ?></option>
@ -213,7 +316,7 @@ require __DIR__ . '/includes/header.php';
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('المورد', 'Supplier')) ?></label>
<select class="form-select" id="item_supplier" required>
<select class="form-select" id="item_supplier">
<option value=""><?= h(tr('-- اختر المورد --', '-- Select Supplier --')) ?></option>
<?php foreach($suppliers as $sup): ?>
<option value="<?= h($sup['id']) ?>"><?= h($sup['name']) ?></option>
@ -223,7 +326,9 @@ require __DIR__ . '/includes/header.php';
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('حفظ التغييرات', 'Save Changes')) ?></button>
<button type="submit" class="btn btn-primary" id="saveItemBtn">
<i class="bi bi-save me-1"></i> <?= h(tr('حفظ التغييرات', 'Save Changes')) ?>
</button>
</div>
</form>
</div>
@ -250,9 +355,47 @@ document.addEventListener('DOMContentLoaded', function () {
});
itemModalObj = new bootstrap.Modal(document.getElementById('itemModal'));
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearchBtn');
let searchTimeout;
if (searchInput) {
if (searchInput.value.length > 0) {
searchInput.focus();
const valLen = searchInput.value.length;
searchInput.setSelectionRange(valLen, valLen);
}
searchInput.addEventListener('input', function() {
const val = this.value;
if (clearSearchBtn) {
clearSearchBtn.style.display = val.length > 0 ? 'block' : 'none';
}
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const trimmed = val.trim();
if (trimmed.length >= 2 || trimmed.length === 0) {
this.closest('form').submit();
}
}, 600);
});
}
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', function() {
if (searchInput) {
searchInput.value = '';
searchInput.closest('form').submit();
}
});
}
});
function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '5', category_id = '', supplier_id = '', image_url = '') {
document.getElementById('item_original_sku').value = sku;
document.getElementById('item_existing_image_url').value = image_url;
document.getElementById('item_sku').value = sku;
document.getElementById('item_name').value = name;
document.getElementById('item_price').value = price;
@ -270,27 +413,61 @@ function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '
preview.id = 'image_preview';
preview.src = image_url;
preview.style.maxHeight = '100px';
preview.className = 'mt-2 rounded';
preview.className = 'mt-2 rounded shadow-sm border';
document.getElementById('item_picture').parentElement.appendChild(preview);
}
document.getElementById('item_picture').value = '';
itemModalObj.show();
}
function handleItemSubmit(e) {
async function handleItemSubmit(e) {
e.preventDefault();
itemModalObj.hide();
Swal.fire({
title: '<?= h(tr('تم الحفظ', 'Saved')) ?>',
text: '<?= h(tr('البيانات الحالية مخزنة كنسخة تجريبية، سيتم تفعيل الحفظ الفعلي لاحقاً.', 'Current data is a starter mock, real saving will be enabled later.')) ?>',
icon: 'success',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
});
const btn = document.getElementById('saveItemBtn');
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
btn.disabled = true;
const formData = new FormData();
formData.append('action', 'save');
formData.append('original_sku', document.getElementById('item_original_sku').value);
formData.append('sku', document.getElementById('item_sku').value);
formData.append('name', document.getElementById('item_name').value);
formData.append('price', document.getElementById('item_price').value);
formData.append('base_stock', document.getElementById('item_base_stock').value);
formData.append('vat', document.getElementById('item_vat').value);
formData.append('category_id', document.getElementById('item_category').value);
formData.append('supplier_id', document.getElementById('item_supplier').value);
formData.append('existing_image_url', document.getElementById('item_existing_image_url').value);
const picInput = document.getElementById('item_picture');
if (picInput.files.length > 0) {
formData.append('picture', picInput.files[0]);
}
try {
const res = await fetch('stock.php', { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
itemModalObj.hide();
Swal.fire({
title: '<?= h(tr('تم الحفظ بنجاح', 'Successfully saved')) ?>',
icon: 'success',
showConfirmButton: false,
timer: 1500
}).then(() => location.reload());
} else {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', json.error || '<?= h(tr('فشل الحفظ', 'Failed to save')) ?>', 'error');
}
} catch(err) {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', '<?= h(tr('حدث خطأ في الاتصال', 'Network error')) ?>', 'error');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
function mockDelete() {
function deleteItem(sku) {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
text: '<?= h(tr('لن تتمكن من التراجع عن هذا!', "You won't be able to revert this!")) ?>',
@ -300,13 +477,27 @@ function mockDelete() {
cancelButtonColor: '#6c757d',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then((result) => {
}).then(async (result) => {
if (result.isConfirmed) {
Swal.fire(
'<?= h(tr('محذوف!', 'Deleted!')) ?>',
'<?= h(tr('حساب تجريبي لا يمكن حذفه.', 'Demo data cannot be deleted.')) ?>',
'success'
);
try {
const formData = new FormData();
formData.append('action', 'delete');
formData.append('sku', sku);
const res = await fetch('stock.php', { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
Swal.fire({
title: '<?= h(tr('محذوف!', 'Deleted!')) ?>',
icon: 'success',
showConfirmButton: false,
timer: 1500
}).then(() => location.reload());
} else {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', json.error || '<?= h(tr('فشل الحذف', 'Failed to delete')) ?>', 'error');
}
} catch(err) {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', '<?= h(tr('حدث خطأ في الاتصال', 'Network error')) ?>', 'error');
}
}
});
}

View File

@ -75,19 +75,19 @@ require __DIR__ . '/includes/header.php';
</section>
<section class="surface-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم', 'Name')) ?></th>
<th><?= h(tr('المسؤول', 'Contact Person')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('البريد', 'Email')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent">ID</th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم', 'Name')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المسؤول', 'Contact Person')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الهاتف', 'Phone')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('البريد', 'Email')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
@ -99,10 +99,10 @@ require __DIR__ . '/includes/header.php';
<td><?= h($item['phone']) ?></td>
<td><?= h($item['email']) ?></td>
<td>
<button class="btn btn-sm btn-light text-primary border" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>

View File

@ -54,20 +54,20 @@ require __DIR__ . '/includes/header.php';
</section>
<section class="surface-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
<tr>
<th><?= h(tr('المستخدم', 'User')) ?></th>
<th><?= h(tr('الدور', 'Role')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th>POS</th>
<th><?= h(tr('تقارير', 'Reports')) ?></th>
<th><?= h(tr('مستخدمون', 'Users')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المستخدم', 'User')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الدور', 'Role')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent">POS</th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('تقارير', 'Reports')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('مستخدمون', 'Users')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<tbody class="border-top-0">
<?php if(empty($accounts)): ?>
<tr><td colspan="7" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
@ -83,10 +83,10 @@ require __DIR__ . '/includes/header.php';
<td><span class="badge <?= in_array($account['role'], ['owner', 'manager'], true) ? 'text-bg-light border' : 'text-bg-secondary' ?>"><?= h(in_array($account['role'], ['owner', 'manager'], true) ? tr('نعم', 'Yes') : tr('لا', 'No')) ?></span></td>
<td><span class="badge <?= $account['role'] === 'owner' ? 'text-bg-light border' : 'text-bg-secondary' ?>"><?= h($account['role'] === 'owner' ? tr('نعم', 'Yes') : tr('لا', 'No')) ?></span></td>
<td>
<button class="btn btn-sm btn-light text-primary border" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="mockEdit()" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>