accounting modules

This commit is contained in:
Thanakorn S
2026-05-13 15:11:24 +07:00
parent 511360faa2
commit 04a683bd02
70 changed files with 4451 additions and 42 deletions
+9
View File
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
$coa = new ChartOfAccounts($pdo2, $company_id);
$answer['output'] = $coa->getAll();
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
$coa = new ChartOfAccounts($pdo2, $company_id);
$stats = $coa->getStats();
$answer = array_merge(['success' => 1], $stats);
exit(json_encode($answer));
+9
View File
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/DepartmentManager.php';
$dept = new DepartmentManager($pdo2, $company_id);
$answer['output'] = $dept->getAll();
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/DepartmentManager.php';
$dept = new DepartmentManager($pdo2, $company_id);
$stats = $dept->getStats();
$answer = array_merge(['success' => 1], $stats);
exit(json_encode($answer));
@@ -0,0 +1,22 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
if (empty($data['account_code']) || empty($data['account_name']) || empty($data['account_type'])) {
exit(json_encode(['message' => 'Account code, name, and type are required']));
}
$coa = new ChartOfAccounts($pdo2, $company_id);
$id = (int)($data['id'] ?? 0);
if ($id) {
$coa->update($id, $data);
$answer['message'] = 'Account updated';
} else {
$coa->create($data);
$answer['message'] = 'Account created';
}
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,22 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/DepartmentManager.php';
if (empty($data['dept_code']) || empty($data['dept_name'])) {
exit(json_encode(['message' => 'Department code and name are required']));
}
$dept = new DepartmentManager($pdo2, $company_id);
$id = (int)($data['id'] ?? 0);
if ($id) {
$dept->update($id, $data);
$answer['message'] = 'Department updated';
} else {
$dept->create($data);
$answer['message'] = 'Department created';
}
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,40 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/PeriodManager.php';
$pm = new PeriodManager($pdo2, $company_id);
$action = $data['action_type'] ?? '';
if ($action === 'create_year') {
$year = (int)($data['year'] ?? date('Y'));
if ($year < 2000 || $year > 2100) exit(json_encode(['message' => 'Invalid year']));
$pm->createYear($year);
$answer['message'] = "All 12 periods for {$year} created";
} elseif ($action === 'create_single') {
$period = $data['period'] ?? '';
if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period)) {
exit(json_encode(['message' => 'Invalid period format (YYYY-MM)']));
}
$pm->create($period);
$answer['message'] = "Period {$period} created";
} elseif ($action === 'open') {
$pm->open((int)($data['id'] ?? 0));
$answer['message'] = 'Period reopened';
} elseif ($action === 'close') {
$pm->close((int)($data['id'] ?? 0));
$answer['message'] = 'Period closed';
} elseif ($action === 'lock') {
$pm->lock((int)($data['id'] ?? 0));
$answer['message'] = 'Period locked';
} else {
exit(json_encode(['message' => 'Unknown action']));
}
$answer['success'] = 1;
exit(json_encode($answer));
+9
View File
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/PeriodManager.php';
$pm = new PeriodManager($pdo2, $company_id);
$answer['output'] = $pm->getAll();
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,9 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/PeriodManager.php';
$pm = new PeriodManager($pdo2, $company_id);
$stats = $pm->getStats();
$answer = array_merge(['success' => 1], $stats);
exit(json_encode($answer));
@@ -0,0 +1,16 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
exit(json_encode(['message' => 'Missing id']));
}
$coa = new ChartOfAccounts($pdo2, $company_id);
$coa->delete($id);
$answer['success'] = 1;
$answer['message'] = 'Account removed';
exit(json_encode($answer));
@@ -0,0 +1,14 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/DepartmentManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) exit(json_encode(['message' => 'Missing id']));
$dept = new DepartmentManager($pdo2, $company_id);
$dept->delete($id);
$answer['success'] = 1;
$answer['message'] = 'Department removed';
exit(json_encode($answer));
@@ -0,0 +1,19 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
exit(json_encode(['message' => 'Missing id']));
}
$coa = new ChartOfAccounts($pdo2, $company_id);
$row = $coa->getById($id);
if (!$row) {
exit(json_encode(['message' => 'Account not found']));
}
$answer['output'] = $row;
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,15 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes_ac/DepartmentManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) exit(json_encode(['message' => 'Missing id']));
$dept = new DepartmentManager($pdo2, $company_id);
$row = $dept->getById($id);
if (!$row) exit(json_encode(['message' => 'Department not found']));
$answer['output'] = $row;
$answer['success'] = 1;
exit(json_encode($answer));
+36
View File
@@ -0,0 +1,36 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Balance Sheet</h1>
<p class="mb-0 text-muted">Assets, liabilities, and equity</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-table fs-1 mb-3 d-block"></i>
<p class="mb-0">Balance Sheet coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+272
View File
@@ -0,0 +1,272 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_master_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="fs-3 mb-1">Chart of Accounts</h1>
<p class="mb-0">Manage your account code structure</p>
</div>
<div>
<a href="<?php echo $server_url?>accounting/manage_account.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>Add Account
</a>
</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="row g-5 mb-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
<i class="ti ti-list-tree fs-4"></i>
</div>
<div>
<span>Total Accounts</span>
<h2 class="fw-bold mb-0" id="stat_total">—</h2>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-check fs-4"></i>
</div>
<div>
<span>Active</span>
<h2 class="fw-bold mb-0" id="stat_active">—</h2>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-info bg-opacity-10 text-info rounded-2">
<i class="ti ti-trending-up fs-4"></i>
</div>
<div>
<span>Revenue Accounts</span>
<h2 class="fw-bold mb-0" id="stat_revenue">—</h2>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-warning bg-opacity-10 text-warning rounded-2">
<i class="ti ti-trending-down fs-4"></i>
</div>
<div>
<span>Expense Accounts</span>
<h2 class="fw-bold mb-0" id="stat_expense">—</h2>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card overflow-hidden">
<div class="card-body p-6">
<div class="mb-5 d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-sm btn-outline-secondary active" onclick="filter_type('')" id="filter_all">All</button>
<button class="btn btn-sm btn-outline-secondary" onclick="filter_type('asset')" id="filter_asset">Asset</button>
<button class="btn btn-sm btn-outline-secondary" onclick="filter_type('liability')" id="filter_liability">Liability</button>
<button class="btn btn-sm btn-outline-secondary" onclick="filter_type('equity')" id="filter_equity">Equity</button>
<button class="btn btn-sm btn-outline-secondary" onclick="filter_type('revenue')" id="filter_revenue">Revenue</button>
<button class="btn btn-sm btn-outline-secondary" onclick="filter_type('expense')" id="filter_expense">Expense</button>
</div>
<div class="position-relative" style="min-width:240px;">
<input type="search" id="search_account" class="form-control ps-9" placeholder="Search code or name…">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="account_table">
<thead class="table-primary border-light">
<tr>
<th>Code</th>
<th>Name</th>
<th>Type</th>
<th>Level</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var search_term = '';
var active_type = '';
var type_badge = {
asset: '<span class="badge bg-primary bg-opacity-10 text-primary">Asset</span>',
liability: '<span class="badge bg-danger bg-opacity-10 text-danger">Liability</span>',
equity: '<span class="badge bg-info bg-opacity-10 text-info">Equity</span>',
revenue: '<span class="badge bg-success bg-opacity-10 text-success">Revenue</span>',
expense: '<span class="badge bg-warning bg-opacity-10 text-warning">Expense</span>',
};
var level_badge = {
item: '<span class="badge bg-primary">Item</span>',
parent: '<span class="badge bg-secondary">Parent</span>',
};
function retrieve_accounts() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/account.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
alasql('CREATE TABLE IF NOT EXISTS ac_account');
alasql.tables.ac_account.data = res.output || [];
change_page(1);
}
});
}
function change_page(page_num) {
var offset = (page_num - 1) * prop_limit;
var kw = search_term.toLowerCase();
var conditions = [];
if (kw) conditions.push(`(LOWER(account_code) LIKE '%${kw}%' OR LOWER(account_name) LIKE '%${kw}%')`);
if (active_type) conditions.push(`account_type = '${active_type}'`);
var where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '';
var filtered = alasql(`SELECT * FROM ac_account ${where} ORDER BY account_code`);
var page = generate_pagination('ac_account', filtered.length);
$('table#account_table tfoot').html(page);
if (filtered.length === 0) {
$('table#account_table tbody').html(
'<tr><td colspan="7" class="text-center py-5 text-muted">No accounts found.</td></tr>'
);
return;
}
var page_data = filtered.slice(offset, offset + prop_limit);
var body = '';
$.each(page_data, function(i, item) {
var is_item = item.is_posting == 1;
var indent = is_item && item.parent_code ? 'ps-4' : '';
var prefix = is_item && item.parent_code ? '<i class="ti ti-corner-down-right text-muted me-1 small"></i>' : '';
body += `<tr class="${is_item ? '' : 'table-light'}">
<td class="py-3 fw-semibold ${indent}">${prefix}${escape_html(item.account_code)}</td>
<td class="py-3">${escape_html(item.account_name)}</td>
<td class="py-3">${type_badge[item.account_type] || item.account_type}</td>
<td class="py-3">${is_item ? level_badge.item : level_badge.parent}</td>
<td class="py-3">${item.status ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
<td class="py-3">
<a href="<?php echo $server_url?>accounting/manage_account.php?id=${item.id}"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger ms-2" onclick="remove_account(${item.id}, $(this))"><i class="ti ti-trash fs-5"></i></a>
</td>
</tr>`;
});
$('table#account_table tbody').html(body);
}
function filter_type(type) {
active_type = type;
$('.btn[id^="filter_"]').removeClass('active');
$('#filter_' + (type || 'all')).addClass('active');
change_page(1);
}
function remove_account(id, element) {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/remove_account.php',
autoPrepare: true,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() {
element.closest('tr').remove();
}
});
}
function retrieve_stats() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/account_stats.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$('#stat_total').text(res.total || 0);
$('#stat_active').text(res.active || 0);
$('#stat_revenue').text(res.revenue || 0);
$('#stat_expense').text(res.expense || 0);
}
});
}
function debounce(fn, ms) {
var t;
return function() { var a = arguments, c = this; clearTimeout(t); t = setTimeout(function() { fn.apply(c, a); }, ms); };
}
$('#search_account').on('input', debounce(function() {
search_term = $(this).val().trim();
change_page(1);
}, 200));
$(async function() {
try {
await retrieve_stats();
await retrieve_accounts();
} catch(e) { console.log(e); }
});
</script>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_master_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="fs-3 mb-1">Departments</h1>
<p class="mb-0">GL department dimensions for journal entries</p>
</div>
<div>
<a href="<?php echo $server_url?>accounting/manage_department.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>Add Department
</a>
</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="row g-5 mb-5">
<div class="col-xl-4 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
<i class="ti ti-building fs-4"></i>
</div>
<div>
<span>Total</span>
<h2 class="fw-bold mb-0" id="stat_total">—</h2>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-check fs-4"></i>
</div>
<div>
<span>Active</span>
<h2 class="fw-bold mb-0" id="stat_active">—</h2>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-secondary bg-opacity-10 text-secondary rounded-2">
<i class="ti ti-x fs-4"></i>
</div>
<div>
<span>Inactive</span>
<h2 class="fw-bold mb-0" id="stat_inactive">—</h2>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card overflow-hidden">
<div class="card-body p-6">
<div class="mb-5 d-flex justify-content-end">
<div class="position-relative" style="min-width:240px;">
<input type="search" id="search_dept" class="form-control ps-9" placeholder="Search code or name…">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="dept_table">
<thead class="table-primary border-light">
<tr>
<th>Code</th>
<th>Name</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var search_term = '';
function retrieve_departments() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/department.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
alasql('CREATE TABLE IF NOT EXISTS ac_department');
alasql.tables.ac_department.data = res.output || [];
change_page(1);
}
});
}
function change_page(page_num) {
var offset = (page_num - 1) * prop_limit;
var kw = search_term.toLowerCase();
var filtered = kw
? alasql(`SELECT * FROM ac_department WHERE LOWER(dept_code) LIKE '%${kw}%' OR LOWER(dept_name) LIKE '%${kw}%' ORDER BY dept_code`)
: alasql(`SELECT * FROM ac_department ORDER BY dept_code`);
$('table#dept_table tfoot').html(generate_pagination('ac_department', filtered.length));
if (filtered.length === 0) {
$('table#dept_table tbody').html('<tr><td colspan="5" class="text-center py-5 text-muted">No departments found.</td></tr>');
return;
}
var page_data = filtered.slice(offset, offset + prop_limit);
var body = '';
$.each(page_data, function(i, item) {
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(item.dept_code)}</td>
<td class="py-3">${escape_html(item.dept_name)}</td>
<td class="py-3 text-muted">${item.description ? escape_html(item.description) : '—'}</td>
<td class="py-3">${item.status ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
<td class="py-3">
<a href="<?php echo $server_url?>accounting/manage_department.php?id=${item.id}"><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:;" class="link-danger ms-2" onclick="remove_department(${item.id}, $(this))"><i class="ti ti-trash fs-5"></i></a>
</td>
</tr>`;
});
$('table#dept_table tbody').html(body);
}
function remove_department(id, element) {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/remove_department.php',
autoPrepare: true,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { element.closest('tr').remove(); }
});
}
function retrieve_stats() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/department_stats.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$('#stat_total').text(res.total || 0);
$('#stat_active').text(res.active || 0);
$('#stat_inactive').text(res.inactive || 0);
}
});
}
function debounce(fn, ms) {
var t;
return function() { var a = arguments, c = this; clearTimeout(t); t = setTimeout(function() { fn.apply(c, a); }, ms); };
}
$('#search_dept').on('input', debounce(function() {
search_term = $(this).val().trim();
change_page(1);
}, 200));
$(async function() {
try {
await retrieve_stats();
await retrieve_departments();
} catch(e) { console.log(e); }
});
</script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">GL Movement</h1>
<p class="mb-0 text-muted">General ledger movement report</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-arrows-exchange fs-1 mb-3 d-block"></i>
<p class="mb-0">GL Movement coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_master_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex align-items-center mb-5 gap-3">
<a href="<?php echo $server_url?>accounting/chart_of_accounts.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">Manage Account</h1>
</div>
</div>
</div>
</div>
<div class="row" id="accountForm">
<div class="col-lg-7">
<div class="card">
<div class="card-body p-6">
<input type="hidden" id="id">
<div class="row g-4">
<div class="col-md-4">
<label class="form-label">Account Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="account_code" placeholder="e.g. 1100" required>
<div class="form-text">1xxx Asset · 2xxx Liability · 3xxx Equity · 4xxx Revenue · 5-9xxx Expense</div>
</div>
<div class="col-md-8">
<label class="form-label">Account Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="account_name" placeholder="e.g. Cash at Bank" required>
</div>
<div class="col-md-6">
<label class="form-label">Account Type <span class="text-danger">*</span></label>
<select class="form-select" id="account_type" required>
<option value="">— Auto-detected from code —</option>
<option value="asset">Asset</option>
<option value="liability">Liability</option>
<option value="equity">Equity</option>
<option value="revenue">Revenue</option>
<option value="expense">Expense</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Account Level <span class="text-danger">*</span></label>
<select class="form-select" id="is_posting" required>
<option value="1">Item — used in journal entries</option>
<option value="0">Parent — grouping / reports only</option>
</select>
</div>
<div class="col-md-12" id="parent_row">
<label class="form-label">Parent Account</label>
<select class="form-select" id="parent_code">
<option value="">— None (top-level parent) —</option>
</select>
<div class="form-text">Only parent-level accounts of the same type are shown.</div>
</div>
<div class="col-md-6">
<label class="form-label">Status</label>
<select class="form-select" id="status">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
<div class="d-flex gap-2 mt-5">
<button class="btn btn-primary" onclick="manage_account();" id="btn_submit">Create Account</button>
<button class="btn btn-secondary" onclick="reset_input('#accountForm');" id="btn_reset">Clear</button>
</div>
</div>
</div>
</div>
<!-- Guide card -->
<div class="col-lg-5">
<div class="card border-0 bg-light">
<div class="card-body p-5">
<h6 class="fw-semibold mb-3"><i class="ti ti-info-circle me-1"></i>Account Level Guide</h6>
<p class="small mb-3">
<strong>Parent</strong> accounts group items for reporting (e.g. <em>1000 Current Assets</em>).
They cannot be used in journal entries.
</p>
<p class="small mb-3">
<strong>Item</strong> accounts are the leaf-level codes posted in every journal entry
(e.g. <em>1110 Cash at Bank</em>).
</p>
<hr class="my-3">
<p class="small mb-1 fw-semibold">Standard ranges:</p>
<table class="table table-sm small mb-0">
<tbody>
<tr><td>1xxx</td><td>Asset</td></tr>
<tr><td>2xxx</td><td>Liability</td></tr>
<tr><td>3xxx</td><td>Equity</td></tr>
<tr><td>4xxx</td><td>Revenue</td></tr>
<tr><td>5–9xxx</td><td>Expense</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var all_accounts = [];
// ── Auto-detect type from code prefix ────────────────────────────────
function detect_type(code) {
var first = parseInt((code || '').charAt(0));
if (first === 1) return 'asset';
if (first === 2) return 'liability';
if (first === 3) return 'equity';
if (first === 4) return 'revenue';
if (first >= 5 && first <= 9) return 'expense';
return '';
}
$('#account_code').on('input', function() {
var type = detect_type($(this).val().trim());
if (type) {
$('#account_type').val(type);
refresh_parent_dropdown(type, null);
}
});
$('#account_type').on('change', function() {
refresh_parent_dropdown($(this).val(), null);
});
// Hide parent row when account level is set to Parent (is_posting=0)
$('#is_posting').on('change', function() {
if ($(this).val() == '0') {
$('#parent_row').hide();
$('#parent_code').val('');
} else {
$('#parent_row').show();
}
});
// ── Parent dropdown: filter to same type + is_posting=0 ─────────────
function refresh_parent_dropdown(type, selected_code) {
var opts = '<option value="">— None (top-level parent) —</option>';
var parents = all_accounts.filter(function(a) {
return a.is_posting == 0 && a.status == 1 && (!type || a.account_type === type);
});
$.each(parents, function(i, item) {
var sel = (selected_code && selected_code == item.account_code) ? 'selected' : '';
opts += `<option value="${escape_html(item.account_code)}" ${sel}>${escape_html(item.account_code)} — ${escape_html(item.account_name)}</option>`;
});
$('#parent_code').html(opts);
}
function load_all_accounts(selected_parent, selected_type) {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/account.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
all_accounts = res.output || [];
refresh_parent_dropdown(selected_type, selected_parent);
}
});
}
function manage_account() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/manage_account.php',
autoPrepare: true,
checkRequired: 1,
action: 'manage',
onSuccess: function() {
window.location.href = '<?php echo $server_url?>accounting/chart_of_accounts.php';
}
});
}
function retrieve_for_edit() {
<?php if (empty($_GET['id'])): ?>
return false;
<?php endif; ?>
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/retrieve_account.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: <?php echo (int)$_GET['id']; ?> },
onSuccess: function(res) {
var item = res.output;
$('#id').val(item.id);
$('#account_code').val(item.account_code);
$('#account_name').val(item.account_name);
$('#account_type').val(item.account_type);
$('#is_posting').val(item.is_posting);
$('#status').val(item.status);
if (item.is_posting == 0) $('#parent_row').hide();
load_all_accounts(item.parent_code, item.account_type);
$('#btn_submit').text('Update Account');
$('#btn_reset').hide();
}
});
}
$(async function() {
try {
<?php if (empty($_GET['id'])): ?>
await load_all_accounts(null, null);
<?php else: ?>
await retrieve_for_edit();
<?php endif; ?>
} catch(e) { console.log(e); }
});
</script>
</body>
</html>
+112
View File
@@ -0,0 +1,112 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_master_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex align-items-center mb-5 gap-3">
<a href="<?php echo $server_url?>accounting/departments.php" class="btn btn-light btn-sm">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
<h1 class="fs-3 mb-0">Manage Department</h1>
</div>
</div>
</div>
<div class="row" id="deptForm">
<div class="col-lg-6">
<div class="card">
<div class="card-body p-6">
<input type="hidden" id="id">
<div class="row g-4">
<div class="col-md-4">
<label class="form-label">Department Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="dept_code" placeholder="e.g. HQ" required>
</div>
<div class="col-md-8">
<label class="form-label">Department Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="dept_name" placeholder="e.g. Head Office" required>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea class="form-control" id="description" rows="3" placeholder="Optional description"></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Status</label>
<select class="form-select" id="status">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
<div class="d-flex gap-2 mt-5">
<button class="btn btn-primary" onclick="manage_department();" id="btn_submit">Create Department</button>
<button class="btn btn-secondary" onclick="reset_input('#deptForm');" id="btn_reset">Clear</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
function manage_department() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/manage_department.php',
autoPrepare: true,
checkRequired: 1,
action: 'manage',
onSuccess: function() {
window.location.href = '<?php echo $server_url?>accounting/departments.php';
}
});
}
function retrieve_for_edit() {
<?php if (empty($_GET['id'])): ?>
return false;
<?php endif; ?>
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/retrieve_department.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: <?php echo (int)$_GET['id']; ?> },
onSuccess: function(res) {
var item = res.output;
$('#id').val(item.id);
$('#dept_code').val(item.dept_code);
$('#dept_name').val(item.dept_name);
$('#description').val(item.description);
$('#status').val(item.status);
$('#btn_submit').text('Update Department');
$('#btn_reset').hide();
}
});
}
$(async function() {
try { await retrieve_for_edit(); } catch(e) { console.log(e); }
});
</script>
</body>
</html>
+274
View File
@@ -0,0 +1,274 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_master_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div>
<h1 class="fs-3 mb-1">Accounting Periods</h1>
<p class="mb-0">Open, close, and lock monthly accounting periods</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" onclick="show_create_year_modal();">
<i class="ti ti-calendar-plus me-1"></i>Generate Year
</button>
<button class="btn btn-primary" onclick="show_create_modal();">
<i class="ti ti-plus me-1"></i>Add Period
</button>
</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="row g-5 mb-5">
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
<i class="ti ti-calendar fs-4"></i>
</div>
<div><span>Total</span><h2 class="fw-bold mb-0" id="stat_total">—</h2></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-lock-open fs-4"></i>
</div>
<div><span>Open</span><h2 class="fw-bold mb-0" id="stat_open">—</h2></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-warning bg-opacity-10 text-warning rounded-2">
<i class="ti ti-lock fs-4"></i>
</div>
<div><span>Closed</span><h2 class="fw-bold mb-0" id="stat_closed">—</h2></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-6">
<div class="d-flex align-items-start gap-3">
<div class="icon-shape icon-lg bg-danger bg-opacity-10 text-danger rounded-2">
<i class="ti ti-lock-filled fs-4"></i>
</div>
<div><span>Locked</span><h2 class="fw-bold mb-0" id="stat_locked">—</h2></div>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card overflow-hidden">
<div class="card-body p-6">
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered" id="period_table">
<thead class="table-primary border-light">
<tr>
<th>Period</th>
<th>Status</th>
<th>Closed At</th>
<th>Locked At</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Add Single Period Modal -->
<div class="modal fade" id="createPeriodModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:340px;">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">Add Period</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<label class="form-label">Period (YYYY-MM)</label>
<input type="month" class="form-control" id="new_period">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button class="btn btn-primary" onclick="create_single();">Create</button>
</div>
</div>
</div>
</div>
<!-- Generate Year Modal -->
<div class="modal fade" id="createYearModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:340px;">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">Generate Year Periods</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<label class="form-label">Year</label>
<input type="number" class="form-control" id="new_year" value="<?php echo date('Y'); ?>" min="2000" max="2100">
<div class="form-text">Creates all 12 months for the selected year (skips existing ones).</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button class="btn btn-primary" onclick="create_year();">Generate</button>
</div>
</div>
</div>
</div>
<?php require '../include_ending.php'; ?>
<script>
var status_badge = {
open: '<span class="badge bg-success">Open</span>',
closed: '<span class="badge bg-warning text-dark">Closed</span>',
locked: '<span class="badge bg-danger">Locked</span>',
};
function retrieve_periods() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/period.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var rows = res.output || [];
if (rows.length === 0) {
$('table#period_table tbody').html('<tr><td colspan="5" class="text-center py-5 text-muted">No periods yet. Use Generate Year to create them.</td></tr>');
return;
}
var body = '';
$.each(rows, function(i, item) {
var actions = '';
if (item.status === 'open') {
actions = `<button class="btn btn-sm btn-outline-warning" onclick="do_action('close', ${item.id})">Close</button>`;
} else if (item.status === 'closed') {
actions = `<button class="btn btn-sm btn-outline-success me-1" onclick="do_action('open', ${item.id})">Reopen</button>
<button class="btn btn-sm btn-outline-danger" onclick="do_action('lock', ${item.id})">Lock</button>`;
} else {
actions = '<span class="text-muted small">Permanent</span>';
}
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(item.period)}</td>
<td class="py-3">${status_badge[item.status] || item.status}</td>
<td class="py-3 text-muted small">${item.closed_at || '—'}</td>
<td class="py-3 text-muted small">${item.locked_at || '—'}</td>
<td class="py-3">${actions}</td>
</tr>`;
});
$('table#period_table tbody').html(body);
}
});
}
function do_action(action_type, id) {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
autoPrepare: true,
checkRequired: 0,
action: 'manage',
data: { action_type: action_type, id: id },
onSuccess: function() {
retrieve_periods();
retrieve_stats();
}
});
}
function create_single() {
var period = $('#new_period').val();
if (!period) return;
ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
autoPrepare: true,
checkRequired: 0,
action: 'manage',
data: { action_type: 'create_single', period: period },
onSuccess: function() {
$('#createPeriodModal').modal('hide');
retrieve_periods();
retrieve_stats();
}
});
}
function create_year() {
var year = $('#new_year').val();
if (!year) return;
ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
autoPrepare: true,
checkRequired: 0,
action: 'manage',
data: { action_type: 'create_year', year: year },
onSuccess: function() {
$('#createYearModal').modal('hide');
retrieve_periods();
retrieve_stats();
}
});
}
function retrieve_stats() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/period_stats.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
$('#stat_total').text(res.total || 0);
$('#stat_open').text(res.open_count || 0);
$('#stat_closed').text(res.closed_count || 0);
$('#stat_locked').text(res.locked_count || 0);
}
});
}
function show_create_modal() { $('#new_period').val(''); $('#createPeriodModal').modal('show'); }
function show_create_year_modal() { $('#createYearModal').modal('show'); }
$(async function() {
try {
await retrieve_stats();
await retrieve_periods();
} catch(e) { console.log(e); }
});
</script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">P&amp;L Statement</h1>
<p class="mb-0 text-muted">Profit and loss statement</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-chart-line fs-1 mb-3 d-block"></i>
<p class="mb-0">P&amp;L Statement coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Trial Balance</h1>
<p class="mb-0 text-muted">Period trial balance report</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-scale fs-1 mb-3 d-block"></i>
<p class="mb-0">Trial Balance coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">VAT Report</h1>
<p class="mb-0 text-muted">Value added tax report</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-receipt-tax fs-1 mb-3 d-block"></i>
<p class="mb-0">VAT Report coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>