accounting workflows
This commit is contained in:
+183
-7
@@ -13,24 +13,200 @@
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">Journal List</h1>
|
||||
<p class="mb-0 text-muted">All journal entries</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Journal Entries</h1>
|
||||
<p class="mb-0 text-muted">All posted general ledger entries</p>
|
||||
</div>
|
||||
<a href="<?php echo $server_url?>journal/new.php" class="btn btn-primary">
|
||||
<i class="ti ti-plus me-1"></i>New Manual Entry
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-books fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">Journal List coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Period</label>
|
||||
<input type="month" id="f_period" class="form-control form-control-sm" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Source Type</label>
|
||||
<select id="f_source" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="">All Types</option>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="credit_note">Credit Note</option>
|
||||
<option value="purchase_invoice">Purchase Invoice</option>
|
||||
<option value="supplier_credit_note">Supplier Credit Note</option>
|
||||
<option value="receipt">Receipt</option>
|
||||
<option value="payment">Payment</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_listing()">
|
||||
<i class="ti ti-search me-1"></i>Load
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<span id="jl_badge" class="text-muted small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0 table-centered">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Type</th>
|
||||
<th>Contact / Note</th>
|
||||
<th>Period</th>
|
||||
<th class="text-end">Debit</th>
|
||||
<th class="text-end">Credit</th>
|
||||
<th>Posted At</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jl_tbody">
|
||||
<tr><td colspan="8" class="text-center text-muted py-5">Use filters and click Load.</td></tr>
|
||||
</tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- GL Detail Modal -->
|
||||
<div class="modal fade" id="gl_detail_modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Journal Entry Detail</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="gl_detail_body">
|
||||
<div class="text-center text-muted py-5">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var src_labels = {
|
||||
invoice: 'Invoice', credit_note: 'Credit Note',
|
||||
purchase_invoice: 'Purchase Invoice', supplier_credit_note: 'Supplier Credit Note',
|
||||
receipt: 'Receipt', payment: 'Payment', manual: 'Manual',
|
||||
};
|
||||
var type_badge_cls = {
|
||||
invoice: 'bg-primary-subtle text-primary',
|
||||
receipt: 'bg-success-subtle text-success',
|
||||
payment: 'bg-warning-subtle text-warning',
|
||||
purchase_invoice: 'bg-danger-subtle text-danger',
|
||||
supplier_credit_note: 'bg-secondary-subtle text-secondary',
|
||||
credit_note: 'bg-info-subtle text-info',
|
||||
manual: 'bg-dark-subtle text-dark',
|
||||
};
|
||||
|
||||
function load_listing() {
|
||||
var period = document.getElementById('f_period').value;
|
||||
var source = document.getElementById('f_source').value;
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_journal_listing.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
period: period,
|
||||
source_type: source,
|
||||
onSuccess: function(res) {
|
||||
var rows = res.output || [];
|
||||
document.getElementById('jl_badge').textContent = rows.length + ' entries';
|
||||
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('jl_tbody').innerHTML =
|
||||
'<tr><td colspan="8" class="text-center text-muted py-5">No entries found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
rows.forEach(function(r) {
|
||||
var cls = type_badge_cls[r.source_type] || 'bg-secondary-subtle text-secondary';
|
||||
var label = src_labels[r.source_type] || r.source_type;
|
||||
html += '<tr>' +
|
||||
'<td class="fw-semibold">' + escape_html(r.doc_number || ('GL-' + r.id)) + '</td>' +
|
||||
'<td><span class="badge rounded-pill ' + cls + ' small">' + escape_html(label) + '</span></td>' +
|
||||
'<td class="small text-muted">' + escape_html(r.contact_name || '—') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.period) + '</td>' +
|
||||
'<td class="text-end small">' + format_number(r.total_debit) + '</td>' +
|
||||
'<td class="text-end small">' + format_number(r.total_credit) + '</td>' +
|
||||
'<td class="small text-muted">' + escape_html(r.posted_at) + '</td>' +
|
||||
'<td><a href="javascript:;" onclick="view_detail(' + r.id + ')"><i class="ti ti-eye fs-5"></i></a>' +
|
||||
(r.source_type === 'manual' ? ' <a href="' + server_url + 'journal/new.php?gl_id=' + r.id + '" class="ms-2"><i class="ti ti-edit fs-5"></i></a>' : '') +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
document.getElementById('jl_tbody').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function view_detail(gl_id) {
|
||||
document.getElementById('gl_detail_body').innerHTML = '<div class="text-center text-muted py-5">Loading...</div>';
|
||||
var modal = new bootstrap.Modal(document.getElementById('gl_detail_modal'));
|
||||
modal.show();
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_journal_detail.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
gl_id: gl_id,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var h = d.header;
|
||||
var lines = d.lines || [];
|
||||
|
||||
var lines_html = lines.map(function(l) {
|
||||
return '<tr>' +
|
||||
'<td>' + escape_html(l.account_code) + '</td>' +
|
||||
'<td>' + escape_html(l.account_name) + '</td>' +
|
||||
'<td class="text-muted small">' + escape_html(l.description || '—') + '</td>' +
|
||||
'<td class="text-end">' + (parseFloat(l.debit) ? format_number(l.debit) : '—') + '</td>' +
|
||||
'<td class="text-end">' + (parseFloat(l.credit) ? format_number(l.credit) : '—') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
||||
document.getElementById('gl_detail_body').innerHTML =
|
||||
'<div class="row g-3 mb-4">' +
|
||||
'<div class="col-6"><p class="text-muted small mb-0">Date</p><strong>' + escape_html(h.journal_date_fmt || h.journal_date || '—') + '</strong></div>' +
|
||||
'<div class="col-6"><p class="text-muted small mb-0">Period</p><strong>' + escape_html(h.period) + '</strong></div>' +
|
||||
'<div class="col-6"><p class="text-muted small mb-0">Reference</p><strong>' + escape_html(h.reference || '—') + '</strong></div>' +
|
||||
'<div class="col-6"><p class="text-muted small mb-0">Source</p><strong>' + escape_html(src_labels[h.source_type] || h.source_type) + '</strong></div>' +
|
||||
(h.description ? '<div class="col-12"><p class="text-muted small mb-0">Description</p><span>' + escape_html(h.description) + '</span></div>' : '') +
|
||||
'</div>' +
|
||||
'<table class="table table-sm">' +
|
||||
'<thead class="table-light"><tr><th>Code</th><th>Account</th><th>Description</th><th class="text-end">Debit</th><th class="text-end">Credit</th></tr></thead>' +
|
||||
'<tbody>' + lines_html + '</tbody>' +
|
||||
'</table>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-load current month on init
|
||||
var now = new Date();
|
||||
document.getElementById('f_period').value =
|
||||
now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
|
||||
load_listing();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+239
-11
@@ -11,26 +11,254 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">New Journal Entry</h1>
|
||||
<p class="mb-0 text-muted">Create a manual journal entry</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-plus fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">New Journal Entry coming soon.</p>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<a href="<?php echo $server_url?>journal/index.php" class="btn btn-light btn-sm">
|
||||
<i class="ti ti-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="fs-3 mb-0" id="page_title">New Manual Journal Entry</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<input type="hidden" id="gl_id">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Journal Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="journal_date" placeholder="d/m/Y" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Period (auto)</label>
|
||||
<input type="text" class="form-control bg-light" id="period_display" readonly placeholder="YYYY-MM">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Reference</label>
|
||||
<input type="text" class="form-control" id="reference" placeholder="e.g. MJE-001">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<input type="text" class="form-control" id="description" placeholder="Journal entry description">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Balance indicator -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center align-items-center gap-2 p-5">
|
||||
<p class="text-muted small mb-1">Debit Total</p>
|
||||
<h3 class="fw-bold mb-0 text-primary" id="bal_debit">0.00</h3>
|
||||
<p class="text-muted small mb-1 mt-3">Credit Total</p>
|
||||
<h3 class="fw-bold mb-0 text-danger" id="bal_credit">0.00</h3>
|
||||
<hr class="w-100">
|
||||
<p class="text-muted small mb-0" id="bal_status">Enter journal lines below.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal lines -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">Journal Lines</h6>
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="add_line()">
|
||||
<i class="ti ti-plus me-1"></i>Add Line
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" id="lines_table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:220px;">Account</th>
|
||||
<th>Description</th>
|
||||
<th style="width:150px;" class="text-end">Debit</th>
|
||||
<th style="width:150px;" class="text-end">Credit</th>
|
||||
<th style="width:40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lines_tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary" onclick="save_journal()">
|
||||
<i class="ti ti-device-floppy me-1"></i><span id="btn_save_text">Save Entry</span>
|
||||
</button>
|
||||
<a href="<?php echo $server_url?>journal/index.php" class="btn btn-secondary ms-2">Cancel</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var all_accounts = [];
|
||||
var line_counter = 0;
|
||||
|
||||
// ── Load accounts ─────────────────────────────────────────────────────────────
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/account.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
all_accounts = (res.output || []).filter(function(a){ return a.is_posting == 1 && a.status == 1; });
|
||||
add_line();
|
||||
add_line();
|
||||
load_for_edit();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Flatpickr date ────────────────────────────────────────────────────────────
|
||||
$(function() {
|
||||
flatpickr('#journal_date', {
|
||||
dateFormat: 'd/m/Y',
|
||||
onChange: function(sel, str) {
|
||||
if (str.length === 10) {
|
||||
var parts = str.split('/');
|
||||
document.getElementById('period_display').value = parts[2] + '-' + parts[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
// Default to today
|
||||
var t = new Date();
|
||||
var dd = String(t.getDate()).padStart(2,'0');
|
||||
var mm = String(t.getMonth()+1).padStart(2,'0');
|
||||
var yy = t.getFullYear();
|
||||
document.getElementById('journal_date').value = dd + '/' + mm + '/' + yy;
|
||||
document.getElementById('period_display').value = yy + '-' + mm;
|
||||
});
|
||||
|
||||
// ── Lines ─────────────────────────────────────────────────────────────────────
|
||||
function account_opts(selected) {
|
||||
var opts = '<option value="">— Account —</option>';
|
||||
all_accounts.forEach(function(a) {
|
||||
var sel = (selected && selected === a.account_code) ? 'selected' : '';
|
||||
opts += '<option value="' + escape_html(a.account_code) + '" ' + sel + '>' +
|
||||
escape_html(a.account_code + ' — ' + a.account_name) + '</option>';
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
|
||||
function add_line(code, desc, dr, cr) {
|
||||
line_counter++;
|
||||
var id = 'line_' + line_counter;
|
||||
var tr = document.createElement('tr');
|
||||
tr.id = id;
|
||||
tr.innerHTML =
|
||||
'<td><select class="form-select form-select-sm line-account" onchange="recalc()">' + account_opts(code || '') + '</select></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm line-desc" value="' + escape_html(desc || '') + '" placeholder="Line description"></td>' +
|
||||
'<td><input type="number" class="form-control form-control-sm text-end line-debit" min="0" step="0.01" value="' + (dr || '') + '" oninput="recalc()"></td>' +
|
||||
'<td><input type="number" class="form-control form-control-sm text-end line-credit" min="0" step="0.01" value="' + (cr || '') + '" oninput="recalc()"></td>' +
|
||||
'<td><button class="btn btn-sm btn-link text-danger p-0" onclick="remove_line(\'' + id + '\')"><i class="ti ti-trash"></i></button></td>';
|
||||
document.getElementById('lines_tbody').appendChild(tr);
|
||||
recalc();
|
||||
}
|
||||
|
||||
function remove_line(id) {
|
||||
var tbody = document.getElementById('lines_tbody');
|
||||
if (tbody.rows.length <= 2) { alert('Minimum 2 lines required.'); return; }
|
||||
document.getElementById(id).remove();
|
||||
recalc();
|
||||
}
|
||||
|
||||
function recalc() {
|
||||
var dr = 0, cr = 0;
|
||||
document.querySelectorAll('.line-debit').forEach(function(el) { dr += parseFloat(el.value) || 0; });
|
||||
document.querySelectorAll('.line-credit').forEach(function(el) { cr += parseFloat(el.value) || 0; });
|
||||
document.getElementById('bal_debit').textContent = dr.toFixed(2);
|
||||
document.getElementById('bal_credit').textContent = cr.toFixed(2);
|
||||
var diff = Math.abs(dr - cr);
|
||||
var status = document.getElementById('bal_status');
|
||||
if (diff < 0.005) {
|
||||
status.textContent = 'Balanced';
|
||||
status.className = 'text-success small mb-0 fw-semibold';
|
||||
} else {
|
||||
status.textContent = 'Out of balance by ' + diff.toFixed(2);
|
||||
status.className = 'text-danger small mb-0';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
function save_journal() {
|
||||
var jdate = document.getElementById('journal_date').value;
|
||||
if (!jdate) { alert('Journal date is required.'); return; }
|
||||
|
||||
var lines = [];
|
||||
document.querySelectorAll('#lines_tbody tr').forEach(function(tr) {
|
||||
var code = tr.querySelector('.line-account').value;
|
||||
var desc = tr.querySelector('.line-desc').value;
|
||||
var debit = parseFloat(tr.querySelector('.line-debit').value) || 0;
|
||||
var credit= parseFloat(tr.querySelector('.line-credit').value) || 0;
|
||||
if (code && (debit || credit)) lines.push({ account_code: code, description: desc, debit: debit, credit: credit });
|
||||
});
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/save_manual_journal.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'manage',
|
||||
gl_id: document.getElementById('gl_id').value || 0,
|
||||
journal_date: jdate,
|
||||
reference: document.getElementById('reference').value,
|
||||
description: document.getElementById('description').value,
|
||||
lines: JSON.stringify(lines),
|
||||
onSuccess: function() {
|
||||
window.location.href = server_url + 'journal/index.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Edit mode ─────────────────────────────────────────────────────────────────
|
||||
function load_for_edit() {
|
||||
<?php if (empty($_GET['gl_id'])): ?>
|
||||
return;
|
||||
<?php endif; ?>
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_journal_detail.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
gl_id: <?php echo (int)$_GET['gl_id']; ?>,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var h = d.header;
|
||||
document.getElementById('gl_id').value = h.id;
|
||||
document.getElementById('journal_date').value = h.journal_date_fmt || '';
|
||||
document.getElementById('period_display').value = h.period || '';
|
||||
document.getElementById('reference').value = h.reference || '';
|
||||
document.getElementById('description').value = h.description || '';
|
||||
document.getElementById('page_title').textContent = 'Edit Journal Entry';
|
||||
document.getElementById('btn_save_text').textContent = 'Update Entry';
|
||||
|
||||
// Clear default blank lines and repopulate
|
||||
document.getElementById('lines_tbody').innerHTML = '';
|
||||
(d.lines || []).forEach(function(l) {
|
||||
add_line(l.account_code, l.description, l.debit, l.credit);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user