Autosave: 20260419-091830

This commit is contained in:
Flatlogic Bot 2026-04-19 09:18:23 +00:00
parent 4f9eddc419
commit 9b7ec271a9
10 changed files with 944 additions and 118 deletions

26
api/suppliers.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../includes/app.php';
$user = require_auth();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$name = trim($_POST['name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
if (!$name) {
echo json_encode(['success' => false, 'error' => tr('الاسم مطلوب', 'Name is required')]);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare('INSERT INTO suppliers (name, phone) VALUES (?, ?)');
$stmt->execute([$name, $phone]);
$id = $pdo->lastInsertId();
echo json_encode(['success' => true, 'supplier' => ['id' => $id, 'name' => $name, 'phone' => $phone]]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

View File

@ -230,6 +230,7 @@ function catalog(): array
"name_ar" => $item["name"],
"name_en" => $item["name"],
"price" => (float)$item["price"],
"cost_price" => (float)($item["cost_price"] ?? 0),
"base_stock" => (int)$item["base_stock"],
"vat" => (float)$item["vat"],
"category_id" => $item["category_id"],
@ -488,6 +489,7 @@ function stock_snapshot(): array
'sold' => $used,
'available' => max(0, $base - $used),
'price' => $item['price'],
'cost_price' => $item['cost_price'] ?? 0,
'category_id' => $item['category_id'],
'supplier_id' => $item['supplier_id'],
'image_url' => $item['image_url'],
@ -525,3 +527,47 @@ function receipt_code(): string
{
return 'AR-' . date('ymd-His') . '-' . random_int(100, 999);
}
function create_purchase(array $data): int
{
db()->beginTransaction();
try {
$stmt = db()->prepare("INSERT INTO purchase_orders
(reference_no, branch_code, user_username, user_name, role_name, supplier_name, items_json, item_count, subtotal, total_amount, status, notes, purchase_date)
VALUES
(:reference_no, :branch_code, :user_username, :user_name, :role_name, :supplier_name, :items_json, :item_count, :subtotal, :total_amount, :status, :notes, NOW())");
$stmt->bindValue(":reference_no", $data["reference_no"]);
$stmt->bindValue(":branch_code", $data["branch_code"]);
$stmt->bindValue(":user_username", $data["user_username"]);
$stmt->bindValue(":user_name", $data["user_name"]);
$stmt->bindValue(":role_name", $data["role_name"]);
$stmt->bindValue(":supplier_name", $data["supplier_name"]);
$stmt->bindValue(":items_json", json_encode($data["items"], JSON_UNESCAPED_UNICODE));
$stmt->bindValue(":item_count", $data["item_count"], PDO::PARAM_INT);
$stmt->bindValue(":subtotal", $data["subtotal"]);
$stmt->bindValue(":total_amount", $data["total_amount"]);
$stmt->bindValue(":status", $data["status"] ?? "completed");
$stmt->bindValue(":notes", $data["notes"]);
$stmt->execute();
$purchaseId = (int) db()->lastInsertId();
// Update stock
foreach ($data["items"] as $item) {
$qty = (int) $item["qty"];
$sku = $item["sku"];
$updateStmt = db()->prepare("UPDATE items SET base_stock = base_stock + :qty, cost_price = :price WHERE sku = :sku");
$updateStmt->bindValue(":qty", $qty, PDO::PARAM_INT);
$updateStmt->bindValue(":price", $item["price"]);
$updateStmt->bindValue(":sku", $sku);
$updateStmt->execute();
}
db()->commit();
return $purchaseId;
} catch (Throwable $e) {
db()->rollBack();
throw $e;
}
}

View File

@ -70,16 +70,8 @@ $isPublic = !isset($user) || !$user;
<a class="list-group-item list-group-item-action <?= $activeNav === 'dashboard' ? 'active' : '' ?>" href="<?= h(url_for('index.php')) ?>">
<i class="bi bi-speedometer2"></i> <?= h(tr('لوحة التحكم', 'Dashboard')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>">
<i class="bi bi-cart-check"></i> <?= h(tr('نقاط البيع', 'POS Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>">
<i class="bi bi-receipt"></i> <?= h(tr('بيع عادي', 'Normal Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('المبيعات', 'Sales')) ?>
</a>
<!-- المخزون (Inventory) - Now First -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['stock', 'categories', 'units']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseStock" role="button" aria-expanded="<?= in_array($activeNav, ['stock', 'categories', 'units']) ? 'true' : 'false' ?>" aria-controls="collapseStock">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-box-seam"></i> <?= h(tr('المخزون', 'Inventory')) ?></span>
@ -99,10 +91,44 @@ $isPublic = !isset($user) || !$user;
</a>
</div>
</div>
<a class="list-group-item list-group-item-action <?= $activeNav === 'purchases' ? 'active' : '' ?>" href="<?= h(url_for('purchases.php')) ?>">
<i class="bi bi-bag-plus"></i> <?= h(tr('المشتريات', 'Purchases')) ?>
<!-- المبيعات (Sales) - Now Collapsible -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['sales', 'normal', 'pos']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseSales" role="button" aria-expanded="<?= in_array($activeNav, ['sales', 'normal', 'pos']) ? 'true' : 'false' ?>" aria-controls="collapseSales">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-cart"></i> <?= h(tr('المبيعات', 'Sales')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['sales', 'normal', 'pos']) ? 'show' : '' ?>" id="collapseSales">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة الفواتير', 'Invoice list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('فاتورة جديدة', 'New invoice')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('نقاط البيع', 'POS')) ?>
</a>
</div>
</div>
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['purchases', 'new_purchase']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapsePurchases" role="button" aria-expanded="<?= in_array($activeNav, ['purchases', 'new_purchase']) ? 'true' : 'false' ?>" aria-controls="collapsePurchases">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-bag-plus"></i> <?= h(tr('المشتريات', 'Purchases')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['purchases', 'new_purchase']) ? 'show' : '' ?>" id="collapsePurchases">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'purchases' ? 'active' : '' ?>" href="<?= h(url_for('purchases.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة المشتريات', 'Purchase list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'new_purchase' ? 'active' : '' ?>" href="<?= h(url_for('new_purchase.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('فاتورة مشتريات جديدة', 'New purchase')) ?>
</a>
</div>
</div>
<a class="list-group-item list-group-item-action <?= $activeNav === 'suppliers' ? 'active' : '' ?>" href="<?= h(url_for('suppliers.php')) ?>">
<i class="bi bi-truck"></i> <?= h(tr('الموردون', 'Suppliers')) ?>
</a>

664
includes/purchase_form.php Normal file
View File

@ -0,0 +1,664 @@
<?php
require_once __DIR__ . '/app.php';
$user = require_roles(['owner', 'manager', 'cashier']);
$pageTitle = tr('فاتورة مشتريات جديدة', 'New Purchase');
$activeNav = 'new_purchase';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
try {
$customers = $customers = [];
$suppliers = db()->query('SELECT id, name, phone FROM suppliers ORDER BY name ASC')->fetchAll();
} catch (Throwable $e) {
$customers = [];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$branchCode = trim((string) ($_POST['branch_code'] ?? ''));
$supplierName = trim((string) ($_POST['supplier_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$purchaseStatus = trim((string) ($_POST['purchase_status'] ?? 'completed'));
$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 invoice.');
} 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 = isset($item['price']) ? (float)$item['price'] : (float)($product['cost_price'] ?? $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 invoice is invalid after product validation.');
} else {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$purchaseId = create_purchase([
'reference_no' => receipt_code(),
'branch_code' => $branchCode,
'user_username' => $user['username'],
'user_name' => $cashierName,
'role_name' => $user['role'],
'supplier_name' => $supplierName !== '' ? $supplierName : null,
'items' => $normalized,
'item_count' => $itemCount,
'subtotal' => $subtotal,
'total_amount' => $subtotal,
'status' => $purchaseStatus,
'notes' => $notes !== '' ? $notes : null,
]);
set_flash('success', tr('تم حفظ فاتورة المشتريات بنجاح. وتم تحديث المخزون.', 'Purchase invoice saved successfully and stock updated.'));
redirect_to('purchases.php');
}
}
}
require __DIR__ . '/header.php';
?>
<style>
.smart-form-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
border: 1px solid #edf2f9;
margin-bottom: 2rem;
}
.smart-form-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid #edf2f9;
background-color: #fcfdfd;
border-radius: 12px 12px 0 0;
}
.smart-form-body {
padding: 2rem;
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-label {
font-weight: 500;
color: #495057;
margin-bottom: 0.4rem;
}
.custom-input {
border: 1px solid #ced4da;
border-radius: 8px;
padding: 0.6rem 1rem;
font-size: 0.95rem;
transition: all 0.2s ease-in-out;
}
.custom-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25);
}
.search-wrapper {
position: relative;
max-width: 600px;
margin-bottom: 2rem;
}
.search-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
color: #6c757d;
}
[dir="rtl"] .search-icon {
left: auto;
right: 1rem;
}
.search-input {
padding-left: 2.5rem;
}
[dir="rtl"] .search-input {
padding-left: 1rem;
padding-right: 2.5rem;
}
.item-search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
z-index: 1000;
max-height: 300px;
overflow-y: auto;
display: none;
border: 1px solid #edf2f9;
margin-top: 0.5rem;
}
.item-search-dropdown.show { display: block; }
.search-item-row {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #edf2f9;
transition: background 0.15s;
}
.search-item-row:hover { background: #f8f9fa; }
.search-item-row:last-child { border-bottom: none; }
.table-modern {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border: 1px solid #edf2f9;
border-radius: 8px;
overflow: hidden;
}
.table-modern th {
background: #f8f9fa;
padding: 1rem;
font-weight: 600;
color: #495057;
border-bottom: 1px solid #edf2f9;
font-size: 0.9rem;
}
.table-modern td {
padding: 1rem;
vertical-align: middle;
border-bottom: 1px solid #edf2f9;
}
.table-modern tr:last-child td {
border-bottom: none;
}
.qty-control {
width: 80px;
text-align: center;
border: 1px solid #ced4da;
border-radius: 6px;
padding: 0.4rem;
}
.btn-remove {
color: #dc3545;
background: rgba(220, 53, 69, 0.1);
border: none;
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn-remove:hover {
background: #dc3545;
color: #fff;
}
.totals-box {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
border: 1px solid #edf2f9;
}
.totals-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
color: #495057;
}
.totals-row.grand-total {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
font-size: 1.25rem;
font-weight: 700;
color: #212529;
margin-bottom: 0;
}
</style>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="fw-bold mb-0 text-dark"><?= h($pageTitle) ?></h3>
<a href="sales.php" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> <?= h(tr('عودة للمبيعات', 'Back to Sales')) ?>
</a>
</div>
<?php if ($error !== ''): ?>
<div class="alert alert-danger rounded-3 shadow-sm mb-4"><i class="bi bi-exclamation-triangle-fill me-2"></i><?= h($error) ?></div>
<?php endif; ?>
<form method="post" id="smart-sale-form">
<input type="hidden" name="cart_json" id="cart_json" value="[]">
<div class="row">
<div class="col-lg-8">
<!-- Items Section -->
<div class="smart-form-card">
<div class="smart-form-header">
<div class="section-title mb-0">
<i class="bi bi-cart-plus text-primary"></i> <?= h(tr('عناصر الفاتورة', 'Invoice Items')) ?>
</div>
</div>
<div class="smart-form-body">
<!-- Search Bar -->
<div class="search-wrapper">
<i class="bi bi-search search-icon"></i>
<input type="text" id="itemSearchInput" class="form-control custom-input search-input form-control-lg" placeholder="<?= h(tr('ابحث بالاسم أو الباركود...', 'Search by name or barcode...')) ?>" autocomplete="off">
<div id="itemDropdown" class="item-search-dropdown"></div>
</div>
<!-- Table -->
<div class="table-responsive">
<table class="table-modern" id="invoiceTable">
<thead>
<tr>
<th width="45%"><?= h(tr('المنتج', 'Product')) ?></th>
<th width="15%" class="text-center"><?= h(tr('السعر', 'Price')) ?></th>
<th width="15%" class="text-center"><?= h(tr('الكمية', 'Qty')) ?></th>
<th width="20%" class="text-center"><?= h(tr('الإجمالي', 'Total')) ?></th>
<th width="5%"></th>
</tr>
</thead>
<tbody id="invoiceLines">
<tr id="emptyInvoiceRow">
<td colspan="5" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2 text-light"></i>
<?= h(tr('لم يتم إضافة أي منتجات بعد.', 'No products added yet.')) ?>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Settings Section -->
<div class="smart-form-card">
<div class="smart-form-header">
<div class="section-title mb-0">
<i class="bi bi-receipt text-primary"></i> <?= h(tr('تفاصيل الفاتورة', 'Invoice Details')) ?>
</div>
</div>
<div class="smart-form-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select class="form-select custom-input" name="branch_code" <?= count($allowedBranches) === 1 ? 'readonly' : '' ?>>
<?php foreach ($allowedBranches as $branchCode): ?>
<option value="<?= h($branchCode) ?>"><?= h(branch_label($branchCode)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3 position-relative">
<label class="form-label"><?= h(tr('المورد', 'Supplier')) ?></label>
<div class="input-group">
<input type="text" id="formSupplier" name="supplier_name" class="form-control custom-input" style="border-right-width: 1px;" placeholder="<?= h(tr('بحث (اسم أو هاتف)', 'Search (Name or Phone)')) ?>" autocomplete="off">
<button class="btn btn-outline-primary px-3" style="border-radius: 0 8px 8px 0;" type="button" onclick="openNewSupplierModal()" title="<?= h(tr('إضافة مورد', 'Add Supplier')) ?>">
<i class="bi bi-person-plus-fill"></i>
</button>
</div>
<div id="formSupplierDropdown" class="item-search-dropdown w-100" style="top: 100%;"></div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('نوع العملية', 'Entry Type')) ?></label>
<select class="form-select custom-input" name="purchase_status">
<option value="completed"><?= h(tr('مكتمل (تم الاستلام)', 'Completed (Received)')) ?></option>
<option value="order"><?= h(tr('طلب شراء (قيد الانتظار)', 'Purchase Order (Pending)')) ?></option>
</select>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></label>
<select class="form-select custom-input" name="payment_method">
<option value="cash"><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card"><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer"><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
</select>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
<textarea class="form-control custom-input" name="notes" rows="2" placeholder="<?= h(tr('أي ملاحظات إضافية...', 'Any additional notes...')) ?>"></textarea>
</div>
<!-- Summary -->
<div class="totals-box mb-4">
<div class="totals-row">
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
<span id="displaySubtotal" class="fw-medium">0.000</span>
</div>
<div class="totals-row">
<span><?= h(tr('الضريبة (' . get_setting('vat_percentage', 5) . '%)', 'VAT (' . get_setting('vat_percentage', 5) . '%)')) ?></span>
<span class="text-success small"><?= h(tr('مشمولة', 'Included')) ?></span>
</div>
<div class="totals-row grand-total">
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
<span id="displayTotal" class="text-primary">0.000 <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 fs-5 rounded-3 shadow-sm">
<i class="bi bi-check-circle me-1"></i> <?= h(tr('حفظ الفاتورة', 'Save Invoice')) ?>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- New Customer Modal -->
<div class="modal fade" id="newSupplierModal" 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('إضافة مورد', 'Add Supplier')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('الاسم', 'Name')) ?> <span class="text-danger">*</span></label>
<input type="text" id="ncName" class="form-control rounded-3">
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
</div>
<div class="d-grid mt-4">
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
let invoiceItems = {};
const searchInput = document.getElementById('itemSearchInput');
const dropdown = document.getElementById('itemDropdown');
const tbody = document.getElementById('invoiceLines');
const emptyRow = document.getElementById('emptyInvoiceRow');
const cartJson = document.getElementById('cart_json');
const currencySuffix = ' <?= h(tr('ر.ع', 'OMR')) ?>';
// Customers Logic
let suppliersData = <?= json_encode($suppliers, JSON_UNESCAPED_UNICODE) ?>;
const custInput = document.getElementById('formSupplier');
const custDropdown = document.getElementById('formSupplierDropdown');
custInput.addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
custDropdown.innerHTML = '';
if (q.length < 2) {
custDropdown.classList.remove('show');
return;
}
const matches = suppliersData.filter(c =>
c.name.toLowerCase().includes(q) ||
(c.phone && c.phone.toLowerCase().includes(q))
).slice(0, 5);
if (matches.length > 0) {
matches.forEach(c => {
const div = document.createElement('div');
div.className = 'search-item-row';
div.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
div.onclick = function() {
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
custDropdown.classList.remove('show');
};
custDropdown.appendChild(div);
});
custDropdown.classList.add('show');
} else {
custDropdown.classList.remove('show');
}
});
document.addEventListener('click', function(e) {
if (!custInput.contains(e.target) && !custDropdown.contains(e.target)) {
custDropdown.classList.remove('show');
}
});
let newSupplierModalObj = null;
function openNewSupplierModal() {
if (!newSupplierModalObj) {
newSupplierModalObj = new bootstrap.Modal(document.getElementById('newSupplierModal'));
}
document.getElementById('ncName').value = '';
document.getElementById('ncPhone').value = '';
newSupplierModalObj.show();
}
async function saveNewCustomer() {
const name = document.getElementById('ncName').value.trim();
const phone = document.getElementById('ncPhone').value.trim();
if (!name) {
alert('<?= h(tr('الاسم مطلوب', 'Name is required')) ?>');
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('phone', phone);
try {
const res = await fetch('api/suppliers.php', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
suppliersData.push(data.supplier);
custInput.value = data.supplier.name + (data.supplier.phone ? ' - ' + data.supplier.phone : '');
newSupplierModalObj.hide();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
} else {
alert(data.error);
}
} catch(err) {
alert('Error saving customer');
}
}
// Search logic
searchInput.addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
dropdown.innerHTML = '';
if (q === '') {
dropdown.classList.remove('show');
return;
}
const matches = catalogArray.filter(item => {
const nameAr = (item.name_ar || '').toLowerCase();
const nameEn = (item.name_en || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return nameAr.includes(q) || nameEn.includes(q) || sku.includes(q);
}).slice(0, 6);
if (matches.length > 0) {
matches.forEach(item => {
const div = document.createElement('div');
div.className = 'search-item-row d-flex justify-content-between align-items-center';
const name = '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en;
div.innerHTML = `
<div>
<div class="fw-medium text-dark">${name}</div>
<div class="text-muted small">SKU: ${item.sku}</div>
</div>
<div class="fw-semibold text-primary">${parseFloat(item.cost_price || item.price).toFixed(3)}</div>
`;
div.onclick = () => {
addItemToInvoice(item.sku);
searchInput.value = '';
dropdown.classList.remove('show');
searchInput.focus();
};
dropdown.appendChild(div);
});
dropdown.classList.add('show');
} else {
dropdown.classList.remove('show');
}
});
// Barcode scanner integration on enter
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const q = this.value.trim();
if(q === '') return;
const match = catalogArray.find(item => item.sku === q);
if (match) {
addItemToInvoice(match.sku);
searchInput.value = '';
dropdown.classList.remove('show');
}
}
});
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
function addItemToInvoice(sku) {
if (invoiceItems[sku]) {
invoiceItems[sku].qty += 1;
} else {
const item = catalogData[sku];
invoiceItems[sku] = {
sku: sku,
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
price: parseFloat(item.cost_price || item.price || 0),
qty: 1
};
}
renderInvoice();
}
function changePrice(sku, newPrice) {
const price = parseFloat(newPrice);
if (!isNaN(price) && price >= 0) {
invoiceItems[sku].price = price;
}
renderInvoice();
}
function changeQty(sku, newQty) {
const qty = parseInt(newQty);
if (isNaN(qty) || qty < 1) {
delete invoiceItems[sku];
} else {
invoiceItems[sku].qty = qty;
}
renderInvoice();
}
function removeItem(sku) {
delete invoiceItems[sku];
renderInvoice();
}
function renderInvoice() {
const skus = Object.keys(invoiceItems);
if (skus.length === 0) {
tbody.innerHTML = '';
tbody.appendChild(emptyRow);
updateTotals(0);
cartJson.value = '[]';
return;
}
tbody.innerHTML = '';
let totalAmount = 0;
const cartData = [];
skus.forEach(sku => {
const item = invoiceItems[sku];
const lineTotal = item.qty * item.price;
totalAmount += lineTotal;
cartData.push({ sku: item.sku, qty: item.qty, price: item.price });
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<div class="fw-medium text-dark">${item.name}</div>
<div class="text-muted small">SKU: ${item.sku}</div>
</td>
<td class="text-center align-middle">
<input type="number" step="0.001" class="qty-control mx-auto fw-medium" min="0" value="${item.price.toFixed(3)}" onchange="changePrice('${sku}', this.value)" onkeyup="if(event.key==='Enter') changePrice('${sku}', this.value)">
</td>
<td class="text-center align-middle">
<input type="number" class="qty-control mx-auto fw-medium" min="1" value="${item.qty}" onchange="changeQty('${sku}', this.value)" onkeyup="if(event.key==='Enter') changeQty('${sku}', this.value)">
</td>
<td class="text-center fw-semibold text-dark align-middle">${lineTotal.toFixed(3)}</td>
<td class="text-center align-middle">
<button type="button" class="btn-remove mx-auto" onclick="removeItem('${sku}')" title="<?= h(tr('إزالة', 'Remove')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
updateTotals(totalAmount);
cartJson.value = JSON.stringify(cartData);
}
function updateTotals(total) {
document.getElementById('displaySubtotal').innerText = total.toFixed(3);
document.getElementById('displayTotal').innerText = total.toFixed(3) + currencySuffix;
}
// Intercept form submission to check if items exist
document.getElementById('smart-sale-form').addEventListener('submit', function(e) {
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
alert('<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>');
}
});
</script>
<?php require __DIR__ . '/footer.php'; ?>

View File

@ -120,12 +120,6 @@ $accounts = demo_users();
<div class="eyebrow text-white-50 mb-3"><?= h(tr('مرحباً بك مجدداً', 'Welcome Back')) ?></div>
<h2 class="display-6 fw-bold mb-4"><?= h(current_lang() === 'ar' ? get_setting('company_name_ar', 'نظام إدارة') : get_setting('company_name_en', 'Management System')) ?></h2>
<p class="lead opacity-75"><?= h(tr('نظام متكامل لتسجيل المبيعات، وإدارة المخزون، والتقارير في واجهة واحدة.', 'Integrated system for logging sales, managing inventory, and reports in one interface.')) ?></p>
<div class="mini-grid mt-5 gap-3 d-flex flex-wrap">
<div class="stat-chip text-white px-3 py-2 rounded"><strong>3</strong> <span><?= h(tr('أدوار', 'Roles')) ?></span></div>
<div class="stat-chip text-white px-3 py-2 rounded"><strong>3</strong> <span><?= h(tr('فروع', 'Branches')) ?></span></div>
<div class="stat-chip text-white px-3 py-2 rounded"><strong>2</strong> <span><?= h(tr('لغات', 'Languages')) ?></span></div>
</div>
</div>
<div class="mt-5 pt-5 border-top border-light border-opacity-25 text-white-50 small">
&copy; <?= date('Y') ?> <?= h($projectName) ?>
@ -145,7 +139,7 @@ $accounts = demo_users();
<img src="<?= h(get_setting('company_logo')) ?>" alt="Logo" class="company-logo">
<?php else: ?>
<div class="company-logo mx-auto bg-primary bg-opacity-10 text-primary d-flex align-items-center justify-content-center" style="width: 90px; height: 90px; font-size: 2.5rem; font-weight: bold;">
<?= h(mb_substr(current_lang() === 'ar' ? get_setting('company_name_ar', ظام') : get_setting('company_name_en', 'System'), 0, 1)) ?>
<?= h((current_lang() === 'ar' ? ' : 'S')) ?>
</div>
<?php endif; ?>
<h3 class="fw-bold mb-1"><?= h(tr('تسجيل الدخول', 'Sign in to your account')) ?></h3>
@ -169,14 +163,14 @@ $accounts = demo_users();
<form method="post" class="mb-5">
<div class="mb-3">
<label class="form-label fw-semibold" for="username"><?= h(tr('اسم المستخدم', 'Username')) ?></label>
<input id="username" name="username" class="form-control form-control-lg rounded-3 bg-light border-0" autocomplete="username" required>
<input id="username" name="username" class="form-control form-control-lg rounded-3 border border-secondary-subtle" autocomplete="username" placeholder="<?= h(tr('اسم المستخدم', 'Username')) ?>" value="<?= h(array_values($accounts)[0]['username'] ?? '') ?>" required>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label fw-semibold mb-0" for="password"><?= h(tr('كلمة المرور', 'Password')) ?></label>
<a href="#" class="text-decoration-none small text-primary fw-medium" data-bs-toggle="modal" data-bs-target="#resetPasswordModal"><?= h(tr('نسيت كلمة المرور؟', 'Forgot password?')) ?></a>
</div>
<input id="password" name="password" type="password" class="form-control form-control-lg rounded-3 bg-light border-0" autocomplete="current-password" required>
<input id="password" name="password" type="password" class="form-control form-control-lg rounded-3 border border-secondary-subtle" autocomplete="current-password" placeholder="********" value="<?= h(array_values($accounts)[0]['password'] ?? '') ?>" required>
</div>
<button class="btn btn-primary btn-lg w-100 rounded-3 shadow-sm fw-semibold" type="submit"><?= h(tr('دخول', 'Sign in')) ?></button>
</form>
@ -221,7 +215,7 @@ $accounts = demo_users();
<p class="text-muted mb-4"><?= h(tr('أدخل اسم المستخدم أو البريد الإلكتروني وسنقوم بإرسال رابط لإعادة تعيين كلمة المرور الخاصة بك.', 'Enter your username or email and we will send you a link to reset your password.')) ?></p>
<div class="mb-3">
<label for="reset_username" class="form-label fw-semibold"><?= h(tr('البريد الإلكتروني / اسم المستخدم', 'Email / Username')) ?></label>
<input type="text" class="form-control form-control-lg bg-light border-0 rounded-3" id="reset_username" name="reset_username" required>
<input type="text" class="form-control form-control-lg rounded-3 border border-secondary-subtle" id="reset_username" name="reset_username" placeholder="user@example.com" required>
</div>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">

2
new_purchase.php Normal file
View File

@ -0,0 +1,2 @@
<?php
require_once __DIR__ . '/includes/purchase_form.php';

33
patch_header.php Normal file
View File

@ -0,0 +1,33 @@
<?php
$content = file_get_contents('includes/header.php');
$search = <<<HTML
<a class="list-group-item list-group-item-action <?= \$activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>">
<i class="bi bi-cart-check"></i> <?= h(tr('نقاط البيع', 'POS Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>">
<i class="bi bi-receipt"></i> <?= h(tr('بيع عادي', 'Normal Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('المبيعات', 'Sales')) ?>
</a>
HTML;
$replace = <<<HTML
<div class="px-3 pt-3 pb-2 text-white-50 text-uppercase small fw-bold">
<?= h(tr('المبيعات', 'Sales')) ?>
</div>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('قائمة الفواتير', 'Invoice list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>">
<i class="bi bi-plus-circle"></i> <?= h(tr('فاتورة جديدة', 'New invoice')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>">
<i class="bi bi-cart-check"></i> <?= h(tr('نقاط البيع', 'POS')) ?>
</a>
HTML;
$newContent = str_replace($search, $replace, $content);
file_put_contents('includes/header.php', $newContent);
echo "Done";

View File

@ -4,45 +4,53 @@ $user = require_roles(['owner', 'manager']);
$pageTitle = tr('المشتريات', 'Purchases');
$activeNav = 'purchases';
$allPurchases = purchase_pipeline();
// Search logic
$search = $_GET['q'] ?? '';
$filteredPurchases = [];
if ($search) {
$lowerSearch = strtolower($search);
foreach ($allPurchases as $key => $row) {
if (
str_contains(strtolower((string)$row['supplier']), $lowerSearch) ||
str_contains(strtolower((string)$row['reference']), $lowerSearch)
) {
$filteredPurchases[$key] = $row;
}
}
} else {
$filteredPurchases = $allPurchases;
}
// Pagination logic
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$total = count($filteredPurchases);
$totalPages = max(1, ceil($total / $limit));
$offset = ($page - 1) * $limit;
$purchaseRows = array_slice($filteredPurchases, $offset, $limit, true);
$params = [];
$where = ' WHERE 1=1 ';
if ($search) {
$where .= ' AND (supplier_name LIKE :search OR reference_no LIKE :search) ';
$params[':search'] = "%$search%";
}
// Pagination counts
$countSql = 'SELECT COUNT(*) FROM purchase_orders' . $where;
$countStmt = db()->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$total = $countStmt->fetchColumn();
$totalPages = max(1, ceil($total / $limit));
// Fetch Data
$sql = 'SELECT * FROM purchase_orders' . $where . ' ORDER BY purchase_date DESC LIMIT :limit OFFSET :offset';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$purchaseRows = $stmt->fetchAll();
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-2"><i class="bi bi-bag-plus me-2"></i><?= h(tr('لوحة استلام الموردين', 'Supplier receiving board')) ?></h3>
<p class="text-muted mb-0"><?= h(tr('هذه صفحة تمهيدية منظمة للمشتريات حتى تكون كل وحدة في صفحة مستقلة من البداية.', 'This is a structured starter page for purchases so every module already has a dedicated screen.')) ?></p>
<h3 class="h5 mb-2"><i class="bi bi-bag-plus me-2"></i><?= h(tr('قائمة المشتريات', 'Purchase List')) ?></h3>
<p class="text-muted mb-0"><?= h(tr('عرض المشتريات من الموردين وتحديث المخزون.', 'View supplier purchases and inventory updates.')) ?></p>
</div>
<div class="col-lg-4 text-lg-end">
<button type="button" class="btn btn-primary" onclick="mockEdit()">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة أمر شراء', 'Add Purchase Order')) ?>
</button>
<a href="new_purchase.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة فاتورة مشتريات', 'Add Purchase Invoice')) ?>
</a>
</div>
</div>
@ -62,22 +70,30 @@ require __DIR__ . '/includes/header.php';
<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('الإجمالي', 'Total Amount')) ?></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('التاريخ', 'Date')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<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>
<tr><td colspan="7" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($purchaseRows as $row): ?>
<tr>
<td><?= h($row['supplier']) ?></td>
<td><?= h($row['reference']) ?></td>
<td><?= h(branch_label($row['branch'])) ?></td>
<td><span class="badge text-bg-light border"><?= h($row['status']) ?></span></td>
<td><?= h($row['eta']) ?></td>
<td><?= h($row['supplier_name'] ?: '-') ?></td>
<td><?= h($row['reference_no']) ?></td>
<td><?= h(branch_label($row['branch_code'])) ?></td>
<td class="fw-semibold"><?= h(currency((float)$row['total_amount'])) ?></td>
<td>
<?php if ($row['status'] === 'order'): ?>
<span class="badge bg-warning text-dark px-3 py-2 rounded-pill"><i class="bi bi-clock"></i> <?= h(tr('طلب شراء', 'Order')) ?></span>
<?php else: ?>
<span class="badge bg-success px-3 py-2 rounded-pill"><i class="bi bi-check-circle"></i> <?= h(tr('مكتمل', 'Completed')) ?></span>
<?php endif; ?>
</td>
<td><?= h(date('Y-m-d', strtotime((string)$row['purchase_date']))) ?></td>
<td>
<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>
@ -108,8 +124,8 @@ require __DIR__ . '/includes/header.php';
<script>
function mockEdit() {
Swal.fire({
title: '<?= h(tr('تعديل (تجريبي)', 'Edit (Demo)')) ?>',
text: '<?= h(tr('هذه الميزة غير مفعلة للبيانات التجريبية.', 'This feature is mock data and not active yet.')) ?>',
title: '<?= h(tr('تعديل (غير متاح)', 'Edit (Disabled)')) ?>',
text: '<?= h(tr('تعديل الفواتير غير متاح حالياً.', 'Editing invoices is currently disabled.')) ?>',
icon: 'info',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
});
@ -118,7 +134,7 @@ function mockEdit() {
function mockDelete() {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
text: '<?= h(tr('لن تتمكن من التراجع عن هذا!', "You won't be able to revert this!")) ?>',
text: '<?= h(tr('هل تريد حذف هذه الفاتورة؟ (هذه الميزة غير متاحة حالياً)', "Do you want to delete this invoice? (Currently disabled)")) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
@ -128,13 +144,13 @@ function mockDelete() {
}).then((result) => {
if (result.isConfirmed) {
Swal.fire(
'<?= h(tr('محذوف!', 'Deleted!')) ?>',
'<?= h(tr('حساب تجريبي لا يمكن حذفه.', 'Demo account cannot be deleted.')) ?>',
'success'
'<?= h(tr('مرفوض!', 'Denied!')) ?>',
'<?= h(tr('حذف الفاتورة غير مدعوم في هذه النسخة.', 'Deleting invoice is not supported in this version.')) ?>',
'error'
);
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -19,7 +19,7 @@ $companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلو
$companyAddress = get_setting('company_address', '');
$companyVat = get_setting('company_vat_number', '300123456789012');
$companyEmail = 'info@flatlogic.com';
$companyPhone = '<?= h(get_setting('company_phone', '')) ?>';
$companyPhone = get_setting('company_phone', '');
require __DIR__ . '/includes/header.php';
?>

127
stock.php
View File

@ -15,6 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$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;
@ -50,8 +51,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
exit;
}
$sql = "UPDATE items SET sku=?, name=?, price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=? " . ($image_url ? ", image_url=?" : "") . " WHERE sku=?";
$params = [$sku, $name, $price, $base_stock, $vat, $category_id, $supplier_id, $unit_id];
$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;
}
@ -63,8 +64,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
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, unit_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$sku, $name, $price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $image_url]);
$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]);
@ -201,6 +202,7 @@ require __DIR__ . '/includes/header.php';
<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>
@ -210,7 +212,7 @@ require __DIR__ . '/includes/header.php';
</thead>
<tbody class="border-top-0">
<?php if(empty($stockRows)): ?>
<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>
<tr><td colspan="10" 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;">
@ -226,6 +228,7 @@ require __DIR__ . '/includes/header.php';
</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>
@ -237,7 +240,7 @@ require __DIR__ . '/includes/header.php';
<?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'] ?? 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')) ?>">
<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 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')) ?>">
@ -276,63 +279,77 @@ require __DIR__ . '/includes/header.php';
<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/*">
</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="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 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>
<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">
<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-4">
<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-4">
<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>
<div class="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 class="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="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 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">
@ -404,12 +421,13 @@ document.addEventListener('DOMContentLoaded', function () {
}
});
function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '<?= h(get_setting('vat_percentage', 5)) ?>', category_id = '', supplier_id = '', unit_id = '', image_url = '') {
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;
@ -446,6 +464,7 @@ async function handleItemSubmit(e) {
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);