Files
wms-app/app/inventory/manage_product.php
T
2026-05-20 10:17:02 +07:00

370 lines
13 KiB
PHP

<?php
session_start();
require '../config.php';
if (!in_array($_SESSION['login_role'] ?? 'viewer', ['owner', 'admin'], true)) {
http_response_code(403);
exit('Access denied.');
}
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_master_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div class="">
<h1 class="fs-3 mb-1">Add Product</h1>
<p class="mb-0">Manage your Product items</p>
</div>
</div>
</div>
</div>
<div class="row" id="productForm">
<div class="col-12">
<div class="card">
<div class="card-body p-6">
<div class="row">
<div class="col-md-6 mb-3">
<label for="product_name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="product_name" placeholder="Enter product name" required>
</div>
<div class="col-md-3 mb-3">
<label for="sku" class="form-label">SKU</label>
<input type="text" class="form-control" id="sku" placeholder="Enter SKU" required>
</div>
<div class="col-md-3 mb-3">
<label for="barcode" class="form-label">Barcode <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" id="barcode" placeholder="e.g. 8850999001234" autocomplete="off" maxlength="64">
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" class="form-control" id="price" placeholder="0.00" step="0.01" required>
</div>
<div class="col-md-2 mb-3">
<label for="cost_price" class="form-label">Cost Price <span class="text-muted fw-normal">(COGS)</span></label>
<input type="number" class="form-control" id="cost_price" placeholder="0.00" step="0.01" min="0">
</div>
<div class="col-md-2 mb-3">
<label class="form-label">Margin</label>
<div class="input-group">
<input type="text" class="form-control" id="margin_display" placeholder="—" disabled>
<span class="input-group-text">%</span>
</div>
</div>
<div class="col-md-2 mb-3">
<label for="uom" class="form-label">UOM</label>
<input type="text" class="form-control" id="uom" placeholder="pcs"
list="uom_suggestions" autocomplete="off" maxlength="20">
<datalist id="uom_suggestions">
<option value="pcs">
<option value="box">
<option value="kg">
<option value="g">
<option value="l">
<option value="ml">
<option value="set">
<option value="roll">
<option value="pair">
<option value="sheet">
<option value="bag">
<option value="bottle">
</datalist>
</div>
<div class="col-md-2 mb-3">
<label for="min_stock" class="form-label">Min Stock</label>
<input type="number" class="form-control" id="min_stock" placeholder="0" min="0" required>
<div class="invalid-feedback" id="min_stock_error"></div>
<small class="text-muted" id="min_stock_hint">Safety floor</small>
</div>
<div class="col-md-3 mb-3">
<label for="reorder_point" class="form-label">Reorder Point</label>
<input type="number" class="form-control" id="reorder_point" placeholder="0" min="0" required>
<div class="invalid-feedback" id="reorder_point_error"></div>
<small class="text-muted" id="reorder_point_hint">Must be greater than or equal to min stock</small>
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" required> </select>
</div>
<div class="col-md-3 mb-3">
<label for="status" class="form-label">status</label>
<select class="form-select" id="status" required>
<option value="1">Active</option>
<option value="0">inactive</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="sales_account" class="form-label">Sales Account</label>
<input type="text" class="form-control account_search" id="sales_account"
placeholder="Search account..." autocomplete="off">
<input type="hidden" id="sales_account_code">
</div>
<div class="col-md-3 mb-3">
<label for="purchase_account" class="form-label">Purchase Account</label>
<input type="text" class="form-control account_search" id="purchase_account"
placeholder="Search account..." autocomplete="off">
<input type="hidden" id="purchase_account_code">
</div>
<div class="col-md-6 mb-3">
<!-- Remove d-none from file input so it shows by default -->
<div class="mb-3">
<label class="form-label">Product Image</label>
<div id="current_image_display" class="d-none">
<div class="d-flex align-items-center gap-2">
<div class='input-group'>
<span class='btn btn-outline-secondary' type='button' id='change_image_btn'>Change</span>
<input type='text' class='form-control' id='current_file_label' disabled>
</div>
</div>
</div>
<input type="file" class="form-control" id="product_image" accept="image/*">
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" rows="4"
placeholder="Enter product description"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" data-min-role="admin" onclick="manage_product();">Create</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<script>
function manage_product() {
let formData = new FormData();
// Append the file (single image, not array)
let imageFile = $('#product_image')[0].files[0];
if (imageFile) {
formData.append('product_image', imageFile);
}
formData.append('keep_files', $('#current_file_label').val())
formData.append('barcode', $('#barcode').val() || '')
formData.append('cost_price', $('#cost_price').val() || 0)
formData.append('uom', $('#uom').val() || 'pcs')
formData.append('sales_account_code', $('#sales_account_code').val() || '')
formData.append('purchase_account_code', $('#purchase_account_code').val() || '')
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/manage_product.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
formData: formData,
action: 'manage',
onSuccess: function(res) {
window.location.href = "<?php echo $server_url?>inventory/product.php";
}
});
}
function retrieve_category() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/product_category.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var option = ``;
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.category}</option>`;
})
$(`select#category`).html(option);
}
});
}
function retrieve_for_edit() {
<?php if(empty($_GET["id"])){ ?>
return false;
<?php } ?>
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/retrieve_product.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$.each(res.output, function(key, item) {
let el = $(`#${key}`);
if (el.attr('type') === 'file') return; // skip file inputs
el.val(item);
});
set_account_field('#sales_account', '#sales_account_code', res.output.sales_account_code || '');
set_account_field('#purchase_account', '#purchase_account_code', res.output.purchase_account_code || '');
// Handle image field
if (res.output.product_image) {
$('#current_file_label').val(res.output.product_image);
$('#current_image_display').removeClass('d-none'); // show filename + change btn
$('#product_image').addClass('d-none'); // hide file input
}
$('#change_image_btn').on('click', function() {
$('#current_image_display').addClass('d-none');
$('#product_image').removeClass('d-none');
});
$(`button[type=submit]`).text('Update');
$(`button[type=reset]`).hide();
update_margin();
}
});
}
$(async function() {
try {
await retrieve_category();
await load_accounts();
await retrieve_for_edit();
init_account_search_inputs(null, window.account_search_accounts || []);
} catch (e) {
console.log(e);
}
});
function load_accounts() {
return ajax_request({
url: "<?php echo $server_url?>accounting/api/engine/account.php",
autoPrepare: true,
checkRequired: 0,
noLoading: true,
action: 'read',
onSuccess: function(res) {
window.account_search_accounts = res.output || [];
}
});
}
function set_account_field(input_selector, hidden_selector, account_code) {
var account = (window.account_search_accounts || []).find(function(row) {
return String(row.account_code) === String(account_code);
});
var display = account ? account.account_code + ' - ' + account.account_name : account_code;
$(input_selector)
.val(display || '')
.attr('secondary', display || '')
.attr('data-account-code', account_code || '');
$(hidden_selector).val(account_code || '');
}
$(document).on('account:selected', '#sales_account', function(e, account) {
$('#sales_account_code').val(account.account_code || '');
});
$(document).on('account:selected', '#purchase_account', function(e, account) {
$('#purchase_account_code').val(account.account_code || '');
});
$(document).on('account:cleared', '#sales_account', function() {
$('#sales_account_code').val('');
});
$(document).on('account:cleared', '#purchase_account', function() {
$('#purchase_account_code').val('');
});
function validate_stock_thresholds() {
var min_stock = parseFloat($('#min_stock').val() || 0);
var reorder_point = parseFloat($('#reorder_point').val() || 0);
// Reset both fields to clean state
$('#min_stock, #reorder_point').removeClass('is-invalid');
$('#min_stock_error, #reorder_point_error').text('');
$('#min_stock_hint, #reorder_point_hint').show();
// Helper: mark a field invalid and hide its hint
function flag(field, msg) {
$('#' + field).addClass('is-invalid');
$('#' + field + '_error').text(msg);
$('#' + field + '_hint').hide();
}
if (min_stock < 0) {
flag('min_stock', 'Cannot be negative.');
return false;
}
if (reorder_point < 0) {
flag('reorder_point', 'Cannot be negative.');
return false;
}
if (min_stock > reorder_point) {
flag('reorder_point', 'Must be greater than or equal to min stock (' + min_stock + ').');
return false;
}
return true;
}
// Live feedback as the user types
$('#min_stock, #reorder_point').on('input', validate_stock_thresholds);
function update_margin() {
var price = parseFloat($('#price').val()) || 0;
var cost_price = parseFloat($('#cost_price').val()) || 0;
if (price > 0) {
var margin = (price - cost_price) / price * 100;
$('#margin_display').val(margin.toFixed(2));
} else {
$('#margin_display').val('');
}
}
$('#price, #cost_price').on('input', update_margin);
</script>
</body>
</html>