accounting workflows
This commit is contained in:
+375
-8
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user