Files
wms-app/app/inventory/warehouse.php
T
2026-05-27 17:14:53 +07:00

562 lines
21 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">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex align-items-center justify-content-between">
<div>
<h1 class="fs-3 mb-1">Warehouse / Location Management</h1>
<p class="mb-0">Manage your warehouses, storage locations, and capacity.</p>
</div>
</div>
</div>
</div>
<!-- Summary stats -->
<div class="row mb-5 g-5">
<div class="col-12">
<div class="card">
<div class="card-body p-0">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap">
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="total_warehouse">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Total Warehouses</span>
<small class="text-info">Across — locations</small>
</div>
<div>
<span class="text-info">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-building-warehouse">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 21v-13l9 -4l9 4v13" />
<path d="M13 13h4v8h-10v-6h6" />
<path d="M13 21v-9a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v3" />
</svg>
</span>
</div>
</div>
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="total_capacity">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Total Capacity</span>
<small class="text-warning">Bin positions available</small>
</div>
<div>
<span class="text-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-building-plus">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 21h9" />
<path d="M9 8h1" />
<path d="M9 12h1" />
<path d="M9 16h1" />
<path d="M14 8h1" />
<path d="M14 12h1" />
<path
d="M5 21v-16c0 -.53 .211 -1.039 .586 -1.414c.375 -.375 .884 -.586 1.414 -.586h10c.53 0 1.039 .211 1.414 .586c.375 .375 .586 .884 .586 1.414v7" />
<path d="M16 19h6" />
<path d="M19 16v6" />
</svg>
</span>
</div>
</div>
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="total_space_used">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Space Used</span>
<small class="text-success">Bins occupied</small>
</div>
<div>
<span class="text-success">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-package">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
<path d="M12 12l8 -4.5" />
<path d="M12 12l0 9" />
<path d="M12 12l-8 -4.5" />
<path d="M16 5.25l-8 4.5" />
</svg>
</span>
</div>
</div>
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="total_product">
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Total Products</span>
<small class="text-primary">Products stored</small>
</div>
<div>
<span class="text-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-map-pin">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
<path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z" />
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-pills mb-3 bg-light p-2 rounded-2 d-inline-flex" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-warehouse-tab" data-bs-toggle="pill"
data-bs-target="#pills-warehouse" type="button" role="tab" aria-controls="pills-warehouse"
aria-selected="true">warehouse</button>
</li>
<li class="nav-item nav-item-secondary" role="presentation">
<button class="nav-link" id="pills-storage-tab" data-bs-toggle="pill" data-bs-target="#pills-storage"
type="button" role="tab" aria-controls="pills-storage" aria-selected="false">storage</button>
</li>
</ul>
<!-- Warehouses tab -->
<div class="row g-5 mb-5 tab-pane fade show active" id="pills-warehouse" role="tabpanel"
aria-labelledby="pills-warehouse-tab">
<div class="col-12">
<div class="card">
<div class="card-body p-5 justify-content-between">
<div class="mb-6 d-flex align-items-center justify-content-between">
<div>
<h2 class="fs-4 mb-2">Warehouses</h2>
<p>All warehouse facilities and their current status</p>
</div>
<div>
<a href="<?php echo $server_url?>inventory/manage_warehouse.php" class="btn btn-primary" data-min-role="admin">
Add Warehouse
</a>
</div>
</div>
<!-- Search bar -->
<div class="col-lg-12 mb-5">
<div class="position-relative">
<input type="search" id="search_warehouse" class="form-control ps-9"
placeholder="Search by warehouse name, location, or manager…">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<!-- Table -->
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="warehouse">
<thead class="table-primary border-primary">
<tr>
<th class="py-3">Warehouse Name</th>
<th class="py-3">Location</th>
<th class="py-3">Manager</th>
<th class="py-3">Capacity</th>
<th class="py-3">Products</th>
<th class="py-3">Status</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Storage tab -->
<div class="row g-5 mb-5 tab-pane fade" id="pills-storage" role="tabpanel" aria-labelledby="pills-storage-tab">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="mb-6 d-flex align-items-center justify-content-between">
<div>
<h2 class="fs-4 mb-2">Storage Locations</h2>
<p id="storage_subtitle">Zones, aisles, and bin assignments</p>
</div>
<div>
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary" data-min-role="admin">
Add Storage
</a>
</div>
</div>
<!-- Search bar -->
<div class="col-lg-12 mb-5">
<div class="position-relative">
<input type="search" id="search_storage" class="form-control ps-9"
placeholder="Search by warehouse or zone…">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<!-- Table -->
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="storage">
<thead class="table-secondary">
<tr>
<th class="py-3">Warehouse</th>
<th class="py-3" id="th_level1">Zone</th>
<th class="py-3" id="th_level2">Aisle Range</th>
<th class="py-3" id="th_level3">Bin Range</th>
<th class="py-3">Items Stored</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<script>
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
function load_company_setting() {
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var s = res.output;
advanced = s.advanced_location == 1;
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
}
});
}
function apply_location_labels() {
// Stats card subtitles
$('#total_capacity small').text(label_bin + ' positions available');
$('#total_space_used small').text(label_bin + 's occupied');
// Storage tab subtitle + search
$('#storage_subtitle').text(advanced
? label_zone + 's, ' + label_aisle + 's, and ' + label_bin + ' assignments'
: label_bin + ' assignments');
$('#search_storage').attr('placeholder', advanced
? 'Search by warehouse or ' + label_zone.toLowerCase() + '…'
: 'Search by warehouse or ' + label_bin.toLowerCase() + '…');
// Table headers
if (advanced) {
$('#th_level1').text(label_zone).show();
$('#th_level2').text(label_aisle + ' Range').show();
$('#th_level3').text(label_bin + ' Range');
} else {
$('#th_level1').hide();
$('#th_level2').hide();
$('#th_level3').text(label_bin + ' Range');
}
}
// ── Current search terms (read by change_page_* when paginating) ─────
var search_term_warehouse = '';
var search_term_storage = '';
// ── Stats card ───────────────────────────────────────────────────────
function retrieve_stats() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse_stats.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$("#total_warehouse span.fs-2").text(res.total_warehouse);
$("#total_warehouse small.text-info").text(`Across ${res.total_location} locations`);
$("#total_capacity span.fs-2").text(format_number(res.total_capacity, 0));
$("#total_space_used span.fs-2").text(format_number(res.space_used, 0));
var pct = res.total_capacity > 0 ?
Math.round(res.space_used / res.total_capacity * 100) :
0;
$("#total_space_used small").text(`${pct}% utilized`);
$("#total_product span.fs-2").text(res.total_product);
}
});
}
// ── Warehouse: fetch + render ────────────────────────────────────────
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
alasql('CREATE TABLE IF NOT EXISTS output');
alasql.tables.output.data = res.output || [];
change_page_warehouse(1);
}
});
}
function change_page_warehouse(page_num) {
var offset = (page_num - 1) * prop_limit;
var kw = search_term_warehouse.toLowerCase();
// Filter in-memory, then paginate
var filtered = kw ?
alasql(`SELECT * FROM output
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(location) LIKE '%${kw}%'
OR LOWER(manager_name) LIKE '%${kw}%'
OR LOWER(manager_surname) LIKE '%${kw}%'`) :
alasql(`SELECT * FROM output`);
// Pagination reflects the filtered total
var page = generate_pagination('warehouse', filtered.length);
$(`table#warehouse tfoot`).html(page);
var page_data = filtered.slice(offset, offset + prop_limit);
if (filtered.length === 0) {
$("table#warehouse > tbody").html(
'<tr><td colspan="7" class="text-center py-5 text-muted">No warehouses match your search.</td></tr>'
);
return;
}
var body = '';
$.each(page_data, function(key, item) {
body += `<tr>
<td class="py-3">${item['warehouse_name']}</td>
<td class="py-3">${item['location']}</td>
<td class="py-3">${item['manager_name']} ${item['manager_surname']}</td>
<td class="py-3" style='text-align: right;'>${format_number(item['capacity'], 2)}</td>
<td class="py-3">${item['unique_product']}</td>
<td class="py-3">
${(item['status']==1)?'<span class="badge bg-success">Active</span>':'<span class="badge bg-secondary">Inactive</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_warehouse.php?id=${item['id']}" class="" data-min-role="admin"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" data-min-role="admin" onclick="remove_warehouse(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>`;
});
$("table#warehouse > tbody").html(body);
}
// ── Storage: fetch + render ──────────────────────────────────────────
function retrieve_storage() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/storage.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
alasql('CREATE TABLE IF NOT EXISTS storage');
alasql.tables.storage.data = res.output || [];
change_page_storage(1);
}
});
}
function change_page_storage(page_num) {
var offset = (page_num - 1) * prop_limit;
var kw = search_term_storage.toLowerCase();
var filtered = kw ?
alasql(`SELECT * FROM storage
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(zone) LIKE '%${kw}%'
OR LOWER(bin) LIKE '%${kw}%'`) :
alasql(`SELECT * FROM storage`);
var page = generate_pagination('storage', filtered.length);
$(`table#storage tfoot`).html(page);
var page_data = filtered.slice(offset, offset + prop_limit);
if (filtered.length === 0) {
$("table#storage > tbody").html(
'<tr><td colspan="6" class="text-center py-5 text-muted">No storage locations match your search.</td></tr>'
);
return;
}
var body = '';
$.each(page_data, function(key, item) {
var location_cells = advanced
? `<td class="py-3">${item['zone']}</td>
<td class="py-3">${item['aisle_from']} - ${item['aisle_to']}</td>
<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`
: `<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`;
body += `<tr>
<td class="py-3">${item['warehouse_name']}</td>
${location_cells}
<td class="py-3"></td>
<td class="py-3">
<a href="<?php echo $server_url?>inventory/manage_storage.php?id=${item['id']}" class="" data-min-role="admin"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger" data-min-role="admin" onclick="remove_storage(${item['id']},$(this))"><i class="ti ti-trash ms-2 fs-5"></i></a>
</td>
</tr>`;
});
$("table#storage > tbody").html(body);
}
// ── Remove handlers ──────────────────────────────────────────────────
function remove_warehouse(id, element) {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/remove_warehouse.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
data: {
id: id
},
action: 'delete',
onSuccess: function(res) {
element.parent().parent().remove();
}
});
}
function remove_storage(id, element) {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/remove_storage.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
data: {
id: id
},
action: 'delete',
onSuccess: function(res) {
element.parent().parent().remove();
}
});
}
// ── Search: debounce + re-render ─────────────────────────────────────
function debounce(fn, ms) {
var t;
return function() {
var args = arguments,
ctx = this;
clearTimeout(t);
t = setTimeout(function() {
fn.apply(ctx, args);
}, ms);
};
}
$('#search_warehouse').on('input', debounce(function() {
search_term_warehouse = $(this).val().trim();
change_page_warehouse(1); // always reset to page 1 on new search
}, 200));
$('#search_storage').on('input', debounce(function() {
search_term_storage = $(this).val().trim();
change_page_storage(1);
}, 200));
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await load_company_setting();
await retrieve_stats();
await retrieve_warehouse();
await retrieve_storage();
} catch (e) {
console.log(e);
}
});
// Remember last active tab across page reloads
$(document).ready(function() {
$('button.nav-link').on('click', function() {
var nav_id = $(this).attr('id');
sessionStorage.setItem("warehouse_nav_id", nav_id);
});
var nav_id = sessionStorage.getItem("warehouse_nav_id");
if (nav_id) {
$(`button#${nav_id}`).trigger('click');
}
});
</script>
</body>
</html>