new updates

This commit is contained in:
Flatlogic Bot 2026-05-01 18:08:28 +00:00
parent 5f5205783f
commit 07894c8f77
8 changed files with 506 additions and 110 deletions

View File

@ -0,0 +1,14 @@
-- Add notes field to products/items for internal product remarks.
-- Safe to run multiple times on existing databases.
SET @db_name := DATABASE();
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'items' AND COLUMN_NAME = 'notes'
),
'SELECT 1',
"ALTER TABLE items ADD COLUMN notes text DEFAULT NULL AFTER image_url"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,34 @@
-- Production sync for 2026-05-01 changes.
-- Safe to import on an existing production database.
-- Includes all schema migrations introduced on 2026-05-01.
-- Current scope:
-- 1) items.notes for internal product remarks in stock management.
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS apply_production_updates_20260501;
DELIMITER $$
CREATE PROCEDURE apply_production_updates_20260501()
BEGIN
-- items.notes
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'items'
AND COLUMN_NAME = 'notes'
) THEN
ALTER TABLE items
ADD COLUMN notes TEXT DEFAULT NULL AFTER image_url;
END IF;
END $$
DELIMITER ;
CALL apply_production_updates_20260501();
DROP PROCEDURE IF EXISTS apply_production_updates_20260501;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
-- Optional verification after import:
-- SHOW COLUMNS FROM items;

View File

