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

566 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>
// custom.js globals: load_location_config(), format_number()
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
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>