249 lines
9.3 KiB
PHP
249 lines
9.3 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../session.php';
|
|
require '../config.php';
|
|
require '../include_header.php';
|
|
?>
|
|
|
|
<body>
|
|
<?php require '../include_topbar.php'; ?>
|
|
<?php require '../include_setting_sidebar.php'; ?>
|
|
|
|
<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-6 gap-3">
|
|
<div>
|
|
<h1 class="fs-3 mb-1">ETL Stock Summary Maintenance</h1>
|
|
<p class="mb-0">Manages the <code>etl_stock_summary</code> table used by the stock overview dashboard</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-5">
|
|
|
|
<!-- Automatic Maintenance -->
|
|
<div class="col-12 col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100" style="position:relative;">
|
|
<div class="card-header bg-transparent border-bottom">
|
|
<h6 class="mb-0"><i class="ti ti-clock me-2"></i>Automatic Maintenance</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted small mb-4">
|
|
The system automatically checks and repairs the stock summary table nightly.
|
|
Each company is assigned a fixed time slot to distribute load across the day.
|
|
</p>
|
|
<table class="table table-sm table-borderless mb-0">
|
|
<tbody>
|
|
<tr>
|
|
<td class="text-muted small" style="width:160px;">Scheduled slot</td>
|
|
<td><strong id="stat_slot">—</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted small">Last ran</td>
|
|
<td id="stat_last_ran">—</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted small">Last result</td>
|
|
<td id="stat_last_result">—</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted small">Gaps found</td>
|
|
<td id="stat_gaps_found">—</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted small">Periods rebuilt</td>
|
|
<td id="stat_periods_rebuilt">—</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="text-muted small">Next run</td>
|
|
<td id="stat_next_run">—</td>
|
|
</tr>
|
|
<tr><td colspan="2"><hr class="my-2"></td></tr>
|
|
<tr>
|
|
<td class="text-muted small">Current gaps</td>
|
|
<td id="stat_current_gaps">
|
|
<span class="spinner-border spinner-border-sm text-secondary"></span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-loader-overlay" id="overlay_status">
|
|
<div class="spinner-border spinner-border-sm text-primary"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Rebuild -->
|
|
<div class="col-12 col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-transparent border-bottom">
|
|
<h6 class="mb-0"><i class="ti ti-database me-2"></i>Manual Full Rebuild</h6>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<p class="text-muted small mb-3">
|
|
Wipes and rebuilds the stock summary for a specific warehouse, one month at a time from raw stock rows.
|
|
Use this after a direct database fix, data import, or if dashboard figures look incorrect.
|
|
</p>
|
|
<div class="alert alert-warning d-flex gap-2 align-items-start py-2 px-3 small mb-4">
|
|
<i class="ti ti-alert-triangle mt-1 flex-shrink-0"></i>
|
|
<span>The dashboard stays usable during the rebuild — it falls back to raw data for any month not yet rebuilt.</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small text-muted">Warehouse</label>
|
|
<select id="sel_warehouse" class="form-select form-select-sm">
|
|
<option value="">Loading…</option>
|
|
</select>
|
|
</div>
|
|
<div class="mt-auto">
|
|
<button class="btn btn-danger" id="btn_rebuild_full">
|
|
<i class="ti ti-database-import me-1"></i>Rebuild ETL for Selected Warehouse
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<?php require '../include_ending.php'; ?>
|
|
|
|
<script>
|
|
var API = server_url + 'setting/api/engine/stock_maintenance.php';
|
|
|
|
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
|
|
function load_status() {
|
|
$('#overlay_status').show();
|
|
ajax_request({
|
|
url: API,
|
|
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
|
|
onSuccess: function(res) {
|
|
var d = res.output;
|
|
$('#stat_slot').text(pad2(d.slot_hour) + ':30 daily');
|
|
$('#stat_last_ran').text(d.last_ran || '—');
|
|
$('#stat_last_result').html(
|
|
d.last_result === 'ok'
|
|
? '<span class="badge bg-success-subtle text-success">OK</span>'
|
|
: d.last_result
|
|
? '<span class="badge bg-danger-subtle text-danger">' + escape_html(d.last_result) + '</span>'
|
|
: '—'
|
|
);
|
|
$('#stat_gaps_found').text(d.last_result ? d.gaps_found : '—');
|
|
$('#stat_periods_rebuilt').text(d.last_result ? d.periods_rebuilt : '—');
|
|
$('#stat_next_run').text(d.next_run || '—');
|
|
$('#overlay_status').hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function load_warehouses() {
|
|
ajax_request({
|
|
url: API,
|
|
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'get_warehouses',
|
|
onSuccess: function(res) {
|
|
var opts = '<option value="">Select warehouse…</option>';
|
|
$.each(res.output || [], function(_, wh) {
|
|
opts += '<option value="' + wh.id + '">' + escape_html(wh.warehouse_name) + '</option>';
|
|
});
|
|
$('#sel_warehouse').html(opts);
|
|
}
|
|
});
|
|
}
|
|
|
|
$('#btn_rebuild_full').on('click', function() {
|
|
var warehouse_id = $('#sel_warehouse').val();
|
|
var warehouse_name = $('#sel_warehouse option:selected').text();
|
|
if (!warehouse_id) { bootbox.alert('Please select a warehouse first.'); return; }
|
|
|
|
bootbox.confirm({
|
|
title: 'Rebuild ETL — ' + warehouse_name,
|
|
message: 'This will rebuild <strong>etl_stock_summary</strong> for <strong>' + escape_html(warehouse_name) + '</strong> one month at a time from raw stock rows. Continue?',
|
|
buttons: {
|
|
confirm: { label: 'Rebuild', className: 'btn-danger' },
|
|
cancel: { label: 'Cancel', className: 'btn-secondary' }
|
|
},
|
|
callback: function(confirmed) {
|
|
if (!confirmed) return;
|
|
|
|
var btn = $('#btn_rebuild_full').prop('disabled', true);
|
|
|
|
ajax_request({
|
|
url: API,
|
|
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
|
|
action: 'get_periods', data: { warehouse_id: warehouse_id },
|
|
onSuccess: function(res) {
|
|
var periods = res.output || [];
|
|
var total = periods.length;
|
|
|
|
if (total === 0) {
|
|
btn.prop('disabled', false);
|
|
bootbox.alert('No approved stock periods found for this warehouse.');
|
|
return;
|
|
}
|
|
|
|
BatchOverlay.show('Rebuilding Stock ETL — ' + warehouse_name, total, { unit: 'periods', color: '#dc3545' });
|
|
|
|
function rebuild_next(i) {
|
|
if (i >= total) {
|
|
BatchOverlay.done(total + ' period(s) rebuilt successfully.', function() {
|
|
btn.prop('disabled', false);
|
|
});
|
|
load_status();
|
|
load_current_gaps();
|
|
return;
|
|
}
|
|
|
|
BatchOverlay.update(i, total, periods[i]);
|
|
|
|
ajax_request({
|
|
url: API,
|
|
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
|
|
action: 'rebuild_period', data: { warehouse_id: warehouse_id, period: periods[i] },
|
|
onSuccess: function() { rebuild_next(i + 1); },
|
|
onError: function() { rebuild_next(i + 1); }
|
|
});
|
|
}
|
|
|
|
rebuild_next(0);
|
|
},
|
|
onError: function() {
|
|
BatchOverlay.hide();
|
|
btn.prop('disabled', false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
function load_current_gaps() {
|
|
ajax_request({
|
|
url: API,
|
|
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'get_current_gaps',
|
|
onSuccess: function(res) {
|
|
var n = res.output.current_gaps;
|
|
$('#stat_current_gaps').html(
|
|
n === 0
|
|
? '<span class="badge bg-success-subtle text-success">0 — up to date</span>'
|
|
: '<span class="badge bg-warning-subtle text-warning">' + n + ' period(s) need repair</span>'
|
|
);
|
|
},
|
|
onError: function() {
|
|
$('#stat_current_gaps').text('—');
|
|
}
|
|
});
|
|
}
|
|
|
|
load_status();
|
|
load_warehouses();
|
|
load_current_gaps();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|