accounting workflows

This commit is contained in:
Thanakorn S
2026-05-20 10:17:02 +07:00
parent 04a683bd02
commit 7396db6ffc
199 changed files with 23539 additions and 5345 deletions
+375 -8
View File
@@ -8,24 +8,168 @@
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row mb-6">
<div class="row">
<div class="col-12">
<h1 class="fs-3 mb-1">Accounting Dashboard</h1>
<p class="mb-0 text-muted">Financial overview</p>
<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">Accounting Dashboard</h1>
<p class="mb-0">Financial overview from posted journal entries</p>
</div>
</div>
</div>
</div>
<!-- Placeholder -->
<!-- Summary cards -->
<div class="row g-5 mb-5">
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4 bg-info bg-opacity-10 border border-info border-opacity-25 rounded-2" style="position:relative;">
<div class="d-flex gap-3">
<div class="icon-shape icon-md bg-info text-white rounded-2">
<i class="ti ti-trending-up fs-4"></i>
</div>
<div>
<h2 class="mb-3 fs-6">Revenue</h2>
<h3 class="fw-bold mb-0" id="stat_revenue">—</h3>
<p class="text-info mb-0 small">Posted entries</p>
</div>
</div>
<div class="card-loader-overlay" id="overlay_revenue">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4 bg-danger bg-opacity-10 border border-danger border-opacity-25 rounded-2" style="position:relative;">
<div class="d-flex gap-3">
<div class="icon-shape icon-md bg-danger text-white rounded-2">
<i class="ti ti-trending-down fs-4"></i>
</div>
<div>
<h2 class="mb-3 fs-6">Expenses</h2>
<h3 class="fw-bold mb-0" id="stat_expense">—</h3>
<p class="text-danger mb-0 small">Posted entries</p>
</div>
</div>
<div class="card-loader-overlay" id="overlay_expense">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4 bg-success bg-opacity-10 border border-success border-opacity-25 rounded-2" style="position:relative;">
<div class="d-flex gap-3">
<div class="icon-shape icon-md bg-success text-white rounded-2">
<i class="ti ti-calculator fs-4"></i>
</div>
<div>
<h2 class="mb-3 fs-6">Net Profit</h2>
<h3 class="fw-bold mb-0" id="stat_net">—</h3>
<p class="text-success mb-0 small">Revenue minus expenses</p>
</div>
</div>
<div class="card-loader-overlay" id="overlay_net">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4 bg-warning bg-opacity-10 border border-warning border-opacity-25 rounded-2" style="position:relative;">
<div class="d-flex gap-3">
<div class="icon-shape icon-md bg-warning text-white rounded-2">
<i class="ti ti-file-description fs-4"></i>
</div>
<div>
<h2 class="mb-3 fs-6">Journal Entries</h2>
<h3 class="fw-bold mb-0" id="stat_journals">—</h3>
<p class="text-warning mb-0 small">Posted entries</p>
</div>
</div>
<div class="card-loader-overlay" id="overlay_journals">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<!-- Posting window banner -->
<div class="row mb-5">
<div class="col-12">
<div class="card" style="position:relative;">
<div class="card-body py-3 d-flex align-items-center gap-3 flex-wrap">
<i class="ti ti-lock text-muted"></i>
<span id="posting_window_banner" class="small text-muted">Loading...</span>
<a href="<?php echo $server_url?>accounting/posting_window.php" class="ms-auto small">Manage Posting Window</a>
</div>
<div class="card-loader-overlay" id="overlay_lock">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<!-- Trend chart + Source breakdown -->
<div class="row g-5 mb-5">
<div class="col-lg-8">
<div class="card h-100" style="position:relative;">
<div class="card-header bg-white border-0 pb-0 pt-4 px-4">
<h6 class="mb-0 fw-semibold">Revenue vs Expenses <small class="text-muted fw-normal">(last 6 months)</small></h6>
</div>
<div class="card-body px-4 pb-4">
<canvas id="trend_chart" height="90"></canvas>
</div>
<div class="card-loader-overlay" id="overlay_trend">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100" style="position:relative;">
<div class="card-header bg-white border-0 pb-0 pt-4 px-4">
<h6 class="mb-0 fw-semibold">Entries by Type</h6>
</div>
<div class="card-body d-flex flex-column gap-3 px-4 pb-4" id="by_source_list">
<div class="text-center text-muted small py-4">Loading...</div>
</div>
<div class="card-loader-overlay" id="overlay_source">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<!-- Recent journal entries -->
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-calculator fs-1 mb-3 d-block"></i>
<p class="mb-0">Accounting dashboard coming soon.</p>
<div class="card overflow-hidden" style="position:relative;">
<div class="card-body p-4 d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-semibold">Recent Journal Entries</h6>
<a href="<?php echo $server_url?>journal/index.php" class="small">View All</a>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered">
<thead class="table-primary border-light">
<tr>
<th>Date</th>
<th>Period</th>
<th>Type</th>
<th>Reference</th>
<th>Description</th>
<th class="text-end">Debit</th>
<th class="text-end">Credit</th>
</tr>
</thead>
<tbody id="recent_tbody">
<tr><td colspan="7" class="text-center text-muted py-4 small">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="card-loader-overlay" id="overlay_recent">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
@@ -35,5 +179,228 @@
<?php require '../include_ending.php'; ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
var trend_chart_instance = null;
var source_labels = {
invoice: 'Invoice',
credit_note: 'Credit Note',
purchase_invoice: 'Purchase Invoice',
supplier_credit_note: 'Supplier Credit Note',
receipt: 'Receipt',
payment: 'Payment',
manual: 'Manual'
};
var source_badges = {
invoice: 'bg-success-subtle text-success',
credit_note: 'bg-warning-subtle text-warning',
purchase_invoice: 'bg-primary-subtle text-primary',
supplier_credit_note: 'bg-secondary-subtle text-secondary',
receipt: 'bg-info-subtle text-info',
payment: 'bg-danger-subtle text-danger',
manual: 'bg-dark-subtle text-dark'
};
function fmt_num(n) {
return Number(n).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2});
}
function show_overlays() {
$('#overlay_revenue, #overlay_expense, #overlay_net, #overlay_journals, #overlay_lock, #overlay_trend, #overlay_source, #overlay_recent').show();
}
function set_dashboard_loading() {
show_overlays();
['stat_revenue', 'stat_expense', 'stat_net', 'stat_journals'].forEach(function(id) {
document.getElementById(id).textContent = '—';
});
document.getElementById('posting_window_banner').textContent = 'Loading...';
document.getElementById('by_source_list').innerHTML =
'<div class="text-center text-muted small py-4">Loading...</div>';
document.getElementById('recent_tbody').innerHTML =
'<tr><td colspan="7" class="text-center text-muted py-4 small">Loading...</td></tr>';
}
function load_dashboard() {
set_dashboard_loading();
// Revenue, Expenses, Net Profit
ajax_request({
url: server_url + 'ac_dashboard/api/engine/pl.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) {
var d = res.output;
document.getElementById('stat_revenue').textContent = fmt_num(d.revenue);
document.getElementById('stat_expense').textContent = fmt_num(d.expense);
$('#overlay_revenue, #overlay_expense').hide();
var net = d.net_profit;
var net_el = document.getElementById('stat_net');
net_el.textContent = fmt_num(net);
var net_card = net_el.closest('.card');
if (net_card) {
net_card.className = net >= 0
? 'card p-4 bg-success bg-opacity-10 border border-success border-opacity-25 rounded-2'
: 'card p-4 bg-danger bg-opacity-10 border border-danger border-opacity-25 rounded-2';
var icon = net_card.querySelector('.icon-shape');
if (icon) icon.className = net >= 0
? 'icon-shape icon-md bg-success text-white rounded-2'
: 'icon-shape icon-md bg-danger text-white rounded-2';
var sub = net_card.querySelector('p');
if (sub) sub.className = net >= 0 ? 'text-success mb-0 small' : 'text-danger mb-0 small';
}
$('#overlay_net').hide();
}
});
// Journal Entries count
ajax_request({
url: server_url + 'ac_dashboard/api/engine/journals.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) {
document.getElementById('stat_journals').textContent = res.output.journal_count;
$('#overlay_journals').hide();
}
});
// Posting window banner
ajax_request({
url: server_url + 'ac_dashboard/api/engine/posting_window.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) { render_posting_window(res.output); }
});
// Revenue vs Expenses trend chart
ajax_request({
url: server_url + 'ac_dashboard/api/engine/trend.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) { render_trend(res.output.trend); }
});
// Entries by type breakdown
ajax_request({
url: server_url + 'ac_dashboard/api/engine/by_source.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) { render_by_source(res.output.by_source); }
});
// Recent journal entries table
ajax_request({
url: server_url + 'ac_dashboard/api/engine/recent.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, action: 'read',
onSuccess: function(res) { render_recent(res.output.recent); }
});
}
function render_posting_window(window_data) {
var el = document.getElementById('posting_window_banner');
var from = window_data.open_from || '';
var to = window_data.open_to || '';
if (!from && !to) {
el.innerHTML = '<span class="text-success fw-semibold">No restrictions</span> — all dates are open for posting and inventory movement.';
} else {
var f = from ? '<strong>' + escape_html(from) + '</strong>' : '<span class="text-muted">no lower limit</span>';
var t = to ? '<strong>' + escape_html(to) + '</strong>' : '<span class="text-muted">no upper limit</span>';
el.innerHTML = 'Posting open from ' + f + ' to ' + t;
}
$('#overlay_lock').hide();
}
function render_trend(trend) {
// Build exactly 6 month labels (last 6 months ending today), fill zeros for missing
var labels = [], revenues = [], expenses = [];
var idx = {};
trend.forEach(function(r){ idx[r.period] = r; });
for (var i = 5; i >= 0; i--) {
var d = new Date();
d.setDate(1);
d.setMonth(d.getMonth() - i);
var m = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
labels.push(m);
revenues.push(idx[m] ? parseFloat(idx[m].revenue) || 0 : 0);
expenses.push(idx[m] ? parseFloat(idx[m].expense) || 0 : 0);
}
if (trend_chart_instance) trend_chart_instance.destroy();
var ctx = document.getElementById('trend_chart').getContext('2d');
trend_chart_instance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{ label: 'Revenue', data: revenues, backgroundColor: 'rgba(2,132,199,0.75)', borderRadius: 4 },
{ label: 'Expenses', data: expenses, backgroundColor: 'rgba(239,68,68,0.65)', borderRadius: 4 },
]
},
options: {
responsive: true,
plugins: { legend: { position: 'top' } },
scales: {
y: { beginAtZero: true, ticks: { callback: function(v){ return fmt_num(v); } } }
}
}
});
$('#overlay_trend').hide();
}
function render_by_source(rows) {
var el = document.getElementById('by_source_list');
if (!rows || rows.length === 0) {
el.innerHTML = '<div class="text-center text-muted small py-4">No entries found.</div>';
$('#overlay_source').hide();
return;
}
var total = rows.reduce(function(s, r){ return s + parseInt(r.cnt); }, 0);
var html = '';
rows.forEach(function(r) {
var pct = total > 0 ? Math.round(r.cnt / total * 100) : 0;
var label = source_labels[r.source_type] || r.source_type;
html += '<div>' +
'<div class="d-flex justify-content-between small mb-1">' +
'<span>' + escape_html(label) + '</span>' +
'<span class="text-muted">' + r.cnt + ' <span class="text-muted">(' + pct + '%)</span></span>' +
'</div>' +
'<div class="progress" style="height:6px;">' +
'<div class="progress-bar" style="width:' + pct + '%;background:#0284c7;"></div>' +
'</div>' +
'</div>';
});
el.innerHTML = html;
$('#overlay_source').hide();
}
function render_recent(rows) {
var tbody = document.getElementById('recent_tbody');
if (!rows || rows.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4 small">No journal entries found.</td></tr>';
$('#overlay_recent').hide();
return;
}
var html = '';
rows.forEach(function(r) {
var badge_cls = source_badges[r.source_type] || 'bg-light text-dark';
var label = source_labels[r.source_type] || r.source_type;
var jdate = r.journal_date || '—';
html += '<tr>' +
'<td class="small">' + escape_html(jdate) + '</td>' +
'<td class="small">' + escape_html(r.period) + '</td>' +
'<td><span class="badge ' + badge_cls + '">' + escape_html(label) + '</span></td>' +
'<td class="small">' + escape_html(r.reference || '—') + '</td>' +
'<td class="small text-muted">' + escape_html(r.description || '—') + '</td>' +
'<td class="small text-end">' + fmt_num(r.total_debit) + '</td>' +
'<td class="small text-end">' + fmt_num(r.total_credit) + '</td>' +
'</tr>';
});
tbody.innerHTML = html;
$('#overlay_recent').hide();
}
load_dashboard();
</script>
</body>
</html>