315 lines
14 KiB
PHP
315 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/includes/app.php';
|
|
$user = require_roles(['owner', 'manager', 'cashier']);
|
|
$pageTitle = tr('المخزون', 'Stock');
|
|
$activeNav = 'stock';
|
|
$dbError = null;
|
|
|
|
$allStock = [];
|
|
try {
|
|
$allStock = stock_snapshot();
|
|
} catch (Throwable $e) {
|
|
$dbError = $e->getMessage();
|
|
}
|
|
|
|
$categories = [];
|
|
$suppliers = [];
|
|
try {
|
|
$pdo = db();
|
|
$categories = $pdo->query('SELECT id, name_ar, name_en FROM categories ORDER BY name_ar ASC')->fetchAll();
|
|
$suppliers = $pdo->query('SELECT id, name FROM suppliers ORDER BY name ASC')->fetchAll();
|
|
} catch (Throwable $e) {
|
|
// Ignore if not present
|
|
}
|
|
|
|
// Search and filter logic
|
|
$search = $_GET['q'] ?? '';
|
|
$catFilter = $_GET['category'] ?? '';
|
|
$supFilter = $_GET['supplier'] ?? '';
|
|
$filteredStock = [];
|
|
|
|
if (empty($dbError)) {
|
|
$lowerSearch = strtolower($search);
|
|
foreach ($allStock as $key => $row) {
|
|
$matchSearch = !$search || str_contains(strtolower((string)$row['sku']), $lowerSearch) || str_contains(strtolower((string)$row['name']), $lowerSearch);
|
|
$matchCat = !$catFilter || (isset($row['category_id']) && $row['category_id'] == $catFilter);
|
|
$matchSup = !$supFilter || (isset($row['supplier_id']) && $row['supplier_id'] == $supFilter);
|
|
|
|
if ($matchSearch && $matchCat && $matchSup) {
|
|
$filteredStock[$key] = $row;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pagination logic
|
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
|
$limit = 10;
|
|
$total = count($filteredStock);
|
|
$totalPages = max(1, ceil($total / $limit));
|
|
$offset = ($page - 1) * $limit;
|
|
$stockRows = array_slice($filteredStock, $offset, $limit, true);
|
|
|
|
require __DIR__ . '/includes/header.php';
|
|
?>
|
|
|
|
<section class="surface-card mb-4">
|
|
<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>
|
|
</div>
|
|
<div class="col-lg-4 text-lg-end">
|
|
<button type="button" class="btn btn-primary" onclick="openItemModal()">
|
|
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة صنف', 'Add Item')) ?>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
<div class="col-md-3">
|
|
<select name="category" class="form-select">
|
|
<option value=""><?= h(tr('الكل (التصنيف)', 'All Categories')) ?></option>
|
|
<?php foreach($categories as $cat): ?>
|
|
<option value="<?= h($cat['id']) ?>" <?= ($catFilter == $cat['id']) ? 'selected' : '' ?>><?= h(current_lang() === 'ar' ? $cat['name_ar'] : $cat['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select name="supplier" class="form-select">
|
|
<option value=""><?= h(tr('الكل (المورد)', 'All Suppliers')) ?></option>
|
|
<?php foreach($suppliers as $sup): ?>
|
|
<option value="<?= h($sup['id']) ?>" <?= ($supFilter == $sup['id']) ? 'selected' : '' ?>><?= h($sup['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-primary w-100" type="submit">
|
|
<i class="bi bi-funnel"></i> <?= h(tr('تصفية', 'Filter')) ?>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="surface-card">
|
|
<?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">
|
|
<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>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if(empty($stockRows)): ?>
|
|
<tr><td colspan="8" class="text-center text-muted py-4"><?= 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>
|
|
<td>
|
|
<?php if ($row['available'] <= 12): ?>
|
|
<span class="badge text-bg-warning"><?= h(tr('منخفض', 'Low')) ?></span>
|
|
<?php else: ?>
|
|
<span class="badge text-bg-light border"><?= h(tr('مستقر', 'Stable')) ?></span>
|
|
<?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')) ?>">
|
|
<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')) ?>">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ($totalPages > 1): ?>
|
|
<nav class="mt-4">
|
|
<ul class="pagination justify-content-center mb-0">
|
|
<?php for($i=1; $i<=$totalPages; $i++): ?>
|
|
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
|
<a class="page-link" href="<?= h(url_for('stock.php', ['p' => $i, 'q' => $search, 'category' => $catFilter, 'supplier' => $supFilter])) ?>"><?= $i ?></a>
|
|
</li>
|
|
<?php endfor; ?>
|
|
</ul>
|
|
</nav>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<!-- Item Modal -->
|
|
<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)">
|
|
<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">
|
|
<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/*">
|
|
</div>
|
|
<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">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="suggestSKU()" title="<?= h(tr('اقتراح رمز', 'Suggest SKU')) ?>">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><?= h(tr('اسم الصنف', 'Product Name')) ?></label>
|
|
<input type="text" class="form-control" id="item_name" required>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-4">
|
|
<label class="form-label"><?= h(tr('السعر', 'Price')) ?></label>
|
|
<input type="number" step="0.001" class="form-control" id="item_price" required>
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label"><?= h(tr('الرصيد الافتتاحي', 'Opening Stock')) ?></label>
|
|
<input type="number" class="form-control" id="item_base_stock" required>
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label"><?= h(tr('الضريبة (VAT %)', 'VAT %')) ?></label>
|
|
<input type="number" step="0.001" class="form-control" id="item_vat" value="5" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?></label>
|
|
<select class="form-select" id="item_category" required>
|
|
<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>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label"><?= h(tr('المورد', 'Supplier')) ?></label>
|
|
<select class="form-select" id="item_supplier" required>
|
|
<option value=""><?= h(tr('-- اختر المورد --', '-- Select Supplier --')) ?></option>
|
|
<?php foreach($suppliers as $sup): ?>
|
|
<option value="<?= h($sup['id']) ?>"><?= h($sup['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</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>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const existingSkus = <?= json_encode(array_values(array_unique(array_map('strval', array_column($allStock, 'sku'))))) ?>;
|
|
|
|
function suggestSKU() {
|
|
let newSku;
|
|
do {
|
|
newSku = Math.floor(10000000 + Math.random() * 90000000).toString().substring(0, 8);
|
|
} while (existingSkus.includes(newSku));
|
|
document.getElementById('item_sku').value = newSku;
|
|
}
|
|
|
|
let itemModalObj = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
});
|
|
|
|
itemModalObj = new bootstrap.Modal(document.getElementById('itemModal'));
|
|
});
|
|
|
|
function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '5', category_id = '', supplier_id = '', image_url = '') {
|
|
document.getElementById('item_sku').value = sku;
|
|
document.getElementById('item_name').value = name;
|
|
document.getElementById('item_price').value = price;
|
|
document.getElementById('item_base_stock').value = base_stock;
|
|
document.getElementById('item_vat').value = vat;
|
|
document.getElementById('item_category').value = category_id;
|
|
document.getElementById('item_supplier').value = supplier_id;
|
|
|
|
// Remove old image preview if any
|
|
const oldPreview = document.getElementById('image_preview');
|
|
if (oldPreview) oldPreview.remove();
|
|
|
|
if (image_url) {
|
|
const preview = document.createElement('img');
|
|
preview.id = 'image_preview';
|
|
preview.src = image_url;
|
|
preview.style.maxHeight = '100px';
|
|
preview.className = 'mt-2 rounded';
|
|
document.getElementById('item_picture').parentElement.appendChild(preview);
|
|
}
|
|
document.getElementById('item_picture').value = '';
|
|
|
|
|
|
itemModalObj.show();
|
|
}
|
|
|
|
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')) ?>'
|
|
});
|
|
}
|
|
|
|
function mockDelete() {
|
|
Swal.fire({
|
|
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
|
|
text: '<?= h(tr('لن تتمكن من التراجع عن هذا!', "You won't be able to revert this!")) ?>',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#dc3545',
|
|
cancelButtonColor: '#6c757d',
|
|
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
|
|
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
Swal.fire(
|
|
'<?= h(tr('محذوف!', 'Deleted!')) ?>',
|
|
'<?= h(tr('حساب تجريبي لا يمكن حذفه.', 'Demo data cannot be deleted.')) ?>',
|
|
'success'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|