gl aggregate table (ETL), cronjob by NODEJS

This commit is contained in:
Thanakorn S
2026-05-23 10:52:27 +07:00
parent 9c275eb358
commit f5ea74e134
15 changed files with 1048 additions and 230 deletions
+44
View File
@@ -0,0 +1,44 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require_once '../../../assets/utils/classes_ac/EtlManager.php';
$etl = new EtlManager($pdo2, $pdo1, $company_id);
$action = $data['action'] ?? '';
try {
if ($action === 'read') {
$answer['output'] = $etl->getStatus();
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($action === 'get_periods') {
$sth = $pdo2->prepare(
"SELECT DISTINCT period FROM td_gl WHERE company_id = :cid ORDER BY period"
);
$sth->execute([':cid' => $company_id]);
$answer['output'] = $sth->fetchAll(PDO::FETCH_COLUMN);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($action === 'rebuild_period') {
$period = $data['period'] ?? '';
if (!preg_match('/^\d{4}-\d{2}$/', $period)) {
$answer['message'] = 'Invalid period.';
exit(json_encode($answer));
}
$etl->rebuildPeriod($period);
$answer['success'] = 1;
exit(json_encode($answer));
}
$answer['message'] = 'Invalid action.';
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
+194
View File
@@ -0,0 +1,194 @@
<?php
session_start();
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 GL Summary Maintenance</h1>
<p class="mb-0">Manages the <code>etl_gl_summary</code> table used by the accounting reports</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 GL 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>
</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 entire GL summary table from raw journal entries.
Use this after a direct database fix, data migration, 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>Rebuilds one month at a time. The dashboard stays usable during the rebuild — it falls back to raw data for any period not yet in the summary.</span>
</div>
<div class="mt-auto">
<button class="btn btn-danger" id="btn_rebuild_full">
<i class="ti ti-database-import me-1"></i>Rebuild Full ETL Table
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
function load_status() {
$('#overlay_status').show();
ajax_request({
url: server_url + 'setting/api/engine/gl_maintenance.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) {
var d = res.output;
$('#stat_slot').text(pad2(d.slot_hour) + ':00 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 pad2(n) { return String(n).padStart(2, '0'); }
$('#btn_rebuild_full').on('click', function() {
bootbox.confirm({
title: 'Rebuild Full ETL Table',
message: 'This will rebuild the GL summary table one month at a time from raw journal entries. 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: server_url + 'setting/api/engine/gl_maintenance.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'get_periods',
onSuccess: function(res) {
var periods = res.output || [];
var total = periods.length;
if (total === 0) {
btn.prop('disabled', false);
bootbox.alert('No GL periods found.');
return;
}
BatchOverlay.show('Rebuilding GL Summary', 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();
return;
}
BatchOverlay.update(i, total, periods[i]);
ajax_request({
url: server_url + 'setting/api/engine/gl_maintenance.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
action: 'rebuild_period', data: { 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);
}
});
}
});
});
load_status();
</script>
</body>
</html>