705 lines
35 KiB
PHP
705 lines
35 KiB
PHP
<?php
|
|
require_once __DIR__ . '/includes/app.php';
|
|
$user = require_permission('stock', 'show');
|
|
$pageTitle = tr('المخزون', 'Stock');
|
|
$activeNav = 'stock';
|
|
$dbError = null;
|
|
|
|
// Handle Export CSV
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'export_csv') {
|
|
$pdo = db();
|
|
$stmt = $pdo->query("SELECT sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id FROM items ORDER BY id DESC");
|
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename=stock_export_' . date('Ymd_His') . '.csv');
|
|
echo "";
|
|
$output = fopen('php://output', 'w');
|
|
fputcsv($output, ['SKU', 'Name', 'Price', 'Cost Price', 'Stock', 'VAT', 'Category ID', 'Supplier ID', 'Unit ID']);
|
|
foreach ($items as $row) { fputcsv($output, $row); }
|
|
fclose($output);
|
|
exit;
|
|
}
|
|
|
|
// Handle Import CSV
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'import_csv') {
|
|
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) {
|
|
$pdo = db();
|
|
$file = fopen($_FILES['csv_file']['tmp_name'], 'r');
|
|
$bom = fread($file, 3);
|
|
if ($bom !== "") rewind($file);
|
|
$header = fgetcsv($file);
|
|
$imported = 0; $updated = 0;
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$stmtInsert = $pdo->prepare("INSERT INTO items (sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmtUpdate = $pdo->prepare("UPDATE items SET name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=? WHERE sku=?");
|
|
$stmtCheck = $pdo->prepare("SELECT id FROM items WHERE sku=?");
|
|
while (($row = fgetcsv($file)) !== false) {
|
|
if (count($row) < 5) continue;
|
|
$sku = trim($row[0]); $name = trim($row[1]);
|
|
if ($sku === '' || $name === '') continue;
|
|
$price = (float)($row[2] ?? 0);
|
|
$cost_price = (float)($row[3] ?? 0);
|
|
$base_stock = (int)($row[4] ?? 0);
|
|
$vat = (float)($row[5] ?? 5);
|
|
$category_id = !empty($row[6]) ? (int)$row[6] : null;
|
|
$supplier_id = !empty($row[7]) ? (int)$row[7] : null;
|
|
$unit_id = !empty($row[8]) ? (int)$row[8] : null;
|
|
$stmtCheck->execute([$sku]);
|
|
if ($stmtCheck->fetchColumn()) {
|
|
$stmtUpdate->execute([$name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $sku]);
|
|
$updated++;
|
|
} else {
|
|
$stmtInsert->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id]);
|
|
$imported++;
|
|
}
|
|
}
|
|
$pdo->commit();
|
|
header('Location: stock.php?import_success=1&imported='.$imported.'&updated='.$updated);
|
|
exit;
|
|
} catch (Exception $e) {
|
|
$pdo->rollBack();
|
|
header('Location: stock.php?import_error='.urlencode($e->getMessage()));
|
|
exit;
|
|
}
|
|
}
|
|
header('Location: stock.php?import_error=No+file');
|
|
exit;
|
|
}
|
|
|
|
// 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);
|
|
$cost_price = (float)($_POST['cost_price'] ?? 0);
|
|
$base_stock = (int)($_POST['base_stock'] ?? 0);
|
|
$vat = (float)($_POST['vat'] ?? get_setting('vat_percentage', 5));
|
|
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
|
|
$supplier_id = !empty($_POST['supplier_id']) ? (int)$_POST['supplier_id'] : null;
|
|
$unit_id = !empty($_POST['unit_id']) ? (int)$_POST['unit_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=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=? " . ($image_url ? ", image_url=?" : "") . " WHERE sku=?";
|
|
$params = [$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_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, cost_price, base_stock, vat, category_id, supplier_id, unit_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_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();
|
|
} 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();
|
|
$units = $pdo->query('SELECT id, name_ar, name_en FROM units ORDER BY name_ar 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-5">
|
|
<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('إدارة الأصناف وجرد المخزون.', 'Manage items and inventory.')) ?></p>
|
|
</div>
|
|
<div class="col-lg-7 text-lg-end">
|
|
<a href="stock.php?action=export_csv" class="btn btn-success shadow-sm me-1 mb-1" title="<?= h(tr('تصدير إكسل/CSV', 'Export Excel/CSV')) ?>">
|
|
<i class="bi bi-file-earmark-excel"></i> <span class="d-none d-md-inline"><?= h(tr('تصدير', 'Export')) ?></span>
|
|
</a>
|
|
<button type="button" class="btn btn-info text-white shadow-sm me-1 mb-1" onclick="openImportModal()" title="<?= h(tr('استيراد إكسل/CSV', 'Import Excel/CSV')) ?>">
|
|
<i class="bi bi-file-earmark-arrow-up"></i> <span class="d-none d-md-inline"><?= h(tr('استيراد', 'Import')) ?></span>
|
|
</button>
|
|
<button type="button" class="btn btn-secondary shadow-sm me-1 mb-1" onclick="printBulkLabels()">
|
|
<i class="bi bi-upc-scan"></i> <span class="d-none d-md-inline"><?= h(tr('ملصقات', 'Labels')) ?></span>
|
|
</button>
|
|
<button type="button" class="btn btn-primary shadow-sm mb-1" onclick="openItemModal()">
|
|
<i class="bi bi-plus-lg"></i> <span class="d-none d-md-inline"><?= h(tr('إضافة', 'Add')) ?></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<?php if (isset($_GET['import_success'])):
|
|
$import_success_message = h(tr('تم الاستيراد بنجاح! أصناف جديدة: ', 'Import successful! New items: ')) . (int)$_GET['imported'] . h(tr('، محدثة: ', ', updated: ')) . (int)$_GET['updated'];
|
|
echo "<div class=\"alert alert-success alert-dismissible fade show mb-0\">" . $import_success_message . "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button></div>";
|
|
endif; ?>
|
|
<?php if (isset($_GET['import_error'])):
|
|
$import_error_message = h(tr('خطأ في الاستيراد: ', 'Import error: ')) . h($_GET['import_error']);
|
|
echo "<div class=\"alert alert-danger alert-dismissible fade show mb-0\">" . $import_error_message . "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button></div>";
|
|
endif; ?>
|
|
</div>
|
|
|
|
<form class="mb-4" method="GET" action="stock.php">
|
|
<div class="row g-2">
|
|
<div class="col-md-4">
|
|
<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">
|
|
<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 shadow-sm" 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 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 width="40" class="text-white border-0 py-3 fw-semibold bg-transparent">
|
|
<input class="form-check-input" type="checkbox" id="selectAllCheckbox" onclick="toggleAllCheckboxes(this)">
|
|
</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('التكلفة', 'Cost')) ?></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 class="border-top-0">
|
|
<?php if(empty($stockRows)): ?>
|
|
<tr><td colspan="11" 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 style="transition: all 0.2s ease;">
|
|
<td>
|
|
<input class="form-check-input item-checkbox" type="checkbox" value="<?= h($row['sku']) ?>">
|
|
</td>
|
|
<td class="text-secondary"><?= h($row['sku']) ?></td>
|
|
<td>
|
|
<?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: ?>
|
|
<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="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><span class="badge bg-light text-secondary border px-2 py-1"><?= h(currency($row['cost_price'] ?? 0)) ?></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['cost_price'] ?? 0) ?>', '<?= h($row['base_stock']) ?>', '<?= h($row['vat'] ?? get_setting('vat_percentage', 5)) ?>', '<?= h($row['category_id'] ?? '') ?>', '<?= h($row['supplier_id'] ?? '') ?>', '<?= h($row['unit_id'] ?? '') ?>', '<?= h($row['image_url'] ?? '') ?>')" data-bs-toggle="tooltip" title="<?= h(tr('تعديل', 'Edit')) ?>">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button type="button" onclick="printSingleLabel('<?= h(addslashes($row['sku'])) ?>')" class="btn btn-sm btn-outline-secondary rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" data-bs-toggle="tooltip" title="<?= h(tr('طباعة ملصق', 'Print Label')) ?>">
|
|
<i class="bi bi-printer"></i>
|
|
</button>
|
|
<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>
|
|
</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>
|
|
|
|
<!-- Import Modal -->
|
|
<div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form action="stock.php" method="POST" enctype="multipart/form-data">
|
|
<input type="hidden" name="action" value="import_csv">
|
|
<div class="modal-header bg-info text-white">
|
|
<h5 class="modal-title" id="importModalLabel"><i class="bi bi-file-earmark-arrow-up me-2"></i><?= h(tr('استيراد من إكسل / CSV', 'Import from Excel / CSV')) ?></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="alert alert-warning small">
|
|
<?= h(tr('ملاحظة: يدعم النظام ملفات CSV فقط. يرجى حفظ ملف Excel بصيغة (CSV UTF-8).', 'Note: The system supports CSV files only. Please save your Excel file as (CSV UTF-8).')) ?>
|
|
</div>
|
|
<p class="text-muted small mb-2">
|
|
<?= h(tr('يجب أن يحتوي الملف على الأعمدة التالية بالترتيب الدقيق:', 'The file must contain exactly these columns in order:')) ?><br>
|
|
<strong>SKU, Name, Price, Cost Price, Stock, VAT, Category ID, Supplier ID, Unit ID</strong>
|
|
</p>
|
|
<div class="mb-3">
|
|
<label class="form-label"><?= h(tr('ملف الإكسل / CSV', 'Excel / CSV File')) ?></label>
|
|
<input type="file" class="form-control" name="csv_file" accept=".csv" required>
|
|
</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-info text-white"><?= h(tr('استيراد', 'Import')) ?></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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)" 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="row">
|
|
<div class="col-12 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="col-md-6 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="15">
|
|
<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="col-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('اسم الصنف', 'Product Name')) ?></label>
|
|
<input type="text" class="form-control" id="item_name" required>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<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-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('التكلفة', 'Cost Price')) ?></label>
|
|
<input type="number" step="0.001" class="form-control" id="item_cost_price" required>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('الرصيد الافتتاحي', 'Opening Stock')) ?></label>
|
|
<input type="number" class="form-control" id="item_base_stock" required>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('الضريبة (VAT %)', 'VAT %')) ?></label>
|
|
<input type="number" step="0.001" class="form-control" id="item_vat" value="<?= h(get_setting('vat_percentage', 5)) ?>" required>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?></label>
|
|
<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>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label"><?= h(tr('المورد', 'Supplier')) ?></label>
|
|
<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>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-12 mb-3">
|
|
<label class="form-label"><?= h(tr('الوحدة', 'Unit')) ?></label>
|
|
<select class="form-select" id="item_unit">
|
|
<option value=""><?= h(tr('-- اختر الوحدة --', '-- Select Unit --')) ?></option>
|
|
<?php foreach($units as $unit): ?>
|
|
<option value="<?= h($unit['id']) ?>"><?= h(current_lang() === 'ar' ? $unit['name_ar'] : $unit['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</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" id="saveItemBtn">
|
|
<i class="bi bi-save me-1"></i> <?= 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'));
|
|
|
|
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 openImportModal() {
|
|
var m = new bootstrap.Modal(document.getElementById('importModal'));
|
|
m.show();
|
|
}
|
|
|
|
function openItemModal(sku = '', name = '', price = '', cost_price = '', base_stock = '', vat = '<?= h(get_setting('vat_percentage', 5)) ?>', category_id = '', supplier_id = '', unit_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;
|
|
document.getElementById('item_cost_price').value = cost_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;
|
|
document.getElementById('item_unit').value = unit_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 shadow-sm border';
|
|
document.getElementById('item_picture').parentElement.appendChild(preview);
|
|
}
|
|
document.getElementById('item_picture').value = '';
|
|
|
|
itemModalObj.show();
|
|
}
|
|
|
|
async function handleItemSubmit(e) {
|
|
e.preventDefault();
|
|
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('cost_price', document.getElementById('item_cost_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('unit_id', document.getElementById('item_unit').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 deleteItem(sku) {
|
|
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(async (result) => {
|
|
if (result.isConfirmed) {
|
|
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');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleAllCheckboxes(source) {
|
|
const checkboxes = document.querySelectorAll(".item-checkbox");
|
|
for (let i = 0; i < checkboxes.length; i++) {
|
|
checkboxes[i].checked = source.checked;
|
|
}
|
|
}
|
|
|
|
function printBulkLabels() {
|
|
const checkboxes = document.querySelectorAll(".item-checkbox:checked");
|
|
if (checkboxes.length === 0) {
|
|
Swal.fire('<?= h(tr("تنبيه", "Warning")) ?>', '<?= h(tr("يرجى تحديد صنف واحد على الأقل", "Please select at least one item")) ?>', 'warning');
|
|
return;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = 'print_labels.php';
|
|
|
|
checkboxes.forEach((cb) => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'skus[]';
|
|
input.value = cb.value;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
function printSingleLabel(sku) {
|
|
const checked = document.querySelectorAll(".item-checkbox:checked");
|
|
if (checked.length > 1) {
|
|
printBulkLabels();
|
|
return;
|
|
}
|
|
window.location.href = 'print_single_label.php?sku=' + encodeURIComponent(sku);
|
|
}
|
|
</script>
|
|
|
|
<?php require __DIR__ . "/includes/footer.php"; ?>
|