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
+39
View File
@@ -0,0 +1,39 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?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="col-12">
<h1 class="fs-3 mb-1">Accounting Dashboard</h1>
<p class="mb-0 text-muted">Financial overview</p>
</div>
</div>
<!-- Placeholder -->
<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>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+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>
+28 -6
View File
@@ -264,13 +264,27 @@ class OrderManager {
if (!$row) {
throw new Exception("Order not found.");
}
if ((int)$row['status'] !== 0) {
throw new Exception("Only draft orders can be edited.");
$cur_status = (int)$row['status'];
if ($cur_status !== 0 && $cur_status !== -2) {
throw new Exception("Only draft or pending orders can be edited.");
}
$log = json_decode($row['log'] ?? '[]', true) ?: [];
$log[] = $logging;
// Auto-promote Pending → Draft when all items have a warehouse assigned
$new_status = $cur_status;
if ($cur_status === -2) {
$all_have_warehouse = !empty($items) && array_reduce(
$items,
fn($ok, $item) => $ok && (int)($item['warehouse_id'] ?? 0) > 0,
true
);
if ($all_have_warehouse) {
$new_status = 0;
}
}
$this->pdo->prepare(
"UPDATE td_order SET
contact_id = :contact_id,
@@ -283,6 +297,7 @@ class OrderManager {
shipping_tracking_number = :shipping_tracking_number,
grand_total = :grand_total,
notes = :notes,
status = :status,
`log` = :log
WHERE id = :id AND company_id = :company_id"
)->execute([
@@ -296,6 +311,7 @@ class OrderManager {
':shipping_tracking_number' => $tracking_no,
':grand_total' => $grand_total,
':notes' => $data['notes'] ?? '',
':status' => $new_status,
':log' => json_encode($log),
':id' => $id,
':company_id' => $this->company_id,
@@ -305,23 +321,28 @@ class OrderManager {
} else {
$log = [$logging];
$log = [$logging];
$quotation_id = (int)($data['quotation_id'] ?? 0);
// Quotation-converted orders start as Pending (-2); direct orders as Draft (0)
$init_status = $quotation_id > 0 ? -2 : 0;
$this->pdo->prepare(
"INSERT INTO td_order
(company_id, uuid, order_number, contact_id, order_date,
(company_id, uuid, quotation_id, order_number, contact_id, order_date,
status, payment_status, subtotal, discount, tax,
shipping_fee, shipping_tracking_number, grand_total, items, notes, `log`, created_at)
VALUES
(:company_id, :uuid, :order_number, :contact_id, :order_date,
0, 0, :subtotal, :discount, :tax,
(:company_id, :uuid, :quotation_id, :order_number, :contact_id, :order_date,
:status, 0, :subtotal, :discount, :tax,
:shipping_fee, :shipping_tracking_number, :grand_total, :items, :notes, :log, :created_at)"
)->execute([
':company_id' => $this->company_id,
':uuid' => bin2hex(random_bytes(16)),
':quotation_id' => $quotation_id,
':order_number' => $this->generateOrderNumber(),
':contact_id' => (int)($data['contact_id'] ?? 0),
':order_date' => $data['order_date'] ?? date('Y-m-d'),
':status' => $init_status,
':subtotal' => $subtotal,
':discount' => $discount,
':tax' => $tax,
@@ -539,6 +560,7 @@ class OrderManager {
"Cannot cancel an order that is processing or completed."
);
}
// Pending orders (-2) have no stock rows — skip the stock reversal loop below
// ── Guard: no active invoice ──────────────────────────────────────
$sth = $this->pdo->prepare(
@@ -0,0 +1,37 @@
<?php
/**
* PostingManager
*
* The ONLY bridge between WMS and the accounting engine.
* WMS API endpoints call methods here — nothing in WMS imports
* accounting classes directly.
*
* Usage in a WMS API endpoint (add at the end, after business doc saves):
* require_once '../../../assets/utils/classes/PostingManager.php';
* (new PostingManager($pdo2, $company_id))->postInvoice($invoice_id);
*
* If posting fails it logs silently — the business document is already saved.
*/
class PostingManager
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: postInvoice(int $invoice_id): void
// TODO: voidInvoice(int $invoice_id): void
// TODO: postPurchaseReceive(int $po_id): void
// TODO: voidPurchaseReceive(int $po_id): void
// TODO: postReceipt(int $receipt_id): void
// TODO: voidReceipt(int $receipt_id): void
// TODO: postPayment(int $payment_id): void
// TODO: voidPayment(int $payment_id): void
}
@@ -0,0 +1,23 @@
<?php
/**
* AccountingEngine
*
* Core double-entry validation and rules.
* Ensures every journal entry balances (total debit = total credit).
* All other accounting classes depend on this for validation.
*/
class AccountingEngine
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: validate journal lines balance (sum debit = sum credit)
// TODO: validate account codes exist and are posting accounts
// TODO: validate period is open
}
@@ -0,0 +1,141 @@
<?php
class ChartOfAccounts
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
public function getAll(): array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_account
WHERE company_id = :cid
ORDER BY account_code ASC"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
public function getByCode(string $account_code): ?array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_account
WHERE company_id = :cid AND account_code = :code
LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':code' => $account_code]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function getByType(string $account_type): array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_account
WHERE company_id = :cid AND account_type = :type AND status = 1
ORDER BY account_code ASC"
);
$sth->execute([':cid' => $this->companyId, ':type' => $account_type]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
public function getById(int $id): ?array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_account
WHERE company_id = :cid AND id = :id
LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function create(array $data): int
{
$sth = $this->pdo->prepare(
"INSERT INTO ac_account
(company_id, account_code, account_name, account_type, parent_code, is_posting, status)
VALUES
(:cid, :code, :name, :type, :parent, :posting, :status)"
);
$sth->execute([
':cid' => $this->companyId,
':code' => trim($data['account_code']),
':name' => trim($data['account_name']),
':type' => $data['account_type'],
':parent' => !empty($data['parent_code']) ? trim($data['parent_code']) : null,
':posting' => (int)($data['is_posting'] ?? 1),
':status' => (int)($data['status'] ?? 1),
]);
return (int)$this->pdo->lastInsertId();
}
public function update(int $id, array $data): void
{
$sth = $this->pdo->prepare(
"UPDATE ac_account SET
account_code = :code,
account_name = :name,
account_type = :type,
parent_code = :parent,
is_posting = :posting,
status = :status
WHERE company_id = :cid AND id = :id"
);
$sth->execute([
':cid' => $this->companyId,
':id' => $id,
':code' => trim($data['account_code']),
':name' => trim($data['account_name']),
':type' => $data['account_type'],
':parent' => !empty($data['parent_code']) ? trim($data['parent_code']) : null,
':posting' => (int)($data['is_posting'] ?? 1),
':status' => (int)($data['status'] ?? 1),
]);
}
public function delete(int $id): void
{
$sth = $this->pdo->prepare(
"UPDATE ac_account SET status = 0
WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
}
public function isPostingAccount(string $account_code): bool
{
$sth = $this->pdo->prepare(
"SELECT is_posting FROM ac_account
WHERE company_id = :cid AND account_code = :code AND status = 1
LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':code' => $account_code]);
return (bool)$sth->fetchColumn();
}
public function getStats(): array
{
$sth = $this->pdo->prepare(
"SELECT
COUNT(*) AS total,
SUM(status = 1) AS active,
SUM(status = 0) AS inactive,
SUM(account_type = 'asset') AS asset,
SUM(account_type = 'liability') AS liability,
SUM(account_type = 'equity') AS equity,
SUM(account_type = 'revenue') AS revenue,
SUM(account_type = 'expense') AS expense
FROM ac_account
WHERE company_id = :cid"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
}
}
@@ -0,0 +1,91 @@
<?php
class DepartmentManager
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
public function getAll(): array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_department
WHERE company_id = :cid
ORDER BY dept_code ASC"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
public function getById(int $id): ?array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_department
WHERE company_id = :cid AND id = :id LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function create(array $data): int
{
$sth = $this->pdo->prepare(
"INSERT INTO ac_department (company_id, dept_code, dept_name, description, status)
VALUES (:cid, :code, :name, :desc, :status)"
);
$sth->execute([
':cid' => $this->companyId,
':code' => trim($data['dept_code']),
':name' => trim($data['dept_name']),
':desc' => $data['description'] ?? null,
':status' => (int)($data['status'] ?? 1),
]);
return (int)$this->pdo->lastInsertId();
}
public function update(int $id, array $data): void
{
$sth = $this->pdo->prepare(
"UPDATE ac_department SET
dept_code = :code,
dept_name = :name,
description = :desc,
status = :status
WHERE company_id = :cid AND id = :id"
);
$sth->execute([
':cid' => $this->companyId,
':id' => $id,
':code' => trim($data['dept_code']),
':name' => trim($data['dept_name']),
':desc' => $data['description'] ?? null,
':status' => (int)($data['status'] ?? 1),
]);
}
public function delete(int $id): void
{
$sth = $this->pdo->prepare(
"UPDATE ac_department SET status = 0
WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
}
public function getStats(): array
{
$sth = $this->pdo->prepare(
"SELECT COUNT(*) AS total,
SUM(status = 1) AS active,
SUM(status = 0) AS inactive
FROM ac_department WHERE company_id = :cid"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
}
}
@@ -0,0 +1,23 @@
<?php
/**
* FinancialReports
*
* Generates all financial statements from ac_balance + ac_journal_line.
* Read-only — no writes.
*/
class FinancialReports
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: getTrialBalance(string $from_period, string $to_period, ?int $dept_id): array
// TODO: getProfitAndLoss(string $from_period, string $to_period, ?int $dept_id): array
// TODO: getBalanceSheet(string $as_of_period, ?int $dept_id): array
// TODO: getGLMovement(string $account_code, string $from_period, string $to_period): array
}
@@ -0,0 +1,25 @@
<?php
/**
* JournalManager
*
* Create, read, and void journal entries in ac_journal + ac_journal_line.
* Updates ac_balance after each post.
* Never called directly by WMS — goes through PostingManager.
*/
class JournalManager
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: create(array $header, array $lines): int — returns journal_id
// TODO: void(int $journal_id): void
// TODO: getBySource(string $source_type, int $source_id): ?array
// TODO: getById(int $journal_id): ?array
// TODO: updateBalance(string $period, string $account_code, ?int $dept_id, float $debit, float $credit): void
}
@@ -0,0 +1,120 @@
<?php
class PeriodManager
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
public function getAll(): array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_period
WHERE company_id = :cid
ORDER BY period DESC"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
public function getById(int $id): ?array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_period WHERE company_id = :cid AND id = :id LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function isOpen(string $period): bool
{
$sth = $this->pdo->prepare(
"SELECT status FROM ac_period
WHERE company_id = :cid AND period = :period LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':period' => $period]);
$status = $sth->fetchColumn();
return $status === 'open';
}
// Get existing period or auto-create it as open
public function getOrCreate(string $period): array
{
$sth = $this->pdo->prepare(
"SELECT * FROM ac_period WHERE company_id = :cid AND period = :period LIMIT 1"
);
$sth->execute([':cid' => $this->companyId, ':period' => $period]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if ($row) return $row;
$this->pdo->prepare(
"INSERT INTO ac_period (company_id, period, status) VALUES (:cid, :period, 'open')"
)->execute([':cid' => $this->companyId, ':period' => $period]);
return $this->getOrCreate($period);
}
// Create a single period (manual)
public function create(string $period): void
{
$this->pdo->prepare(
"INSERT IGNORE INTO ac_period (company_id, period, status) VALUES (:cid, :period, 'open')"
)->execute([':cid' => $this->companyId, ':period' => $period]);
}
// Bulk-create all 12 months of a year
public function createYear(int $year): void
{
$sth = $this->pdo->prepare(
"INSERT IGNORE INTO ac_period (company_id, period, status) VALUES (:cid, :period, 'open')"
);
for ($m = 1; $m <= 12; $m++) {
$sth->execute([
':cid' => $this->companyId,
':period' => sprintf('%04d-%02d', $year, $m),
]);
}
}
public function open(int $id): void
{
$this->pdo->prepare(
"UPDATE ac_period SET status = 'open', closed_at = NULL
WHERE company_id = :cid AND id = :id AND status = 'closed'"
)->execute([':cid' => $this->companyId, ':id' => $id]);
}
public function close(int $id): void
{
$this->pdo->prepare(
"UPDATE ac_period SET status = 'closed', closed_at = NOW()
WHERE company_id = :cid AND id = :id AND status = 'open'"
)->execute([':cid' => $this->companyId, ':id' => $id]);
}
public function lock(int $id): void
{
$this->pdo->prepare(
"UPDATE ac_period SET status = 'locked', locked_at = NOW()
WHERE company_id = :cid AND id = :id AND status = 'closed'"
)->execute([':cid' => $this->companyId, ':id' => $id]);
}
public function getStats(): array
{
$sth = $this->pdo->prepare(
"SELECT COUNT(*) AS total,
SUM(status = 'open') AS open_count,
SUM(status = 'closed') AS closed_count,
SUM(status = 'locked') AS locked_count
FROM ac_period WHERE company_id = :cid"
);
$sth->execute([':cid' => $this->companyId]);
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
}
}
@@ -0,0 +1,23 @@
<?php
/**
* TaxReportManager
*
* Generates VAT and withholding tax reports from journal lines
* tagged to tax accounts (output VAT, input VAT, WHT).
* Read-only — no writes.
*/
class TaxReportManager
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: getOutputVAT(string $from_period, string $to_period): array
// TODO: getInputVAT(string $from_period, string $to_period): array
// TODO: getWithholdingTax(string $from_period, string $to_period): array
}
@@ -0,0 +1,21 @@
<?php
/**
* InvoicePosting
*
* Builds the journal entry for a posted sales invoice.
* DR: Accounts Receivable
* CR: Revenue + Output VAT
*/
class InvoicePosting
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: build(int $invoice_id): array — returns [header, lines] ready for JournalManager
}
@@ -0,0 +1,21 @@
<?php
/**
* PaymentPosting
*
* Builds the journal entry when a supplier payment is recorded.
* DR: Accounts Payable
* CR: Bank / Cash
*/
class PaymentPosting
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: build(int $payment_id): array — returns [header, lines] ready for JournalManager
}
@@ -0,0 +1,21 @@
<?php
/**
* PurchasePosting
*
* Builds the journal entry when a Purchase Order is received (GRN).
* DR: Inventory
* CR: Accounts Payable
*/
class PurchasePosting
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: build(int $po_id): array — returns [header, lines] ready for JournalManager
}
@@ -0,0 +1,21 @@
<?php
/**
* ReceiptPosting
*
* Builds the journal entry when a customer receipt is recorded.
* DR: Bank / Cash
* CR: Accounts Receivable
*/
class ReceiptPosting
{
private PDO $pdo;
private int $companyId;
public function __construct(PDO $pdo, int $company_id)
{
$this->pdo = $pdo;
$this->companyId = $company_id;
}
// TODO: build(int $receipt_id): array — returns [header, lines] ready for JournalManager
}
+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">Payments</h1>
<p class="mb-0 text-muted">Payments to suppliers</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-credit-card fs-1 mb-3 d-block"></i>
<p class="mb-0">Payments 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">Purchase Invoices</h1>
<p class="mb-0 text-muted">Supplier invoices</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-file-invoice fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Invoices 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">Purchase Orders</h1>
<p class="mb-0 text-muted">Purchase order management</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-clipboard-list fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Orders 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">Purchase Requests</h1>
<p class="mb-0 text-muted">Internal purchase requests</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-notes fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Requests 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">Payments</h1>
<p class="mb-0 text-muted">Cash and bank payments</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-credit-card fs-1 mb-3 d-block"></i>
<p class="mb-0">Payments 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">Receipts</h1>
<p class="mb-0 text-muted">Cash and bank receipts</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-cash fs-1 mb-3 d-block"></i>
<p class="mb-0">Receipts coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+51 -7
View File
@@ -1,7 +1,20 @@
<?php
$current_page = basename($_SERVER['PHP_SELF']);
$master_role = $_SESSION['login_role'] ?? 'viewer';
$cp = $_SERVER['PHP_SELF'];
function master_active(...$frags) {
global $cp;
foreach ($frags as $f) { if (str_contains($cp, $f)) return 'active'; }
return '';
}
$master_role = $_SESSION['login_role'] ?? 'viewer';
$master_can_admin = in_array($master_role, ['owner', 'admin'], true);
$master_app_access = $_SESSION['login_app_access'] ?? 'wms';
$master_show_accounting = in_array($master_app_access, ['all', 'accounting']);
$master_back_url = ($_SESSION['login_current_app'] ?? 'wms') === 'accounting'
? $server_url . 'ac_dashboard/index.php'
: $server_url . 'dashboard/index.php';
$master_back_label = ($_SESSION['login_current_app'] ?? 'wms') === 'accounting'
? 'Back to Accounting'
: 'Back to WMS';
?>
<!-- SIDEBAR -->
@@ -23,7 +36,7 @@
</li>
<li>
<a class="nav-link <?php echo in_array($current_page, ['product.php', 'manage_product.php', 'manage_category.php']) ? 'active' : ''; ?>"
<a class="nav-link <?php echo master_active('/inventory/product', '/inventory/manage_product', '/inventory/manage_category'); ?>"
href="<?php echo $server_url?>inventory/product.php">
<i class="ti ti-category"></i>
<span class="nav-text">Products</span>
@@ -32,7 +45,7 @@
<?php if ($master_can_admin): ?>
<li>
<a class="nav-link <?php echo in_array($current_page, ['warehouse.php', 'manage_warehouse.php', 'manage_storage.php']) ? 'active' : ''; ?>"
<a class="nav-link <?php echo master_active('/inventory/warehouse', '/inventory/manage_warehouse', '/inventory/manage_storage'); ?>"
href="<?php echo $server_url?>inventory/warehouse.php">
<i class="ti ti-map-pin"></i>
<span class="nav-text">Warehouse Location</span>
@@ -41,22 +54,53 @@
<?php endif; ?>
<li>
<a class="nav-link <?php echo in_array($current_page, ['contact.php', 'manage_contact.php', 'manage_contact_type.php']) ? 'active' : ''; ?>"
<a class="nav-link <?php echo master_active('/contact/'); ?>"
href="<?php echo $server_url?>contact/contact.php">
<i class="ti ti-users"></i>
<span class="nav-text">Contact</span>
</a>
</li>
<!-- Accounting -->
<?php if ($master_show_accounting): ?>
<li class="nav-text-space mt-2">
<small class="nav-text text-muted">Accounting</small>
</li>
<li>
<a class="nav-link <?php echo master_active('/accounting/chart_of_accounts', '/accounting/manage_account'); ?>"
href="<?php echo $server_url?>accounting/chart_of_accounts.php">
<i class="ti ti-list-tree"></i>
<span class="nav-text">Chart of Accounts</span>
</a>
</li>
<li>
<a class="nav-link <?php echo master_active('/accounting/departments', '/accounting/manage_department'); ?>"
href="<?php echo $server_url?>accounting/departments.php">
<i class="ti ti-building"></i>
<span class="nav-text">Departments</span>
</a>
</li>
<li>
<a class="nav-link <?php echo master_active('/accounting/periods'); ?>"
href="<?php echo $server_url?>accounting/periods.php">
<i class="ti ti-calendar"></i>
<span class="nav-text">Accounting Periods</span>
</a>
</li>
<?php endif; ?>
<!-- Navigation -->
<li class="nav-text-space mt-2">
<small class="nav-text text-muted">Navigation</small>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>dashboard/index.php">
<a class="nav-link" href="<?php echo $master_back_url; ?>">
<i class="ti ti-arrow-left"></i>
<span class="nav-text">Back to App</span>
<span class="nav-text"><?php echo $master_back_label; ?></span>
</a>
</li>
+24 -18
View File
@@ -1,6 +1,12 @@
<?php
$sidebar_role = $_SESSION['login_role'] ?? 'viewer';
$sidebar_role = $_SESSION['login_role'] ?? 'viewer';
$sidebar_can_staff = in_array($sidebar_role, ['owner', 'admin', 'staff'], true);
$cp = $_SERVER['PHP_SELF'];
function wms_active(...$frags) {
global $cp;
foreach ($frags as $f) { if (str_contains($cp, $f)) return 'active'; }
return '';
}
?>
<!-- SIDEBAR -->
<aside id="sidebar" class="sidebar overflow-y-auto">
@@ -21,14 +27,14 @@
</li>
<li>
<a class="nav-link active" href="<?php echo $server_url?>dashboard/index.php">
<a class="nav-link <?php echo wms_active('/dashboard/index'); ?>" href="<?php echo $server_url?>dashboard/index.php">
<i class="ti ti-home"></i>
<span class="nav-text">Dashboard</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>dashboard/low_stock_products.php">
<a class="nav-link <?php echo wms_active('low_stock'); ?>" href="<?php echo $server_url?>dashboard/low_stock_products.php">
<i class="ti ti-alert-triangle"></i>
<span class="nav-text">Low Stock</span>
</a>
@@ -40,7 +46,7 @@
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_overview.php">
<a class="nav-link <?php echo wms_active('stock_overview'); ?>" href="<?php echo $server_url?>ics/stock_overview.php">
<i class="ti ti-clipboard"></i>
<span class="nav-text">Stock Overview</span>
</a>
@@ -48,35 +54,35 @@
<?php if ($sidebar_can_staff): ?>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_in.php">
<a class="nav-link <?php echo wms_active('stock_in'); ?>" href="<?php echo $server_url?>ics/stock_in.php">
<i class="ti ti-package-import"></i>
<span class="nav-text">Stock In</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_out.php">
<a class="nav-link <?php echo wms_active('stock_out'); ?>" href="<?php echo $server_url?>ics/stock_out.php">
<i class="ti ti-package-export"></i>
<span class="nav-text">Stock Out</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/stock_transfer.php">
<a class="nav-link <?php echo wms_active('stock_transfer'); ?>" href="<?php echo $server_url?>ics/stock_transfer.php">
<i class="ti ti-package"></i>
<span class="nav-text">Stock Transfer</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/sku_barcode_label.php">
<a class="nav-link <?php echo wms_active('sku_barcode'); ?>" href="<?php echo $server_url?>ics/sku_barcode_label.php">
<i class="ti ti-barcode"></i>
<span class="nav-text">SKU Labels</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/location_barcode_label.php">
<a class="nav-link <?php echo wms_active('location_barcode'); ?>" href="<?php echo $server_url?>ics/location_barcode_label.php">
<i class="ti ti-map-pin-code"></i>
<span class="nav-text">Location Labels</span>
</a>
@@ -89,28 +95,28 @@
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>order/order.php">
<a class="nav-link <?php echo wms_active('/order/order', '/order/manage_order', '/order/confirm_order'); ?>" href="<?php echo $server_url?>order/order.php">
<i class="ti ti-shopping-cart"></i>
<span class="nav-text">Sales Orders</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>order/return.php">
<a class="nav-link <?php echo wms_active('/order/return', 'manage_return'); ?>" href="<?php echo $server_url?>order/return.php">
<i class="ti ti-arrow-back-up"></i>
<span class="nav-text">Returns</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>order/invoice.php">
<a class="nav-link <?php echo wms_active('/order/invoice', 'manage_invoice', 'print_invoice'); ?>" href="<?php echo $server_url?>order/invoice.php">
<i class="ti ti-file-invoice"></i>
<span class="nav-text">Sales Invoices</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>po/po.php">
<li>
<a class="nav-link <?php echo wms_active('/po/'); ?>" href="<?php echo $server_url?>po/po.php">
<i class="ti ti-clipboard-list"></i>
<span class="nav-text">Purchase Orders</span>
</a>
@@ -122,28 +128,28 @@
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>reports/stock_movement.php">
<a class="nav-link <?php echo wms_active('stock_movement'); ?>" href="<?php echo $server_url?>reports/stock_movement.php">
<i class="ti ti-chart-bar"></i>
<span class="nav-text">Stock Movement</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>reports/expired_stock.php">
<a class="nav-link <?php echo wms_active('expired_stock'); ?>" href="<?php echo $server_url?>reports/expired_stock.php">
<i class="ti ti-clock"></i>
<span class="nav-text">Expired Stock</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>reports/occupy_rack.php">
<a class="nav-link <?php echo wms_active('occupy_rack'); ?>" href="<?php echo $server_url?>reports/occupy_rack.php">
<i class="ti ti-layout-grid"></i>
<span class="nav-text">Rack Occupancy</span>
</a>
</li>
<li>
<a class="nav-link" href="<?php echo $server_url?>reports/product_lot.php">
<a class="nav-link <?php echo wms_active('product_lot'); ?>" href="<?php echo $server_url?>reports/product_lot.php">
<i class="ti ti-packages"></i>
<span class="nav-text">Product Lots</span>
</a>
+183
View File
@@ -0,0 +1,183 @@
<?php
$sidebar_role = $_SESSION['login_role'] ?? 'viewer';
$sidebar_can_staff = in_array($sidebar_role, ['owner', 'admin', 'staff'], true);
$cp = $_SERVER['PHP_SELF'];
function ac_active(...$frags) {
global $cp;
foreach ($frags as $f) { if (str_contains($cp, $f)) return 'active'; }
return '';
}
?>
<!-- SIDEBAR -->
<aside id="sidebar" class="sidebar overflow-y-auto">
<div class="logo-area">
<a href="index.html" class="d-inline-flex">
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
<span class="logo-text ms-2">
<img src="<?php echo $server_url?>assets/images/logo.png" alt="" height="40" style="padding: 5px 0;"/>
</span>
</a>
</div>
<ul class="nav flex-column">
<!-- Main -->
<li class="nav-text-space">
<small class="nav-text text-muted">Main</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('/ac_dashboard/'); ?>" href="<?php echo $server_url?>ac_dashboard/index.php">
<i class="ti ti-home"></i>
<span class="nav-text">Dashboard</span>
</a>
</li>
<!-- Revenue -->
<li class="nav-text-space">
<small class="nav-text text-muted">Revenue</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('/revenue/quotation'); ?>" href="<?php echo $server_url?>revenue/quotation.php">
<i class="ti ti-file-description"></i>
<span class="nav-text">Quotations</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/revenue/order'); ?>" href="<?php echo $server_url?>revenue/order.php">
<i class="ti ti-shopping-cart"></i>
<span class="nav-text">Sales Orders</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/revenue/invoice'); ?>" href="<?php echo $server_url?>revenue/invoice.php">
<i class="ti ti-file-invoice"></i>
<span class="nav-text">Sales Invoices</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/revenue/receipt'); ?>" href="<?php echo $server_url?>revenue/receipt.php">
<i class="ti ti-cash"></i>
<span class="nav-text">Receipts</span>
</a>
</li>
<!-- Purchase -->
<li class="nav-text-space">
<small class="nav-text text-muted">Purchase</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('/expense/purchase_request'); ?>" href="<?php echo $server_url?>expense/purchase_request.php">
<i class="ti ti-notes"></i>
<span class="nav-text">Purchase Requests</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/expense/purchase_order'); ?>" href="<?php echo $server_url?>expense/purchase_order.php">
<i class="ti ti-clipboard-list"></i>
<span class="nav-text">Purchase Orders</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/expense/purchase_invoice'); ?>" href="<?php echo $server_url?>expense/purchase_invoice.php">
<i class="ti ti-file-invoice"></i>
<span class="nav-text">Purchase Invoices</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/expense/payment'); ?>" href="<?php echo $server_url?>expense/payment.php">
<i class="ti ti-credit-card"></i>
<span class="nav-text">Payments</span>
</a>
</li>
<!-- Finance -->
<li class="nav-text-space">
<small class="nav-text text-muted">Finance</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('/finance/receipt'); ?>" href="<?php echo $server_url?>finance/receipt.php">
<i class="ti ti-cash"></i>
<span class="nav-text">Receipts</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/finance/payment'); ?>" href="<?php echo $server_url?>finance/payment.php">
<i class="ti ti-credit-card"></i>
<span class="nav-text">Payments</span>
</a>
</li>
<!-- Journal Entries -->
<li class="nav-text-space">
<small class="nav-text text-muted">Journal Entries</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('/journal/index'); ?>" href="<?php echo $server_url?>journal/index.php">
<i class="ti ti-books"></i>
<span class="nav-text">Journal List</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('/journal/new'); ?>" href="<?php echo $server_url?>journal/new.php">
<i class="ti ti-plus"></i>
<span class="nav-text">New Entry</span>
</a>
</li>
<!-- Reports -->
<li class="nav-text-space">
<small class="nav-text text-muted">Reports</small>
</li>
<li>
<a class="nav-link <?php echo ac_active('trial_balance'); ?>" href="<?php echo $server_url?>accounting/trial_balance.php">
<i class="ti ti-scale"></i>
<span class="nav-text">Trial Balance</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('pl_statement'); ?>" href="<?php echo $server_url?>accounting/pl_statement.php">
<i class="ti ti-chart-line"></i>
<span class="nav-text">P&amp;L Statement</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('balance_sheet'); ?>" href="<?php echo $server_url?>accounting/balance_sheet.php">
<i class="ti ti-table"></i>
<span class="nav-text">Balance Sheet</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('gl_movement'); ?>" href="<?php echo $server_url?>accounting/gl_movement.php">
<i class="ti ti-arrows-exchange"></i>
<span class="nav-text">GL Movement</span>
</a>
</li>
<li>
<a class="nav-link <?php echo ac_active('vat_report'); ?>" href="<?php echo $server_url?>accounting/vat_report.php">
<i class="ti ti-receipt-tax"></i>
<span class="nav-text">VAT Report</span>
</a>
</li>
<li><br><br><br><br></li>
</ul>
</aside>
+59
View File
@@ -17,6 +17,36 @@ $topbar_picture = $_SESSION['login_profile_picture'] ?? '';
$topbar_avatar_src = $topbar_picture
? htmlspecialchars($server_url . 'uploads/profile/' . $topbar_picture, ENT_QUOTES, 'UTF-8')
: htmlspecialchars($server_url . 'assets/images/avatar-1.jpg', ENT_QUOTES, 'UTF-8');
// ── App switcher ──────────────────────────────────────────────────────────────
$topbar_app_access = $_SESSION['login_app_access'] ?? 'wms';
// Detect which app this page belongs to.
// Master-data pages (/accounting/, /inventory/, /contact/, /setting/) are neutral — don't change the session.
$_wms_paths = ['/dashboard/', '/ics/', '/order/', '/po/', '/reports/'];
$_ac_paths = ['/ac_dashboard/', '/revenue/', '/expense/', '/finance/', '/journal/'];
$topbar_current_app = null; // null = neutral page (master data / setting)
foreach ($_wms_paths as $_p) {
if (str_contains($_SERVER['PHP_SELF'], $_p)) { $topbar_current_app = 'wms'; break; }
}
if (!$topbar_current_app) {
foreach ($_ac_paths as $_p) {
if (str_contains($_SERVER['PHP_SELF'], $_p)) { $topbar_current_app = 'accounting'; break; }
}
}
// Only persist to session when on a definitive app page
if ($topbar_current_app) {
$_SESSION['login_current_app'] = $topbar_current_app;
}
// Default session on first visit or neutral pages
if (empty($_SESSION['login_current_app'])) {
$_SESSION['login_current_app'] = ($topbar_app_access === 'accounting') ? 'accounting' : 'wms';
}
$topbar_show_wms = in_array($topbar_app_access, ['all', 'wms']);
$topbar_show_accounting = in_array($topbar_app_access, ['all', 'accounting']);
?>
<div id="overlay" class="overlay"></div>
@@ -59,6 +89,35 @@ $topbar_avatar_src = $topbar_picture
<!-- Navbar nav -->
<ul class="list-unstyled d-flex align-items-center mb-0 gap-1">
<!-- App switcher -->
<?php if ($topbar_show_wms && $topbar_show_accounting): ?>
<li class="dropdown">
<a class="position-relative btn-icon btn-sm btn-light btn rounded-circle" data-bs-toggle="dropdown"
aria-expanded="false" href="#" role="button" title="Switch App">
<i class="ti ti-layout-grid" style="font-size:18px;"></i>
</a>
<div class="dropdown-menu dropdown-menu-end p-2" style="min-width:170px;">
<?php $_sess_app = $_SESSION['login_current_app'] ?? 'wms'; ?>
<a href="<?php echo $server_url?>dashboard/index.php"
class="dropdown-item rounded d-flex align-items-center gap-2 py-2 <?php echo $_sess_app === 'wms' ? 'active' : ''; ?>">
<i class="ti ti-box"></i>
<span>WMS</span>
<?php if ($_sess_app === 'wms'): ?>
<i class="ti ti-check ms-auto text-success"></i>
<?php endif; ?>
</a>
<a href="<?php echo $server_url?>ac_dashboard/index.php"
class="dropdown-item rounded d-flex align-items-center gap-2 py-2 <?php echo $_sess_app === 'accounting' ? 'active' : ''; ?>">
<i class="ti ti-calculator"></i>
<span>Accounting</span>
<?php if ($_sess_app === 'accounting'): ?>
<i class="ti ti-check ms-auto text-success"></i>
<?php endif; ?>
</a>
</div>
</li>
<?php endif; ?>
<!-- Bell icon -->
<li>
<a class="position-relative btn-icon btn-sm btn-light btn rounded-circle" data-bs-toggle="dropdown"
+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">Journal List</h1>
<p class="mb-0 text-muted">All journal entries</p>
</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>
</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">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>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+1
View File
@@ -124,6 +124,7 @@ $_SESSION["login_name"] = $temp["name"];
$_SESSION["login_surname"] = $temp["surname"];
$_SESSION["login_company_id"] = $temp["default_company"];
$_SESSION["login_profile_picture"] = $temp["profile_picture"] ?? '';
$_SESSION["login_app_access"] = $temp["app_access"] ?? 'wms';
$role_sth = $pdo1->prepare(
"SELECT role FROM company_map_user
+5 -2
View File
@@ -2,9 +2,12 @@
require '../session.php';
require '../config.php';
require '../include_header.php';
// successful login
// successful login — redirect based on app_access
if(!empty($_SESSION["login_status"])){
header('Location: '.$server_url.'dashboard/index.php');
$redirect = ($_SESSION['login_app_access'] ?? 'wms') === 'accounting'
? $server_url . 'ac_dashboard/index.php'
: $server_url . 'dashboard/index.php';
header('Location: ' . $redirect);
exit;
}
+11 -2
View File
@@ -8,11 +8,20 @@
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
try {
$new_id = null;
$new_id = null;
$quotation_id = (int)($data['quotation_id'] ?? 0);
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $quotation_id, &$new_id) {
$order = new OrderManager($pdo, $company_id);
$new_id = $order->saveOrder($data, $logging);
// On new order created from a quotation: link both records
if ($new_id && $quotation_id > 0) {
$pdo->prepare(
"UPDATE ac_quotation SET order_id = :order_id, status = 5
WHERE id = :id AND company_id = :cid AND status = 2"
)->execute([':order_id' => $new_id, ':id' => $quotation_id, ':cid' => $company_id]);
}
});
$answer['success'] = 1;
+14 -6
View File
@@ -244,6 +244,7 @@
// ── Status helpers ────────────────────────────────────────────────────────
function order_status_badge(status) {
const map = {
'-2': '<span class="badge bg-danger">Pending — Awaiting Warehouse</span>',
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Confirmed</span>',
@@ -380,19 +381,26 @@
has_active_invoice = has_active_invoice || false;
returnable_count = returnable_count ?? 1;
fulfillment_status = parseInt(fulfillment_status) || 0;
var is_pending = status === -2;
var is_draft = status === 0;
var can_confirm = status === 0;
var can_cancel = status === 0 || status === 1;
var is_editable = is_pending || is_draft; // both Pending and Draft are editable
var can_confirm = status === 0; // only Draft can be confirmed (not Pending)
var can_cancel = status === -2 || status === 0 || status === 1;
var is_cancelled = status === -1;
var is_completed = status === 3;
var can_fulfill = status >= 1 && !is_cancelled && !is_completed;
// Edit controls
$('#btn_add_item, #contact, #order_date, #notes, #discount, #tax, #shipping_fee')
.prop('disabled', !is_draft);
$('#shipping_tracking_number').prop('disabled', !(is_draft || can_fulfill));
$('.item_sku, .item_qty, .item_price, .item_warehouse').prop('disabled', !is_draft);
$('a[onclick^="remove_item"]').toggleClass('d-none', !is_draft);
.prop('disabled', !is_editable);
$('#shipping_tracking_number').prop('disabled', !(is_editable || can_fulfill));
$('.item_sku, .item_qty, .item_price, .item_warehouse').prop('disabled', !is_editable);
$('a[onclick^="remove_item"]').toggleClass('d-none', !is_editable);
// Pending hint
if (is_pending) {
$('#page_subtitle').text('Assign a warehouse for each item, then save to move this order to Draft.');
}
// Action buttons
set_btn_state('#btn_save', is_draft, 'Only draft orders can be edited');
+3 -1
View File
@@ -104,6 +104,7 @@
<div class="col-lg-3">
<select id="filter_status" class="form-select">
<option value="">All Status</option>
<option value="-2">Pending</option>
<option value="-1">Cancelled</option>
<option value="0">Draft</option>
<option value="1">Confirmed</option>
@@ -169,13 +170,14 @@
function order_status_badge(status) {
status = parseInt(status);
const map = {
'-2': '<span class="badge bg-danger">Pending</span>',
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Confirmed</span>',
'2': '<span class="badge bg-primary">Processing</span>',
'3': '<span class="badge bg-success">Completed</span>',
};
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
return map[String(status)] ?? '<span class="badge bg-light text-dark">—</span>';
}
function payment_status_badge(status) {
@@ -0,0 +1,87 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
$quotation_id = (int)($data['quotation_id'] ?? 0);
if (!$quotation_id) {
exit(json_encode(['success' => 0, 'message' => 'Quotation ID required']));
}
// Load quotation — must be Accepted (status=2)
$sth = $pdo2->prepare(
"SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.id = :id AND q.company_id = :cid AND q.status = 2
LIMIT 1"
);
$sth->execute([':id' => $quotation_id, ':cid' => $company_id]);
$q = $sth->fetch(PDO::FETCH_ASSOC);
if (!$q) {
exit(json_encode(['success' => 0, 'message' => 'Quotation not found or not in Accepted status']));
}
// Check not already converted
if ((int)$q['order_id'] > 0) {
exit(json_encode(['success' => 0, 'message' => 'Quotation has already been converted to an order']));
}
// Map quotation items → order items (warehouse_id left 0 = to be filled by WMS)
$qt_items = json_decode($q['items'] ?? '[]', true) ?: [];
$ord_items = [];
foreach ($qt_items as $i => $qi) {
$ord_items[] = [
'item_id' => $i,
'product_sku' => $qi['product_sku'] ?? '',
'product_name' => $qi['description'] ?? '',
'warehouse_id' => 0,
'zone' => '',
'aisle' => '',
'rack' => '',
'lot_number' => '',
'serial_number' => '',
'quantity' => (float)($qi['quantity'] ?? 0),
'price' => (float)($qi['unit_price'] ?? 0),
'total_price' => (float)($qi['total_price'] ?? 0),
'stock_out_id' => 0,
];
}
$order_data = [
'quotation_id' => $quotation_id,
'contact_id' => (int)$q['contact_id'],
'order_date' => date('Y-m-d'),
'items' => $ord_items,
'discount' => (float)$q['discount'],
'tax' => (float)$q['tax'],
'shipping_fee' => 0,
'shipping_tracking_number' => '',
'notes' => $q['notes'] ?? '',
];
try {
$new_order_id = null;
dbTransaction($pdo2, function($pdo) use ($order_data, $company_id, $logging, $quotation_id, &$new_order_id) {
$order = new OrderManager($pdo, $company_id);
$new_order_id = $order->saveOrder($order_data, $logging);
// Mark quotation as Converted and link order_id
$pdo->prepare(
"UPDATE ac_quotation SET status = 5, order_id = :oid
WHERE id = :id AND company_id = :cid"
)->execute([':oid' => $new_order_id, ':id' => $quotation_id, ':cid' => $company_id]);
});
$answer['success'] = 1;
$answer['message'] = 'Order created successfully';
$answer['order_id'] = $new_order_id;
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
+107
View File
@@ -0,0 +1,107 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$action = $data['action'] ?? '';
$id = (int)($data['id'] ?? 0);
// ── Generate quotation number ─────────────────────────────────────────────────
function generate_quotation_number(PDO $pdo, int $cid): string {
$prefix = 'QT-' . date('Ym') . '-';
$sql = "SELECT quotation_number FROM ac_quotation
WHERE company_id = :cid AND quotation_number LIKE :prefix
ORDER BY id DESC LIMIT 1";
$sth = $pdo->prepare($sql);
$sth->execute([':cid' => $cid, ':prefix' => $prefix . '%']);
$last = $sth->fetchColumn();
$seq = $last ? ((int)substr($last, -4) + 1) : 1;
return $prefix . str_pad($seq, 4, '0', STR_PAD_LEFT);
}
// ── Compute totals from items ─────────────────────────────────────────────────
function compute_totals(array $items, float $discount, float $tax): array {
$subtotal = array_sum(array_map(fn($i) => (float)($i['total_price'] ?? 0), $items));
$grand = $subtotal - $discount + $tax;
return [round($subtotal, 4), round(max(0, $grand), 4)];
}
if ($action === 'create') {
$items = json_decode($data['items'] ?? '[]', true) ?: [];
$discount = (float)($data['discount'] ?? 0);
$tax = (float)($data['tax'] ?? 0);
[$subtotal, $grand] = compute_totals($items, $discount, $tax);
$qnum = generate_quotation_number($pdo2, $company_id);
$sql = "INSERT INTO ac_quotation
(company_id, uuid, quotation_number, contact_id, quotation_date, valid_until,
status, subtotal, discount, tax, grand_total, items, notes, log, created_at)
VALUES
(:cid, :uuid, :qnum, :contact_id, :qdate, :valid_until,
0, :sub, :disc, :tax, :grand, :items, :notes, :log, NOW())";
$sth = $pdo2->prepare($sql);
$sth->execute([
':cid' => $company_id,
':uuid' => $uuid,
':qnum' => $qnum,
':contact_id' => (int)($data['contact_id'] ?? 0),
':qdate' => $data['quotation_date'] ?: null,
':valid_until'=> $data['valid_until'] ?: null,
':sub' => $subtotal,
':disc' => $discount,
':tax' => $tax,
':grand' => $grand,
':items' => json_encode($items),
':notes' => $data['notes'] ?? '',
':log' => json_encode($logging),
]);
$answer['success'] = 1;
$answer['message'] = 'Quotation created';
$answer['new_id'] = (int)$pdo2->lastInsertId();
$answer['quotation_number'] = $qnum;
} elseif ($action === 'update') {
if (!$id) { exit(json_encode(['success' => 0, 'message' => 'ID required'])); }
$items = json_decode($data['items'] ?? '[]', true) ?: [];
$discount = (float)($data['discount'] ?? 0);
$tax = (float)($data['tax'] ?? 0);
[$subtotal, $grand] = compute_totals($items, $discount, $tax);
$sql = "UPDATE ac_quotation SET
contact_id = :contact_id,
quotation_date = :qdate,
valid_until = :valid_until,
subtotal = :sub,
discount = :disc,
tax = :tax,
grand_total = :grand,
items = :items,
notes = :notes
WHERE id = :id AND company_id = :cid AND status = 0";
$sth = $pdo2->prepare($sql);
$sth->execute([
':contact_id' => (int)($data['contact_id'] ?? 0),
':qdate' => $data['quotation_date'] ?: null,
':valid_until' => $data['valid_until'] ?: null,
':sub' => $subtotal,
':disc' => $discount,
':tax' => $tax,
':grand' => $grand,
':items' => json_encode($items),
':notes' => $data['notes'] ?? '',
':id' => $id,
':cid' => $company_id,
]);
$answer['success'] = 1;
$answer['message'] = 'Quotation updated';
} else {
$answer['message'] = 'Unknown action';
}
exit(json_encode($answer));
@@ -0,0 +1,19 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$sql = "SELECT
COUNT(*) AS total,
SUM(status = 0) AS draft,
SUM(status = 1) AS sent,
SUM(status = 2) AS accepted,
SUM(status = 3) AS rejected
FROM ac_quotation
WHERE company_id = :cid";
$sth = $pdo2->prepare($sql);
$sth->execute([':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
$answer['success'] = 1;
$answer['output'] = $row;
exit(json_encode($answer));
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$id = (int)($data['id'] ?? 0);
if ($id) {
// Single quotation
$sql = "SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.id = :id AND q.company_id = :cid
LIMIT 1";
$sth = $pdo2->prepare($sql);
$sth->execute([':id' => $id, ':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) { exit(json_encode(['success' => 0, 'message' => 'Not found'])); }
$row['items'] = json_decode($row['items'] ?? '[]', true);
$answer['output'] = $row;
} else {
// List
$sql = "SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.company_id = :cid
ORDER BY q.id DESC";
$sth = $pdo2->prepare($sql);
$sth->execute([':cid' => $company_id]);
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
}
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,42 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
if (!$id || !$action) {
exit(json_encode(['success' => 0, 'message' => 'ID and action required']));
}
// Load current status
$sth = $pdo2->prepare("SELECT status FROM ac_quotation WHERE id = :id AND company_id = :cid LIMIT 1");
$sth->execute([':id' => $id, ':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) { exit(json_encode(['success' => 0, 'message' => 'Not found'])); }
$cur = (int)$row['status'];
$transitions = [
'send' => ['from' => [0], 'to' => 1, 'label' => 'Sent'],
'accept' => ['from' => [1], 'to' => 2, 'label' => 'Accepted'],
'reject' => ['from' => [1], 'to' => 3, 'label' => 'Rejected'],
'reopen' => ['from' => [1,2,3],'to' => 0, 'label' => 'Draft'],
'cancel' => ['from' => [0,1], 'to' => -1,'label' => 'Cancelled'],
];
if (!isset($transitions[$action])) {
exit(json_encode(['success' => 0, 'message' => 'Invalid action']));
}
$t = $transitions[$action];
if (!in_array($cur, $t['from'], true)) {
exit(json_encode(['success' => 0, 'message' => 'Transition not allowed from current status']));
}
$sth = $pdo2->prepare("UPDATE ac_quotation SET status = :status WHERE id = :id AND company_id = :cid");
$sth->execute([':status' => $t['to'], ':id' => $id, ':cid' => $company_id]);
$answer['success'] = 1;
$answer['message'] = 'Status updated to ' . $t['label'];
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">Sales Invoices</h1>
<p class="mb-0 text-muted">Sales invoice management</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-file-invoice fs-1 mb-3 d-block"></i>
<p class="mb-0">Sales Invoices coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+500
View File
@@ -0,0 +1,500 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$quotation_id = (int)($_GET['id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1" id="page_title">
<?php echo $quotation_id ? 'Quotation Detail' : 'New Quotation'; ?>
</h1>
<p class="mb-0 text-muted" id="page_subtitle">
<?php echo $quotation_id ? '' : 'Create a new sales quotation'; ?>
</p>
</div>
<a href="<?php echo $server_url?>revenue/quotation.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: Quotation form -->
<div class="col-lg-8">
<!-- Header card -->
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Quotation Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Contact</label>
<input type="text" id="contact" class="form-control"
placeholder="Search contact..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Quotation Date</label>
<input type="text" id="quotation_date" class="form-control"
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Valid Until</label>
<input type="text" id="valid_until" class="form-control"
placeholder="DD/MM/YYYY (optional)" autocomplete="off">
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Notes / Terms</label>
<textarea id="notes" class="form-control" rows="2"
placeholder="Payment terms, delivery conditions, notes..."></textarea>
</div>
</div>
</div>
</div>
<!-- Items card -->
<div class="card mb-5">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Line Items</h2>
<button class="btn btn-sm btn-outline-primary" id="btn_add_item"
onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Line
</button>
</div>
<div class="table-responsive">
<table class="table mb-0" id="item_table">
<thead class="table-light">
<tr>
<th style="width:120px;">SKU</th>
<th>Description</th>
<th style="width:90px;">Qty</th>
<th style="width:120px;">Unit Price</th>
<th style="width:120px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right: Status + Summary + Actions -->
<div class="col-lg-4">
<!-- Status card (existing quotation) -->
<div class="card mb-5 d-none" id="status_card">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Status</h2>
<div id="badge_status" class="mb-2"></div>
<p class="mb-0 small fw-semibold text-muted" id="quotation_number_display"></p>
</div>
</div>
<!-- Totals -->
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Summary</h2>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span id="summary_subtotal">0.00</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">Discount</span>
<input type="number" id="discount" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted">Tax (VAT)</span>
<input type="number" id="tax" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()">
</div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5">
<span>Grand Total</span>
<span id="summary_grand_total">0.00</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<button class="btn btn-primary w-100" id="btn_save" onclick="save_quotation()">
<i class="ti ti-device-floppy me-1"></i>Save Draft
</button>
<button class="btn btn-info text-white w-100 d-none" id="btn_send"
onclick="change_status('send')">
<i class="ti ti-send me-1"></i>Mark as Sent
</button>
<button class="btn btn-success w-100 d-none" id="btn_accept"
onclick="change_status('accept')">
<i class="ti ti-circle-check me-1"></i>Mark as Accepted
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_reject"
onclick="change_status('reject')">
<i class="ti ti-x me-1"></i>Mark as Rejected
</button>
<button class="btn btn-outline-secondary w-100 d-none" id="btn_reopen"
onclick="change_status('reopen')">
<i class="ti ti-refresh me-1"></i>Reopen as Draft
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel"
onclick="change_status('cancel')">
<i class="ti ti-ban me-1"></i>Cancel Quotation
</button>
<hr class="d-none" id="hr_convert">
<a href="javascript:void(0);" class="btn btn-success w-100 d-none" id="btn_convert"
onclick="convert_to_order()">
<i class="ti ti-transfer me-1"></i>Convert to Order
</a>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var quotation_id = <?php echo $quotation_id; ?>;
var quotation_data = null;
var item_index = 0;
var status_badge = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Sent</span>',
'2': '<span class="badge bg-success">Accepted</span>',
'3': '<span class="badge bg-danger">Rejected</span>',
'5': '<span class="badge bg-primary">Converted</span>',
};
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
var subtotal = 0;
$('#item_tbody tr').each(function() {
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var total = qty * price;
$(this).find('.item_total').text(format_number(total, 2));
subtotal += total;
});
var discount = parseFloat($('#discount').val()) || 0;
var tax = parseFloat($('#tax').val()) || 0;
var grand = subtotal - discount + tax;
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_grand_total').text(format_number(grand, 2));
}
// ── Add item row ──────────────────────────────────────────────────────────
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_sku"
placeholder="SKU..." value="${escape_html(item.product_sku || '')}" autocomplete="off">
</td>
<td>
<input type="text" class="form-control form-control-sm item_desc"
placeholder="Description..." value="${escape_html(item.description || '')}" autocomplete="off">
</td>
<td>
<input type="number" class="form-control form-control-sm item_qty"
value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_price"
value="${item.unit_price || 0}" min="0" step="any" oninput="recalc_totals()">
</td>
<td class="item_total text-end align-middle">
${format_number((item.quantity || 1) * (item.unit_price || 0), 2)}
</td>
<td class="align-middle">
<a href="javascript:void(0);" class="link-danger remove_item_btn" onclick="remove_item(${idx})">
<i class="ti ti-trash fs-5"></i>
</a>
</td>
</tr>`;
$('#item_tbody').append(row);
recalc_totals();
}
function remove_item(idx) {
$(`#item_row_${idx}`).remove();
recalc_totals();
}
// ── Collect items from DOM ────────────────────────────────────────────────
function collect_items() {
var items = [];
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var desc = $(this).find('.item_desc').val().trim();
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
if (!desc && !sku && qty === 0) return;
items.push({
product_sku: sku,
description: desc,
quantity: qty,
unit_price: price,
total_price: parseFloat((qty * price).toFixed(4)),
});
});
return items;
}
// ── Set UI mode based on status ───────────────────────────────────────────
function set_mode(status) {
status = parseInt(status);
var is_draft = status === 0;
var is_sent = status === 1;
var is_accepted = status === 2;
var is_converted = status === 5;
var is_closed = status >= 2 || status === -1;
var is_editable = is_draft;
// Edit controls
$('#contact, #quotation_date, #valid_until, #notes, #discount, #tax, #btn_add_item')
.prop('disabled', !is_editable);
$('.item_sku, .item_desc, .item_qty, .item_price').prop('disabled', !is_editable);
$('.remove_item_btn').toggleClass('d-none', !is_editable);
// Action buttons
$('#btn_save').toggleClass('d-none', !is_draft);
$('#btn_send').toggleClass('d-none', !is_draft);
$('#btn_accept').toggleClass('d-none', !is_sent);
$('#btn_reject').toggleClass('d-none', !is_sent);
$('#btn_reopen').toggleClass('d-none', is_draft || is_closed);
$('#btn_cancel').toggleClass('d-none', is_closed || (is_draft && !quotation_id));
// Convert to Order — only on Accepted, not yet converted
var show_convert = is_accepted && !is_converted;
$('#hr_convert').toggleClass('d-none', !show_convert);
$('#btn_convert').toggleClass('d-none', !show_convert);
}
// ── Load existing quotation ───────────────────────────────────────────────
function retrieve_quotation() {
if (!quotation_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/retrieve_quotation.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: quotation_id },
onSuccess: function(res) {
quotation_data = res.output;
var q = quotation_data;
$('#page_title').text(q.quotation_number);
$('#page_subtitle').text('');
$('#status_card').removeClass('d-none');
$('#badge_status').html(status_badge[String(q.status)] || q.status);
$('#quotation_number_display').text(q.quotation_number);
$('#contact').val(q.contact_name || '');
$('#contact_id').val(q.contact_id || 0);
$('#quotation_date').val(q.quotation_date ? format_date(q.quotation_date) : '');
$('#valid_until').val(q.valid_until ? format_date(q.valid_until) : '');
$('#notes').val(q.notes || '');
$('#discount').val(parseFloat(q.discount) || 0);
$('#tax').val(parseFloat(q.tax) || 0);
$('#item_tbody').empty();
item_index = 0;
(q.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(q.status);
}
});
}
// ── Save draft ────────────────────────────────────────────────────────────
function save_quotation() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one line item.');
return;
}
ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/manage_quotation.php',
autoPrepare: true,
checkRequired: 0,
action: quotation_id ? 'update' : 'create',
data: {
id: quotation_id,
contact_id: $('#contact_id').val() || 0,
quotation_date: to_iso_date($('#quotation_date').val()),
valid_until: to_iso_date($('#valid_until').val()),
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax: $('#tax').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!quotation_id && res.new_id) {
window.location.href = '<?php echo $server_url?>revenue/manage_quotation.php?id=' + res.new_id;
} else {
retrieve_quotation();
}
}
});
}
// ── Change status ─────────────────────────────────────────────────────────
function change_status(action_type) {
var labels = {
send: 'Mark this quotation as Sent to customer?',
accept: 'Mark this quotation as Accepted?',
reject: 'Mark this quotation as Rejected?',
reopen: 'Reopen this quotation as Draft?',
cancel: 'Cancel this quotation?',
};
bootbox.confirm({
message: labels[action_type] || 'Confirm?',
buttons: {
confirm: { label: 'Confirm', className: 'btn-primary' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/update_quotation_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: quotation_id, action_type: action_type },
onSuccess: function() {
retrieve_quotation();
}
});
}
});
}
// ── Convert to Order ─────────────────────────────────────────────────────
function convert_to_order() {
bootbox.confirm({
message: 'Convert this quotation to a Pending sales order? WMS staff will assign warehouse locations before confirming.',
buttons: {
confirm: { label: 'Convert to Order', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/convert_quotation.php',
autoPrepare: true,
checkRequired: 0,
action: 'create',
data: { quotation_id: quotation_id },
onSuccess: function(res) {
retrieve_quotation(); // reload — status now shows Converted
}
});
}
});
}
// ── Contact autocomplete ──────────────────────────────────────────────────
function init_contact_search() {
$('#contact').autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>contact/api/engine/contact.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res((data.output || []).map(c => ({
label: c.contact_name,
value: c.contact_name,
id: c.id,
})));
}
});
},
select: function(e, ui) {
$('#contact_id').val(ui.item.id);
}
});
}
// ── Boot ──────────────────────────────────────────────────────────────────
$(async function() {
try {
if (quotation_id) {
await retrieve_quotation();
} else {
var today = flatpickr.formatDate(new Date(), 'd/m/Y');
$('#quotation_date').val(today);
add_item_row();
set_mode(0);
}
flatpickr('#quotation_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#valid_until', { dateFormat: 'd/m/Y', allowInput: true });
init_contact_search();
} 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">Sales Orders</h1>
<p class="mb-0 text-muted">Sales order management</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-shopping-cart fs-1 mb-3 d-block"></i>
<p class="mb-0">Sales Orders coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+235
View File
@@ -0,0 +1,235 @@
<?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">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Quotations</h1>
<p class="mb-0 text-muted">Sales quotations sent to customers</p>
</div>
<a href="<?php echo $server_url?>revenue/manage_quotation.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>New Quotation
</a>
</div>
</div>
</div>
<!-- Stat cards -->
<div class="row g-5 mb-5">
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-primary bg-opacity-10 text-primary rounded-2">
<i class="ti ti-file-description fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Total</p>
<h3 class="fw-bold mb-0" id="stat_total">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-warning bg-opacity-10 text-warning rounded-2">
<i class="ti ti-pencil fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Draft</p>
<h3 class="fw-bold mb-0" id="stat_draft">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-info bg-opacity-10 text-info rounded-2">
<i class="ti ti-send fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Sent</p>
<h3 class="fw-bold mb-0" id="stat_sent">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-circle-check fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Accepted</p>
<h3 class="fw-bold mb-0" id="stat_accepted">—</h3>
</div>
</div>
</div>
</div>
</div>
<!-- Filter + Table -->
<div class="row g-5">
<div class="col-12">
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<div class="col-lg-6">
<div class="position-relative">
<input type="text" id="search_input" class="form-control ps-9"
placeholder="Search quotation number or contact...">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-3">
<select id="filter_status" class="form-select">
<option value="">All Status</option>
<option value="0">Draft</option>
<option value="1">Sent</option>
<option value="2">Accepted</option>
<option value="3">Rejected</option>
<option value="5">Converted</option>
<option value="-1">Cancelled</option>
</select>
</div>
<div class="col-lg-3">
<button class="btn btn-secondary w-100" onclick="retrieve_quotations()">
<i class="ti ti-search me-1"></i>Search
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="quotation_list">
<thead class="table-primary border-light">
<tr>
<th>Quotation #</th>
<th>Date</th>
<th>Valid Until</th>
<th>Contact</th>
<th>Items</th>
<th>Grand Total</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var status_badge = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Sent</span>',
'2': '<span class="badge bg-success">Accepted</span>',
'3': '<span class="badge bg-danger">Rejected</span>',
'5': '<span class="badge bg-primary">Converted</span>',
};
function retrieve_quotations() {
return ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/retrieve_quotation.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var all = res.output || [];
var keyword = $('#search_input').val().toLowerCase();
var fil_status = $('#filter_status').val();
var data = all.filter(function(q) {
var match_kw = !keyword ||
(q.quotation_number || '').toLowerCase().includes(keyword) ||
(q.contact_name || '').toLowerCase().includes(keyword);
var match_st = fil_status === '' || String(q.status) === fil_status;
return match_kw && match_st;
});
// Stats from full list (unfiltered)
$('#stat_total').text(all.length);
$('#stat_draft').text(all.filter(q => String(q.status) === '0').length);
$('#stat_sent').text(all.filter(q => String(q.status) === '1').length);
$('#stat_accepted').text(all.filter(q => String(q.status) === '2').length);
alasql('CREATE TABLE IF NOT EXISTS quotation_list');
alasql.tables.quotation_list.data = data;
var page = generate_pagination('quotation_list', data.length);
$('table#quotation_list tfoot').html(page);
if (typeof change_page_quotation_list === 'function') change_page_quotation_list(1);
}
});
}
function change_page_quotation_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM quotation_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
$.each(page_data, function(i, q) {
var items = typeof q.items === 'string' ? JSON.parse(q.items || '[]') : (q.items || []);
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(q.quotation_number)}</td>
<td class="py-3">${format_date(q.quotation_date)}</td>
<td class="py-3">${q.valid_until ? format_date(q.valid_until) : '<span class="text-muted">—</span>'}</td>
<td class="py-3">${escape_html(q.contact_name || '—')}</td>
<td class="py-3">${items.length} item${items.length !== 1 ? 's' : ''}</td>
<td class="py-3 fw-semibold">${format_number(q.grand_total, 2)}</td>
<td class="py-3">${status_badge[String(q.status)] || q.status}</td>
<td class="py-3">
<a href="<?php echo $server_url?>revenue/manage_quotation.php?id=${q.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#quotation_list tbody').html(body);
}
$(function() {
retrieve_quotations();
$('#search_input').on('keyup', function(e) {
if (e.key === 'Enter') retrieve_quotations();
});
});
</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">Receipts</h1>
<p class="mb-0 text-muted">Customer receipts</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-cash fs-1 mb-3 d-block"></i>
<p class="mb-0">Receipts coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>