39529-vm/products.php
2026-04-09 10:12:19 +00:00

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(); ?>