581 lines
22 KiB
PHP
581 lines
22 KiB
PHP
<?php
|
|
session_start();
|
|
require '../config.php';
|
|
require '../include_header.php';
|
|
?>
|
|
|
|
<body>
|
|
<?php require '../include_topbar.php';?>
|
|
<?php require '../include_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="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
|
|
<div>
|
|
<h1 class="fs-3 mb-1">Rack Occupancy</h1>
|
|
<p class="mb-0">View occupied and available rack positions across all warehouses</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary stats -->
|
|
<div class="row g-5 mb-5">
|
|
<div class="col-xl-3 col-md-6 col-12">
|
|
<div class="card">
|
|
<div class="card-body p-6">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
|
|
<i class="ti ti-box-seam fs-4"></i>
|
|
</div>
|
|
<div>
|
|
<span>Total Racks</span>
|
|
<h2 class="fw-bold mb-0" id="stat_total">—</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 col-12">
|
|
<div class="card">
|
|
<div class="card-body p-6">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<div class="icon-shape icon-lg bg-danger bg-opacity-10 text-danger rounded-2">
|
|
<i class="ti ti-lock fs-4"></i>
|
|
</div>
|
|
<div>
|
|
<span>Occupied</span>
|
|
<h2 class="fw-bold mb-0" id="stat_occupied">—</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 col-12">
|
|
<div class="card">
|
|
<div class="card-body p-6">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
|
|
<i class="ti ti-lock-open fs-4"></i>
|
|
</div>
|
|
<div>
|
|
<span>Available</span>
|
|
<h2 class="fw-bold mb-0" id="stat_empty">—</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-3 col-md-6 col-12">
|
|
<div class="card">
|
|
<div class="card-body p-6">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<div class="icon-shape icon-lg bg-info bg-opacity-10 text-info rounded-2">
|
|
<i class="ti ti-chart-pie fs-4"></i>
|
|
</div>
|
|
<div>
|
|
<span>Occupancy Rate</span>
|
|
<h2 class="fw-bold mb-0" id="stat_rate">—</h2>
|
|
</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-all-tab" data-bs-toggle="pill"
|
|
data-bs-target="#pills-all" type="button" role="tab">All</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="pills-occupied-tab" data-bs-toggle="pill"
|
|
data-bs-target="#pills-occupied" type="button" role="tab">Occupied</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="pills-empty-tab" data-bs-toggle="pill"
|
|
data-bs-target="#pills-empty" type="button" role="tab">Available</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- All tab -->
|
|
<div class="row g-5 mb-5 tab-pane fade show active" id="pills-all" role="tabpanel">
|
|
<div class="col-12">
|
|
<div class="card overflow-hidden">
|
|
<div class="card-body p-6">
|
|
<div class="mb-6 d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h3 class="mb-1 fs-4">All Racks</h3>
|
|
<p class="mb-0">Complete rack list across all warehouses</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-12 mb-5">
|
|
<div class="position-relative">
|
|
<input type="search" id="search_all" class="form-control ps-9"
|
|
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
|
<span class="position-absolute top-25 ms-4">
|
|
<i class="ti ti-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_all">
|
|
<thead class="table-primary border-light">
|
|
<tr>
|
|
<th>Warehouse</th>
|
|
<th class="th_loc_zone">Zone</th>
|
|
<th class="th_loc_aisle">Aisle</th>
|
|
<th class="th_loc_rack">Rack</th>
|
|
<th>SKU</th>
|
|
<th>Product</th>
|
|
<th>Status</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
<tfoot></tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Occupied tab -->
|
|
<div class="row g-5 mb-5 tab-pane fade" id="pills-occupied" role="tabpanel">
|
|
<div class="col-12">
|
|
<div class="card overflow-hidden">
|
|
<div class="card-body p-6">
|
|
<div class="mb-6 d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h3 class="mb-1 fs-4">Occupied Racks</h3>
|
|
<p class="mb-0">Racks currently holding stock</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-12 mb-5">
|
|
<div class="position-relative">
|
|
<input type="search" id="search_occupied" class="form-control ps-9"
|
|
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
|
<span class="position-absolute top-25 ms-4">
|
|
<i class="ti ti-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_occupied">
|
|
<thead class="table-danger border-light">
|
|
<tr>
|
|
<th>Warehouse</th>
|
|
<th class="th_loc_zone">Zone</th>
|
|
<th class="th_loc_aisle">Aisle</th>
|
|
<th class="th_loc_rack">Rack</th>
|
|
<th>SKU</th>
|
|
<th>Product</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
<tfoot></tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Available tab -->
|
|
<div class="row g-5 mb-5 tab-pane fade" id="pills-empty" role="tabpanel">
|
|
<div class="col-12">
|
|
<div class="card overflow-hidden">
|
|
<div class="card-body p-6">
|
|
<div class="mb-6 d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h3 class="mb-1 fs-4">Available Racks</h3>
|
|
<p class="mb-0">Empty racks ready to receive stock</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-12 mb-5">
|
|
<div class="position-relative">
|
|
<input type="search" id="search_empty" class="form-control ps-9"
|
|
placeholder="Search by warehouse, zone, aisle, or rack…">
|
|
<span class="position-absolute top-25 ms-4">
|
|
<i class="ti ti-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_empty">
|
|
<thead class="table-success border-light">
|
|
<tr>
|
|
<th>Warehouse</th>
|
|
<th class="th_loc_zone">Zone</th>
|
|
<th class="th_loc_aisle">Aisle</th>
|
|
<th class="th_loc_rack">Rack</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
<tfoot></tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Rack Log Modal -->
|
|
<div class="modal fade" id="rackLogModal" tabindex="-1" aria-labelledby="rackLogModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="rackLogModalLabel">Rack Log</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<table class="table mb-0 table-hover table-centered">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Date / Time</th>
|
|
<th>Action</th>
|
|
<th>SKU</th>
|
|
<th>User</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rack_log_body">
|
|
<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<?php require "../include_ending.php";?>
|
|
|
|
<script>
|
|
|
|
// ── Location config ───────────────────────────────────────────────────────
|
|
var advanced = false;
|
|
var label_rack = 'Location';
|
|
var label_zone = 'Zone';
|
|
var label_aisle = 'Aisle';
|
|
|
|
function load_location_config(callback) {
|
|
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_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
|
label_zone = s.location_label_zone || 'Zone';
|
|
label_aisle = s.location_label_aisle || 'Aisle';
|
|
if (callback) callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
function apply_location_labels() {
|
|
if (advanced) {
|
|
$('.th_loc_zone').text(label_zone).show();
|
|
$('.th_loc_aisle').text(label_aisle).show();
|
|
$('.th_loc_rack').text(label_rack);
|
|
} else {
|
|
$('.th_loc_zone').hide();
|
|
$('.th_loc_aisle').hide();
|
|
$('.th_loc_rack').text(label_rack);
|
|
}
|
|
var loc_hint = advanced
|
|
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_rack.toLowerCase()
|
|
: label_rack.toLowerCase();
|
|
$('#search_all, #search_occupied').attr('placeholder',
|
|
'Search by warehouse, ' + loc_hint + ', or SKU…');
|
|
$('#search_empty').attr('placeholder',
|
|
'Search by warehouse or ' + loc_hint + '…');
|
|
$('#rackLogModalLabel').text(label_rack + ' Log');
|
|
$('#stat_label_racks').text('Total ' + label_rack + 's');
|
|
}
|
|
|
|
// Returns 1 or 3 <td> cells depending on mode
|
|
function location_cells(item) {
|
|
if (advanced) {
|
|
return '<td class="py-3">' + (item.zone || '—') + '</td>'
|
|
+ '<td class="py-3">' + (item.aisle || '—') + '</td>'
|
|
+ '<td class="py-3">' + (item.rack || '—') + '</td>';
|
|
}
|
|
return '<td class="py-3">' + (item.rack || '—') + '</td>';
|
|
}
|
|
|
|
// Rack log modal label
|
|
function rack_label(item) {
|
|
return advanced
|
|
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.rack
|
|
: item.warehouse_name + ' ' + item.rack;
|
|
}
|
|
|
|
var search_all = '';
|
|
var search_occupied = '';
|
|
var search_empty = '';
|
|
|
|
|
|
// ── Fetch ─────────────────────────────────────────────────────────────
|
|
function retrieve_rack_occupancy() {
|
|
|
|
return ajax_request({
|
|
url: "<?php echo $server_url?>reports/api/engine_report/rack_occupancy.php",
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
debugMode: 0,
|
|
onSuccess: function(res) {
|
|
|
|
// Stats
|
|
var total = res.total || 0;
|
|
var occupied = res.occupied || 0;
|
|
var empty = res.empty || 0;
|
|
var rate = total > 0 ? Math.round((occupied / total) * 100) : 0;
|
|
|
|
$('#stat_total').text(format_number(total));
|
|
$('#stat_occupied').text(format_number(occupied));
|
|
$('#stat_empty').text(format_number(empty));
|
|
$('#stat_rate').text(rate + '%');
|
|
|
|
// Store in alasql
|
|
alasql('CREATE TABLE IF NOT EXISTS rack_all');
|
|
alasql.tables.rack_all.data = res.output || [];
|
|
|
|
change_page_all(1);
|
|
change_page_occupied(1);
|
|
change_page_empty(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ── Render: All ───────────────────────────────────────────────────────
|
|
function change_page_all(page_num) {
|
|
|
|
var offset = (page_num - 1) * prop_limit;
|
|
var kw = search_all.toLowerCase();
|
|
|
|
var filtered = kw ? alasql(
|
|
`SELECT * FROM rack_all
|
|
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
|
|
OR LOWER(zone) LIKE '%${kw}%'
|
|
OR LOWER(aisle) LIKE '%${kw}%'
|
|
OR LOWER(rack) LIKE '%${kw}%'
|
|
OR LOWER(product_sku) LIKE '%${kw}%'
|
|
OR LOWER(product_name) LIKE '%${kw}%'`
|
|
) : alasql('SELECT * FROM rack_all');
|
|
|
|
$('table#tbl_all tfoot').html(generate_pagination('tbl_all', filtered.length));
|
|
|
|
if (filtered.length === 0) {
|
|
$('table#tbl_all > tbody').html(
|
|
'<tr><td colspan="7" class="text-center py-5 text-muted">No racks match your search.</td></tr>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
|
var badge = item.status === 'occupied'
|
|
? '<span class="badge bg-danger">Occupied</span>'
|
|
: '<span class="badge bg-success">Available</span>';
|
|
body += `<tr>
|
|
<td class="py-3">${item.warehouse_name}</td>
|
|
` + location_cells(item) + `
|
|
<td class="py-3">${item.product_sku || '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3">${badge}</td>
|
|
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
|
</tr>`;
|
|
});
|
|
$('table#tbl_all > tbody').html(body);
|
|
}
|
|
|
|
|
|
// ── Render: Occupied ─────────────────────────────────────────────────
|
|
function change_page_occupied(page_num) {
|
|
|
|
var offset = (page_num - 1) * prop_limit;
|
|
var kw = search_occupied.toLowerCase();
|
|
|
|
var base = alasql("SELECT * FROM rack_all WHERE status = 'occupied'");
|
|
|
|
var filtered = kw ? base.filter(function(item) {
|
|
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
|
|| (item.zone || '').toLowerCase().includes(kw)
|
|
|| (item.aisle || '').toLowerCase().includes(kw)
|
|
|| (item.rack || '').toLowerCase().includes(kw)
|
|
|| (item.product_sku || '').toLowerCase().includes(kw)
|
|
|| (item.product_name || '').toLowerCase().includes(kw);
|
|
}) : base;
|
|
|
|
$('table#tbl_occupied tfoot').html(generate_pagination('tbl_occupied', filtered.length));
|
|
|
|
if (filtered.length === 0) {
|
|
$('table#tbl_occupied > tbody').html(
|
|
'<tr><td colspan="6" class="text-center py-5 text-muted">No occupied racks found.</td></tr>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
|
body += `<tr>
|
|
<td class="py-3">${item.warehouse_name}</td>
|
|
` + location_cells(item) + `
|
|
<td class="py-3">${item.product_sku}</td>
|
|
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
|
</tr>`;
|
|
});
|
|
$('table#tbl_occupied > tbody').html(body);
|
|
}
|
|
|
|
|
|
// ── Render: Available ─────────────────────────────────────────────────
|
|
function change_page_empty(page_num) {
|
|
|
|
var offset = (page_num - 1) * prop_limit;
|
|
var kw = search_empty.toLowerCase();
|
|
|
|
var base = alasql("SELECT * FROM rack_all WHERE status = 'empty'");
|
|
|
|
var filtered = kw ? base.filter(function(item) {
|
|
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
|
|| (item.zone || '').toLowerCase().includes(kw)
|
|
|| (item.aisle || '').toLowerCase().includes(kw)
|
|
|| (item.rack || '').toLowerCase().includes(kw);
|
|
}) : base;
|
|
|
|
$('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length));
|
|
|
|
if (filtered.length === 0) {
|
|
$('table#tbl_empty > tbody').html(
|
|
'<tr><td colspan="4" class="text-center py-5 text-muted">No available racks found.</td></tr>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
|
body += `<tr>
|
|
<td class="py-3">${item.warehouse_name}</td>
|
|
` + location_cells(item) + `
|
|
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
|
</tr>`;
|
|
});
|
|
$('table#tbl_empty > tbody').html(body);
|
|
}
|
|
|
|
|
|
// ── Search ────────────────────────────────────────────────────────────
|
|
function debounce(fn, ms) {
|
|
var t;
|
|
return function() {
|
|
clearTimeout(t);
|
|
t = setTimeout(fn, ms);
|
|
};
|
|
}
|
|
|
|
$('#search_all').on('input', debounce(function() {
|
|
search_all = $(this).val().trim();
|
|
change_page_all(1);
|
|
}, 200));
|
|
|
|
$('#search_occupied').on('input', debounce(function() {
|
|
search_occupied = $(this).val().trim();
|
|
change_page_occupied(1);
|
|
}, 200));
|
|
|
|
$('#search_empty').on('input', debounce(function() {
|
|
search_empty = $(this).val().trim();
|
|
change_page_empty(1);
|
|
}, 200));
|
|
|
|
|
|
// ── Re-render on tab switch ───────────────────────────────────────────
|
|
$('#pills-occupied-tab').on('shown.bs.tab', function() { change_page_occupied(1); });
|
|
$('#pills-empty-tab').on('shown.bs.tab', function() { change_page_empty(1); });
|
|
$('#pills-all-tab').on('shown.bs.tab', function() { change_page_all(1); });
|
|
|
|
// Remember last active tab
|
|
$(document).ready(function() {
|
|
$('button.nav-link').on('click', function() {
|
|
sessionStorage.setItem('rack_nav_id', $(this).attr('id'));
|
|
});
|
|
var nav_id = sessionStorage.getItem('rack_nav_id');
|
|
if (nav_id) $(`button#${nav_id}`).trigger('click');
|
|
});
|
|
|
|
|
|
// ── Rack Log ─────────────────────────────────────────────────────────
|
|
function view_rack_log(rack_id, label) {
|
|
|
|
$('#rackLogModalLabel').text('Rack Log — ' + label);
|
|
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
|
|
$('#rackLogModal').modal('show');
|
|
|
|
ajax_request({
|
|
url: "<?php echo $server_url?>reports/api/engine_report/rack_log.php",
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
data: { "rack_id": rack_id },
|
|
onSuccess: function(res) {
|
|
|
|
if (!res.output || res.output.length === 0) {
|
|
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>');
|
|
return;
|
|
}
|
|
|
|
var action_badge = {
|
|
'occupy': '<span class="badge bg-danger">Occupy</span>',
|
|
'release': '<span class="badge bg-success">Release</span>',
|
|
'transfer_in': '<span class="badge bg-primary">Transfer In</span>',
|
|
'transfer_out': '<span class="badge bg-warning text-dark">Transfer Out</span>',
|
|
};
|
|
|
|
var body = '';
|
|
$.each(res.output, function(i, row) {
|
|
body += `<tr>
|
|
<td class="py-3">${format_date(row.dt)}</td>
|
|
<td class="py-3">${action_badge[row.action] || row.action}</td>
|
|
<td class="py-3">${row.product_sku || '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td>
|
|
</tr>`;
|
|
});
|
|
$('#rack_log_body').html(body);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ── Boot ─────────────────────────────────────────────────────────────
|
|
$(async function() {
|
|
try {
|
|
await load_location_config(apply_location_labels);
|
|
await retrieve_rack_occupancy();
|
|
} catch(e) {
|
|
console.log(e);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|