339 lines
17 KiB
PHP
339 lines
17 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/app.php';
|
|
app_bootstrap();
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
|
flash('danger', 'انتهت صلاحية الجلسة. أعد المحاولة.');
|
|
header('Location: products.php');
|
|
exit;
|
|
}
|
|
|
|
$action = (string) ($_POST['action'] ?? '');
|
|
$redirect = 'products.php';
|
|
|
|
if ($action === 'create_product') {
|
|
$result = create_product($_POST);
|
|
} elseif ($action === 'update_product') {
|
|
$productId = (int) ($_POST['product_id'] ?? 0);
|
|
$result = update_product($productId, $_POST);
|
|
if (!$result['success'] && $productId > 0) {
|
|
$redirect = 'products.php?edit=' . $productId;
|
|
}
|
|
} elseif ($action === 'adjust_stock') {
|
|
$productId = (int) ($_POST['product_id'] ?? 0);
|
|
$result = adjust_product_stock(
|
|
$productId,
|
|
(string) ($_POST['direction'] ?? ''),
|
|
(float) ($_POST['qty'] ?? 0),
|
|
(string) ($_POST['reason'] ?? ''),
|
|
(string) ($_POST['notes'] ?? '')
|
|
);
|
|
if (!$result['success'] && $productId > 0) {
|
|
$redirect = 'products.php?adjust=' . $productId;
|
|
}
|
|
} else {
|
|
$result = ['success' => false, 'message' => 'الإجراء المطلوب غير معروف.'];
|
|
}
|
|
|
|
flash($result['success'] ? 'success' : 'danger', $result['message']);
|
|
header('Location: ' . $redirect);
|
|
exit;
|
|
}
|
|
|
|
$editId = (int) ($_GET['edit'] ?? 0);
|
|
$adjustId = (int) ($_GET['adjust'] ?? 0);
|
|
$editingProduct = $editId > 0 ? fetch_product_by_id($editId) : null;
|
|
$products = fetch_products();
|
|
$productOptions = fetch_product_options();
|
|
$movements = fetch_inventory_movements(12);
|
|
|
|
$finished = 0;
|
|
$raw = 0;
|
|
$low = 0;
|
|
$totalStock = 0.0;
|
|
foreach ($products as $product) {
|
|
$qty = (float) $product['stock_qty'];
|
|
$totalStock += $qty;
|
|
if ($product['category'] === 'finished') {
|
|
$finished++;
|
|
} else {
|
|
$raw++;
|
|
}
|
|
if ($qty <= 40) {
|
|
$low++;
|
|
}
|
|
}
|
|
|
|
$productForm = [
|
|
'sku' => $editingProduct['sku'] ?? '',
|
|
'name' => $editingProduct['name'] ?? '',
|
|
'category' => $editingProduct['category'] ?? 'finished',
|
|
'unit' => $editingProduct['unit'] ?? '',
|
|
'stock_qty' => '',
|
|
'cost_price' => isset($editingProduct['cost_price']) ? format_qty((float) $editingProduct['cost_price']) : '',
|
|
'sale_price' => isset($editingProduct['sale_price']) ? format_qty((float) $editingProduct['sale_price']) : '',
|
|
];
|
|
|
|
function movement_type_label(string $type): array
|
|
{
|
|
return match ($type) {
|
|
'opening' => ['label' => 'رصيد افتتاحي', 'class' => 'neutral'],
|
|
'sale' => ['label' => 'بيع', 'class' => 'warning'],
|
|
'adjustment_in' => ['label' => 'إضافة يدوية', 'class' => 'success'],
|
|
'adjustment_out' => ['label' => 'خصم يدوي', 'class' => 'pending'],
|
|
default => ['label' => $type, 'class' => 'neutral'],
|
|
};
|
|
}
|
|
|
|
render_header('الأصناف والمخزون', 'products');
|
|
?>
|
|
<section class="mb-4">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-4">
|
|
<div>
|
|
<span class="section-kicker">Products & Inventory</span>
|
|
<h1 class="page-title mb-2">إدارة الأصناف وضبط المخزون</h1>
|
|
<p class="page-lead mb-0">أصبحت الشاشة الآن تشغيلية: إضافة صنف جديد، تعديل بياناته، وضبط المخزون يدويًا مع سجل حركات واضح. كما أن أوامر البيع تسجل الآن حركة خصم تلقائية داخل نفس السجل.</p>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<span class="badge text-bg-light border text-dark">إجمالي المخزون: <?= h(format_qty($totalStock)) ?></span>
|
|
<span class="badge text-bg-light border text-dark">منتجات نهائية: <?= h((string) $finished) ?></span>
|
|
<span class="badge text-bg-light border text-dark">خامات: <?= h((string) $raw) ?></span>
|
|
<span class="badge text-bg-light border text-dark">منخفضة: <?= h((string) $low) ?></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="mb-4 mb-lg-5">
|
|
<div class="row g-4 align-items-stretch">
|
|
<div class="col-xl-7">
|
|
<div class="card panel-card h-100">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<span class="section-kicker"><?= $editingProduct ? 'Edit product' : 'Create product' ?></span>
|
|
<h2 class="section-title mb-0"><?= $editingProduct ? 'تعديل بيانات الصنف' : 'إضافة صنف جديد' ?></h2>
|
|
</div>
|
|
<?php if ($editingProduct): ?>
|
|
<a class="btn btn-sm btn-outline-secondary" href="products.php">إلغاء التعديل</a>
|
|
<?php else: ?>
|
|
<span class="text-muted small">يمكنك إنشاء خامة أو منتج نهائي مع رصيد افتتاحي</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<form method="post" class="row g-3">
|
|
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
|
<input type="hidden" name="action" value="<?= $editingProduct ? 'update_product' : 'create_product' ?>">
|
|
<?php if ($editingProduct): ?>
|
|
<input type="hidden" name="product_id" value="<?= h((string) $editingProduct['id']) ?>">
|
|
<?php endif; ?>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="sku">SKU</label>
|
|
<input class="form-control" type="text" id="sku" name="sku" maxlength="80" required value="<?= h($productForm['sku']) ?>" placeholder="FG-104">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label" for="name">اسم الصنف</label>
|
|
<input class="form-control" type="text" id="name" name="name" maxlength="160" required value="<?= h($productForm['name']) ?>" placeholder="مثال: جل تعقيم 250 مل">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="category">الفئة</label>
|
|
<select class="form-select" id="category" name="category" required>
|
|
<option value="finished" <?= $productForm['category'] === 'finished' ? 'selected' : '' ?>>منتج نهائي</option>
|
|
<option value="raw" <?= $productForm['category'] === 'raw' ? 'selected' : '' ?>>خامة</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="unit">وحدة القياس</label>
|
|
<input class="form-control" type="text" id="unit" name="unit" maxlength="40" required value="<?= h($productForm['unit']) ?>" placeholder="كرتون / برميل / حبة">
|
|
</div>
|
|
<?php if ($editingProduct): ?>
|
|
<div class="col-md-4">
|
|
<label class="form-label">المخزون الحالي</label>
|
|
<div class="form-control-plaintext product-inline-note"><?= h(format_qty((float) $editingProduct['stock_qty'])) ?> <?= h($editingProduct['unit']) ?> — غيّره من بطاقة ضبط المخزون</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="stock_qty">الرصيد الافتتاحي</label>
|
|
<input class="form-control" type="number" min="0" step="0.01" id="stock_qty" name="stock_qty" value="<?= h($productForm['stock_qty']) ?>" placeholder="0">
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="cost_price">تكلفة الوحدة</label>
|
|
<input class="form-control" type="number" min="0" step="0.01" id="cost_price" name="cost_price" required value="<?= h($productForm['cost_price']) ?>" placeholder="0.00">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="sale_price">سعر البيع الافتراضي</label>
|
|
<input class="form-control" type="number" min="0" step="0.01" id="sale_price" name="sale_price" required value="<?= h($productForm['sale_price']) ?>" placeholder="0.00">
|
|
</div>
|
|
<div class="col-12 d-flex justify-content-between align-items-center gap-2 flex-wrap mt-2">
|
|
<div class="text-muted small">التسعير الخاص للعملاء يبقى في شاشة العملاء، بينما هذه البطاقة تدير Master data للصنف نفسه.</div>
|
|
<button class="btn btn-dark" type="submit"><?= $editingProduct ? 'حفظ التعديلات' : 'إضافة الصنف' ?></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-5">
|
|
<div class="card panel-card h-100" id="adjust-stock-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<span class="section-kicker">Stock adjustment</span>
|
|
<h2 class="section-title mb-0">ضبط المخزون يدويًا</h2>
|
|
</div>
|
|
<span class="status-badge neutral">Audit trail</span>
|
|
</div>
|
|
<form method="post" class="row g-3">
|
|
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
|
<input type="hidden" name="action" value="adjust_stock">
|
|
<div class="col-12">
|
|
<label class="form-label" for="adjust_product_id">الصنف</label>
|
|
<select class="form-select" id="adjust_product_id" name="product_id" required>
|
|
<option value="">اختر الصنف</option>
|
|
<?php foreach ($productOptions as $option): ?>
|
|
<?php $selected = $adjustId === (int) $option['id'] ? 'selected' : ''; ?>
|
|
<option value="<?= h((string) $option['id']) ?>" <?= $selected ?>><?= h($option['name']) ?> — <?= h($option['sku']) ?> (<?= h(format_qty((float) $option['stock_qty'])) ?> <?= h($option['unit']) ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="direction">العملية</label>
|
|
<select class="form-select" id="direction" name="direction" required>
|
|
<option value="add">إضافة</option>
|
|
<option value="subtract">خصم</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="adjust_qty">الكمية</label>
|
|
<input class="form-control" type="number" min="0.01" step="0.01" id="adjust_qty" name="qty" required placeholder="10">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="reason">السبب</label>
|
|
<input class="form-control" type="text" id="reason" name="reason" maxlength="120" required placeholder="جرد / استلام / هالك">
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="notes">ملاحظات إضافية</label>
|
|
<textarea class="form-control" id="notes" name="notes" rows="3" maxlength="255" placeholder="وصف مختصر لسبب التعديل"></textarea>
|
|
</div>
|
|
<div class="col-12 d-flex justify-content-between align-items-center gap-2 flex-wrap mt-2">
|
|
<div class="text-muted small">كل تعديل يسجل قبل/بعد في جدول الحركات، لذلك ستتمكن لاحقًا من ربطه بالمشتريات والتصنيع بسهولة.</div>
|
|
<button class="btn btn-outline-dark" type="submit">تنفيذ التعديل</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="mb-4 mb-lg-5">
|
|
<div class="card panel-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<span class="section-kicker">Product master</span>
|
|
<h2 class="section-title mb-0">كل الأصناف</h2>
|
|
</div>
|
|
<span class="text-muted small">تعديل البيانات الأساسية من هنا، وضبط الكميات من البطاقة الجانبية</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle app-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>SKU</th>
|
|
<th>الصنف</th>
|
|
<th>الفئة</th>
|
|
<th>المتاح</th>
|
|
<th>تكلفة</th>
|
|
<th>بيع افتراضي</th>
|
|
<th>الحالة</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($products as $product): ?>
|
|
<?php $qty = (float) $product['stock_qty']; ?>
|
|
<tr>
|
|
<td class="fw-semibold"><?= h($product['sku']) ?></td>
|
|
<td>
|
|
<div class="fw-semibold"><?= h($product['name']) ?></div>
|
|
<div class="text-muted small"><?= h($product['unit']) ?></div>
|
|
</td>
|
|
<td><span class="status-badge <?= $product['category'] === 'finished' ? 'neutral' : 'pending' ?>"><?= $product['category'] === 'finished' ? 'منتج نهائي' : 'خامة' ?></span></td>
|
|
<td><?= h(format_qty($qty)) ?> <?= h($product['unit']) ?></td>
|
|
<td><?= h(format_money((float) $product['cost_price'])) ?></td>
|
|
<td><?= h((float) $product['sale_price'] > 0 ? format_money((float) $product['sale_price']) : '—') ?></td>
|
|
<td>
|
|
<?php if ($qty <= 40): ?>
|
|
<span class="status-badge warning">منخفض</span>
|
|
<?php else: ?>
|
|
<span class="status-badge success">مستقر</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex gap-2 justify-content-end flex-wrap">
|
|
<a class="btn btn-sm btn-outline-secondary" href="products.php?edit=<?= h((string) $product['id']) ?>">تعديل</a>
|
|
<a class="btn btn-sm btn-outline-dark" href="products.php?adjust=<?= h((string) $product['id']) ?>#adjust-stock-card">ضبط مخزون</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="card panel-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
<div>
|
|
<span class="section-kicker">Inventory journal</span>
|
|
<h2 class="section-title mb-0">آخر حركات المخزون</h2>
|
|
</div>
|
|
<span class="text-muted small">يشمل الرصيد الافتتاحي، التعديلات اليدوية، وخصومات البيع الجديدة</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle app-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>الوقت</th>
|
|
<th>الصنف</th>
|
|
<th>النوع</th>
|
|
<th>التغيير</th>
|
|
<th>قبل ← بعد</th>
|
|
<th>مرجع / ملاحظات</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (!$movements): ?>
|
|
<tr>
|
|
<td colspan="6">
|
|
<div class="empty-state compact">
|
|
<div class="empty-title">لا توجد حركات حتى الآن</div>
|
|
<div class="empty-copy">أضف صنفًا برصيد افتتاحي أو نفّذ ضبط مخزون أو أمر بيع لتظهر هنا أول حركة.</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($movements as $movement): ?>
|
|
<?php $meta = movement_type_label((string) $movement['movement_type']); ?>
|
|
<?php $delta = (float) $movement['qty_change']; ?>
|
|
<tr>
|
|
<td><?= h(date('Y-m-d H:i', strtotime($movement['created_at']))) ?></td>
|
|
<td>
|
|
<div class="fw-semibold"><?= h($movement['product_name']) ?></div>
|
|
<div class="text-muted small"><?= h($movement['sku']) ?></div>
|
|
</td>
|
|
<td><span class="status-badge <?= h($meta['class']) ?>"><?= h($meta['label']) ?></span></td>
|
|
<td class="<?= $delta >= 0 ? 'text-success' : 'text-danger' ?> fw-semibold"><?= h(($delta >= 0 ? '+' : '') . format_qty($delta)) ?> <?= h($movement['unit']) ?></td>
|
|
<td><?= h(format_qty((float) $movement['stock_before'])) ?> → <?= h(format_qty((float) $movement['stock_after'])) ?></td>
|
|
<td>
|
|
<div><?= h($movement['reference_code'] ?: '—') ?></div>
|
|
<div class="text-muted small"><?= h($movement['notes'] ?: 'بدون ملاحظات') ?></div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php render_footer(); ?>
|