@ -73,6 +73,7 @@ CREATE TABLE IF NOT EXISTS `items` (
`category_id` int(10) unsigned DEFAULT NULL,
`supplier_id` int(10) unsigned DEFAULT NULL,
`image_url` varchar(255) DEFAULT NULL,
`notes` text DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`unit_id` int(10) unsigned DEFAULT NULL,
`in_catalog` tinyint(1) NOT NULL DEFAULT 0,

View File

@ -24,7 +24,7 @@ $isEidSale = (($editSale['order_type'] ?? 'standard') === 'eid');
$activeNav = $isEidSale ? 'eid_orders' : 'sales';
$error = '';
$editPaymentSummary = sale_payment_summary($editSale);
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? ($editPaymentSummary['paid_amount'] > 0 ? number_format((float) $editPaymentSummary['paid_amount'], 3, '.', '') : ''));
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? number_format((float) $editPaymentSummary['paid_amount'], 3, '.', ''));
$catalog = catalog();
$allowedBranches = get_user_branches($user);
$deliveryOptions = eid_delivery_status_options();
@ -45,6 +45,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$paymentAmountInput = trim((string) ($_POST['payment_amount'] ?? ''));
$paymentAmountForCalculation = $paymentAmountInput === '' ? '0' : $paymentAmountInput;
$saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed'));
$saleStatusInput = $saleStatus;
$notes = trim((string) ($_POST['notes'] ?? ''));
@ -106,7 +107,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($paymentAmountInput !== '' && !is_numeric($paymentAmountInput)) {
$error = tr('أدخل مبلغاً مدفوعاً صحيحاً.', 'Enter a valid paid amount.');
} else {
$paymentMeta = sale_payment_breakdown($totalAmount, $paymentMethod, $paymentAmountInput);
$paymentMeta = sale_payment_breakdown($totalAmount, $paymentMethod, $paymentAmountForCalculation);
if ($paymentMeta['due_amount'] > 0.0005 && !$customerId) {
$error = tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ أو دفعة جزئية.', 'Select a registered customer when there is a remaining balance or partial payment.');
}
@ -186,7 +187,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
set_flash($flashType, $flashMessage);
redirect_to('sale.php', ['id' => $editSaleId]);
redirect_to($isEidSale ? 'eid_orders.php' : 'sales.php');
}
}
}
@ -482,7 +483,21 @@ require __DIR__ . '/includes/header.php';
<div class="mb-3">
<label class="form-label" for="payment_amount"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" class="form-control custom-input" id="payment_amount" name="payment_amount" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل الدفعة الجزئية وسيتم إعادة احتساب المتبقي تلقائياً.', 'Adjust the paid amount and the remaining balance will be recalculated automatically.')) ?></div>
<div class="row g-2 mt-2" aria-live="polite">
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المدفوع بعد الحفظ', 'Paid after save')) ?></div>
<div class="fw-semibold text-success" id="displayPaidAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المتبقي بعد الحفظ', 'Remaining after save')) ?></div>
<div class="fw-semibold text-primary" id="displayDueAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
</div>
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل المبلغ المدفوع يدوياً وسيتم إعادة احتساب المتبقي تلقائياً.', 'You can edit the paid amount manually and the remaining balance will be recalculated automatically.')) ?></div>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
@ -802,15 +817,28 @@ function renderInvoice() {
cartJson.value = JSON.stringify(cartData);
}
function formatMoney(value) {
return value.toFixed(3) + currencySuffix;
}
function updatePaymentAmountHint() {
const paymentAmountField = document.getElementById('payment_amount');
const paymentAmountHint = document.getElementById('paymentAmountHint');
const displayPaidAmountLive = document.getElementById('displayPaidAmountLive');
const displayDueAmountLive = document.getElementById('displayDueAmountLive');
if (!paymentAmountField || !paymentAmountHint) {
return;
}
const entered = Math.max(0, parseFloat(paymentAmountField.value || '0') || 0);
const due = Math.max(0, currentInvoiceTotal - Math.min(entered, currentInvoiceTotal));
paymentAmountHint.innerText = partialPaymentText + ' ' + due.toFixed(3) + currencySuffix;
const paid = Math.min(entered, currentInvoiceTotal);
const due = Math.max(0, currentInvoiceTotal - paid);
paymentAmountHint.innerText = partialPaymentText + ' ' + formatMoney(due);
if (displayPaidAmountLive) {
displayPaidAmountLive.innerText = formatMoney(paid);
}
if (displayDueAmountLive) {
displayDueAmountLive.innerText = formatMoney(due);
}
}
function syncPaymentAmount(force = false) {

View File

@ -481,7 +481,7 @@ require __DIR__ . '/includes/header.php';
<td>
<div class="d-flex justify-content-end gap-2">
<?php if ((($order['payment_summary']['payment_status'] ?? ($order['payment_status'] ?? 'unpaid'))) !== 'paid'): ?>
<form method="post" action="" class="d-inline" onsubmit="return confirm('<?= h(tr('هل أنت متأكد من تغيير حالة الدفع إلى مدفوع؟', 'Are you sure you want to mark as paid?')) ?>');">
<form method="post" action="" class="d-inline js-confirm-mark-paid" data-confirm-message="<?= h(tr('هل أنت متأكد من تغيير حالة الدفع إلى مدفوع؟', 'Are you sure you want to mark as paid?')) ?>" data-confirm-title="<?= h(tr('تأكيد الإجراء', 'Confirm action')) ?>">
<input type="hidden" name="action" value="mark_as_paid">
<input type="hidden" name="id" value="<?= h($order['id']) ?>">
<button type="submit" class="btn btn-sm btn-outline-success" title="<?= h(tr('تحويل لمدفوع', 'Mark as Paid')) ?>">
@ -530,4 +530,40 @@ require __DIR__ . '/includes/header.php';
<?php endif; ?>
<?php endif; ?>
</section>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.js-confirm-mark-paid').forEach(function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
var message = form.getAttribute('data-confirm-message') || '';
var title = form.getAttribute('data-confirm-title') || '';
if (typeof Swal === 'undefined') {
if (window.confirm(message)) {
form.submit();
}
return;
}
Swal.fire({
icon: 'warning',
title: title,
text: message,
position: 'center',
showCancelButton: true,
confirmButtonText: '<?= h(tr('نعم، تحويلها لمدفوع', 'Yes, mark it as paid')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
confirmButtonColor: '#198754',
cancelButtonColor: '#6c757d',
focusCancel: true
}).then(function (result) {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -208,6 +208,20 @@ try {
@file_put_contents($flagFileV9, '1');
}
$flagFileV10 = sys_get_temp_dir() . '/.schema_migrated_v10_' . md5(__DIR__);
if (!file_exists($flagFileV10)) {
$pdo = db();
$hasItemsTable = (bool) $pdo->query("SHOW TABLES LIKE 'items'")->fetchColumn();
if ($hasItemsTable) {
$hasNotesColumn = (bool) $pdo->query("SHOW COLUMNS FROM items LIKE 'notes'")->fetchColumn();
if (!$hasNotesColumn) {
$pdo->exec("ALTER TABLE items ADD COLUMN notes text DEFAULT NULL AFTER image_url");
}
}
@file_put_contents($flagFileV10, '1');
}
} catch (\Throwable $e) {}
@ -693,6 +707,7 @@ function catalog(): array
"category_id" => $item["category_id"], "in_catalog" => (int)($item["in_catalog"] ?? 0),
"supplier_id" => $item["supplier_id"],
"image_url" => $item["image_url"],
"notes" => $item["notes"] ?? null,
"created_at" => $item["created_at"] ?? null,
"unit_id" => $item["unit_id"],
"unit_ar" => $item["u_name_ar"] ?? "قطعة",
@ -2185,6 +2200,8 @@ function stock_snapshot(): array
'supplier_id' => $item['supplier_id'],
'unit_id' => $item['unit_id'],
'image_url' => $item['image_url'],
'in_catalog' => $item['in_catalog'] ?? 0,
'notes' => $item['notes'] ?? null,
'vat' => $item['vat'],
];
}

View File

@ -480,7 +480,21 @@ require __DIR__ . '/header.php';
<div class="mb-3">
<label class="form-label" for="payment_amount"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" class="form-control custom-input" id="payment_amount" name="payment_amount" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك إدخال دفعة جزئية وسيتم تتبع الباقي تلقائياً.', 'You can enter a partial payment and the remaining balance will be tracked automatically.')) ?></div>
<div class="row g-2 mt-2" aria-live="polite">
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المدفوع بعد الحفظ', 'Paid after save')) ?></div>
<div class="fw-semibold text-success" id="displayPaidAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المتبقي بعد الحفظ', 'Remaining after save')) ?></div>
<div class="fw-semibold text-primary" id="displayDueAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
</div>
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل المبلغ المدفوع يدوياً وسيتم تتبع الباقي تلقائياً.', 'You can edit the paid amount manually and the remaining balance will be tracked automatically.')) ?></div>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
@ -833,15 +847,28 @@ function renderInvoice() {
cartJson.value = JSON.stringify(cartData);
}
function formatMoney(value) {
return value.toFixed(3) + currencySuffix;
}
function updatePaymentAmountHint() {
const paymentAmountField = document.getElementById('payment_amount');
const paymentAmountHint = document.getElementById('paymentAmountHint');
const displayPaidAmountLive = document.getElementById('displayPaidAmountLive');
const displayDueAmountLive = document.getElementById('displayDueAmountLive');
if (!paymentAmountField || !paymentAmountHint) {
return;
}
const entered = Math.max(0, parseFloat(paymentAmountField.value || '0') || 0);
const due = Math.max(0, currentInvoiceTotal - Math.min(entered, currentInvoiceTotal));
paymentAmountHint.innerText = partialPaymentText + ' ' + due.toFixed(3) + currencySuffix;
const paid = Math.min(entered, currentInvoiceTotal);
const due = Math.max(0, currentInvoiceTotal - paid);
paymentAmountHint.innerText = partialPaymentText + ' ' + formatMoney(due);
if (displayPaidAmountLive) {
displayPaidAmountLive.innerText = formatMoney(paid);
}
if (displayDueAmountLive) {
displayDueAmountLive.innerText = formatMoney(due);
}
}
function syncPaymentAmount(force = false) {

439
stock.php
View File

@ -8,13 +8,13 @@ $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");
$stmt = $pdo->query("SELECT sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id, notes 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 "\xEF\xBB\xBF";
$output = fopen('php://output', 'w');
fputcsv($output, ['SKU', 'Name', 'Price', 'Cost Price', 'Stock', 'VAT', 'Category ID', 'Supplier ID', 'Unit ID']);
fputcsv($output, ['SKU', 'Name', 'Price', 'Cost Price', 'Stock', 'VAT', 'Category ID', 'Supplier ID', 'Unit ID', 'Notes']);
foreach ($items as $row) { fputcsv($output, $row); }
fclose($output);
exit;
@ -74,8 +74,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$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=?");
$stmtInsert = $pdo->prepare("INSERT INTO items (sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmtUpdate = $pdo->prepare("UPDATE items SET name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=?, notes=? WHERE sku=?");
$stmtCheck = $pdo->prepare("SELECT id FROM items WHERE sku=?");
$valid_categories = $pdo->query("SELECT id FROM categories")->fetchAll(PDO::FETCH_COLUMN);
@ -93,12 +93,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$category_id = (!empty($row[6]) && in_array((int)$row[6], $valid_categories)) ? (int)$row[6] : null;
$supplier_id = (!empty($row[7]) && in_array((int)$row[7], $valid_suppliers)) ? (int)$row[7] : null;
$unit_id = (!empty($row[8]) && in_array((int)$row[8], $valid_units)) ? (int)$row[8] : null;
$notes = isset($row[9]) ? trim((string)$row[9]) : null;
$notes = $notes === '' ? null : $notes;
$stmtCheck->execute([$sku]);
if ($stmtCheck->fetchColumn()) {
$stmtUpdate->execute([$name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $sku]);
$stmtUpdate->execute([$name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $notes, $sku]);
$updated++;
} else {
$stmtInsert->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id]);
$stmtInsert->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $notes]);
$imported++;
}
}
@ -132,6 +134,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$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;
$notes = trim((string)($_POST['notes'] ?? ''));
$notes = $notes !== '' ? $notes : null;
if (!$sku || !$name) {
echo json_encode(['success' => false, 'error' => 'Missing SKU or Name']);
@ -162,8 +166,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
exit;
}
$sql = "UPDATE items SET sku=?, name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=?, in_catalog=? " . ($image_url ? ", image_url=?" : "") . " WHERE sku=?";
$params = [$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $in_catalog];
$sql = "UPDATE items SET sku=?, name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=?, notes=?, in_catalog=? " . ($image_url ? ", image_url=?" : "") . " WHERE sku=?";
$params = [$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $notes, $in_catalog];
if ($image_url) {
$params[] = $image_url;
}
@ -175,8 +179,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, cost_price, base_stock, vat, category_id, supplier_id, unit_id, in_catalog, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $in_catalog, $image_url]);
$stmt = $pdo->prepare("INSERT INTO items (sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id, notes, in_catalog, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$sku, $name, $price, $cost_price, $base_stock, $vat, $category_id, $supplier_id, $unit_id, $notes, $in_catalog, $image_url]);
}
echo json_encode(['success' => true]);
@ -299,6 +303,172 @@ function sortable_header($field, $label, $currentSortField, $currentSortOrder, $
require __DIR__ . '/includes/header.php';
?>
<style>
#itemModal .modal-dialog {
max-width: min(1140px, calc(100vw - 2rem));
}
#itemModal .modal-content {
border: 0;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.18);
}
#itemModal .modal-header {
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #0d6efd, #2f80ff) !important;
}
#itemModal .modal-body {
padding: 1.25rem;
background: linear-gradient(180deg, #f8fbff 0%, #f5f7fb 100%);
}
#itemModal .modal-footer {
padding: 1rem 1.25rem;
background: #fff;
border-top: 1px solid rgba(13, 110, 253, 0.08);
}
.item-form-panel {
height: 100%;
padding: 1rem 1rem 0.75rem;
border: 1px solid rgba(13, 110, 253, 0.1);
border-radius: 20px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
.item-form-panel__eyebrow {
display: inline-block;
margin-bottom: 0.35rem;
color: #6c757d;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.item-form-panel__title {
margin-bottom: 1rem;
color: #1f2937;
font-weight: 700;
}
.item-form-panel .form-label {
margin-bottom: 0.4rem;
color: #475467;
font-size: 0.86rem;
font-weight: 600;
}
.item-form-panel .form-control,
.item-form-panel .form-select {
min-height: 46px;
border-color: #dbe4f0;
border-radius: 14px;
box-shadow: none;
}
.item-form-panel textarea.form-control {
min-height: 148px;
resize: vertical;
}
.item-form-panel .form-control:focus,
.item-form-panel .form-select:focus {
border-color: rgba(13, 110, 253, 0.45);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.12);
}
.item-form-panel .input-group > .form-control {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
.item-form-panel .input-group > .btn {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-color: #dbe4f0;
}
.item-form-note {
display: block;
margin-top: 0.45rem;
color: #6c757d;
font-size: 0.78rem;
}
.item-switch-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.9rem 1rem;
border: 1px solid rgba(13, 110, 253, 0.12);
border-radius: 16px;
background: linear-gradient(180deg, #fbfdff 0%, #f3f8ff 100%);
}
.item-switch-wrap .form-check {
margin-bottom: 0;
}
.item-switch-wrap .form-check-input {
float: none;
margin: 0;
transform: scale(1.15);
}
.item-image-dropzone {
padding: 1rem;
border: 1px dashed rgba(13, 110, 253, 0.35);
border-radius: 18px;
background: linear-gradient(180deg, #fbfdff 0%, #f0f7ff 100%);
}
.item-image-preview {
min-height: 190px;
margin-top: 0.85rem;
padding: 1rem;
border: 1px solid rgba(13, 110, 253, 0.12);
border-radius: 16px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.item-image-preview img {
max-width: 100%;
max-height: 160px;
object-fit: contain;
border-radius: 14px;
}
.item-image-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
text-align: center;
color: #98a2b3;
font-size: 0.92rem;
}
@media (max-width: 991.98px) {
#itemModal .modal-dialog {
max-width: calc(100vw - 1rem);
}
#itemModal .modal-body,
#itemModal .modal-footer {
padding: 1rem;
}
}
</style>
<section class="mb-4">
<div class="row g-4 align-items-center mb-3">
<div class="col-lg-5">
@ -423,7 +593,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['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'] ?? '') ?>', '<?= h($row['in_catalog'] ?? 0) ?>')" 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'] ?? '') ?>', '<?= h($row['in_catalog'] ?? 0) ?>', <?= h(json_encode((string) ($row['notes'] ?? ''), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>)" 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')) ?>">
@ -490,94 +660,136 @@ require __DIR__ . '/includes/header.php';
<!-- Item Modal -->
<div class="modal fade" id="itemModal" tabindex="-1" aria-labelledby="itemModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<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 class="modal-header bg-primary text-white">
<div>
<h5 class="modal-title mb-1" id="itemModalLabel"><?= h(tr('إضافة / تعديل صنف', 'Add / Edit Item')) ?></h5>
<div class="small text-white-50"><?= h(tr('نموذج أسرع ومنظم بثلاثة أعمدة لإدخال بيانات الصنف.', 'A faster 3-column layout for product details.')) ?></div>
</div>
<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 class="row g-3">
<div class="col-12 col-lg-4">
<div class="item-form-panel">
<span class="item-form-panel__eyebrow"><?= h(tr('الأساسيات', 'Basics')) ?></span>
<h6 class="item-form-panel__title"><?= h(tr('بيانات الصنف', 'Item Identity')) ?></h6>
<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>
<small class="item-form-note"><?= h(tr('يمكنك توليد رمز سريع تلقائياً.', 'Generate a quick SKU automatically.')) ?></small>
</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="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-0">
<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>
</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('الظهور في المتجر (الكتالوج)', 'Visible in Catalog')) ?></label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" id="item_in_catalog" style="transform: scale(1.3); margin-left: -1.5em; margin-right: 0.5em;">
<label class="form-check-label" for="item_in_catalog" style="margin-right: 2.5em;"><?= h(tr('عرض أونلاين', 'Show Online')) ?></label>
<div class="col-12 col-lg-4">
<div class="item-form-panel">
<span class="item-form-panel__eyebrow"><?= h(tr('البيع والمخزون', 'Pricing & Stock')) ?></span>
<h6 class="item-form-panel__title"><?= h(tr('التسعير والمخزون', 'Pricing & Inventory')) ?></h6>
<div class="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="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="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="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="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="mb-0">
<label class="form-label"><?= h(tr('الظهور في المتجر (الكتالوج)', 'Visible in Catalog')) ?></label>
<div class="item-switch-wrap">
<div>
<div class="fw-semibold text-dark"><?= h(tr('عرض أونلاين', 'Show Online')) ?></div>
<small class="text-muted"><?= h(tr('إظهار الصنف في الكتالوج والمتجر.', 'Display this item in the catalog/store.')) ?></small>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="item_in_catalog">
</div>
</div>
</div>
</div>
</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-12 col-lg-4">
<div class="item-form-panel">
<span class="item-form-panel__eyebrow"><?= h(tr('الصورة والملاحظات', 'Media & Notes')) ?></span>
<h6 class="item-form-panel__title"><?= h(tr('الصورة والملاحظات الداخلية', 'Image & Internal Notes')) ?></h6>
<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="item-image-dropzone mb-3">
<label class="form-label d-block"><?= h(tr('صورة الصنف', 'Item Picture')) ?></label>
<input type="file" class="form-control" id="item_picture" accept="image/*">
<div class="item-image-preview" id="itemPicturePreview">
<div class="item-image-placeholder">
<i class="bi bi-card-image fs-2"></i>
<span><?= h(tr('لا توجد صورة محددة بعد', 'No image selected yet')) ?></span>
</div>
</div>
</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 class="mb-0">
<label class="form-label"><?= h(tr('ملاحظات المنتج', 'Product Notes')) ?></label>
<textarea class="form-control" id="item_notes" rows="6" maxlength="1000" placeholder="<?= h(tr('ملاحظات داخلية عن المنتج...', 'Internal notes about this product...')) ?>"></textarea>
<small class="item-form-note"><?= h(tr('هذه الملاحظات داخلية وتساعد الفريق عند التعديل أو الجرد.', 'These notes are internal and help during edits or stock counting.')) ?></small>
</div>
</div>
</div>
</div>
</div>
@ -613,6 +825,23 @@ document.addEventListener('DOMContentLoaded', function () {
itemModalObj = new bootstrap.Modal(document.getElementById('itemModal'));
const pictureInput = document.getElementById('item_picture');
if (pictureInput) {
pictureInput.addEventListener('change', function () {
const file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
renderItemImagePreview(document.getElementById('item_existing_image_url').value || '');
return;
}
const reader = new FileReader();
reader.onload = function (event) {
renderItemImagePreview((event.target && event.target.result) ? event.target.result : '');
};
reader.readAsDataURL(file);
});
}
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearchBtn');
let searchTimeout;
@ -655,7 +884,27 @@ function openImportModal() {
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 = '', in_catalog = 0) {
function renderItemImagePreview(imageUrl = '') {
const preview = document.getElementById('itemPicturePreview');
if (!preview) return;
preview.innerHTML = '';
if (imageUrl) {
const img = document.createElement('img');
img.src = imageUrl;
img.alt = '<?= h(tr('معاينة صورة الصنف', 'Item image preview')) ?>';
preview.appendChild(img);
return;
}
const placeholder = document.createElement('div');
placeholder.className = 'item-image-placeholder';
placeholder.innerHTML = '<i class="bi bi-card-image fs-2"></i><span><?= h(tr('لا توجد صورة محددة بعد', 'No image selected yet')) ?></span>';
preview.appendChild(placeholder);
}
function openItemModal(sku = '', name = '', price = '', cost_price = '', base_stock = '', vat = '<?= h(get_setting('vat_percentage', 5)) ?>', category_id = '', supplier_id = '', unit_id = '', image_url = '', in_catalog = 0, notes = '') {
document.getElementById('item_original_sku').value = sku;
document.getElementById('item_existing_image_url').value = image_url;
document.getElementById('item_sku').value = sku;
@ -667,22 +916,11 @@ function openItemModal(sku = '', name = '', price = '', cost_price = '', base_st
document.getElementById('item_category').value = category_id;
document.getElementById('item_supplier').value = supplier_id;
document.getElementById('item_unit').value = unit_id;
document.getElementById('item_notes').value = notes || '';
document.getElementById('item_in_catalog').checked = (parseInt(in_catalog) === 1);
// 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 = '';
renderItemImagePreview(image_url || '');
itemModalObj.show();
}
@ -705,6 +943,7 @@ async function handleItemSubmit(e) {
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('notes', document.getElementById('item_notes').value);
formData.append('in_catalog', document.getElementById('item_in_catalog').checked ? '1' : '0');
formData.append('existing_image_url', document.getElementById('item_existing_image_url').value);