diff --git a/app/ac_dashboard/index.php b/app/ac_dashboard/index.php new file mode 100644 index 0000000..5bbd89d --- /dev/null +++ b/app/ac_dashboard/index.php @@ -0,0 +1,39 @@ + + + + + + + +
+
+ + +
+
+

Accounting Dashboard

+

Financial overview

+
+
+ + +
+
+
+ +

Accounting dashboard coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/accounting/api/engine/account.php b/app/accounting/api/engine/account.php new file mode 100644 index 0000000..1a3bed4 --- /dev/null +++ b/app/accounting/api/engine/account.php @@ -0,0 +1,9 @@ +getAll(); +$answer['success'] = 1; +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/account_stats.php b/app/accounting/api/engine/account_stats.php new file mode 100644 index 0000000..5811abc --- /dev/null +++ b/app/accounting/api/engine/account_stats.php @@ -0,0 +1,9 @@ +getStats(); +$answer = array_merge(['success' => 1], $stats); +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/department.php b/app/accounting/api/engine/department.php new file mode 100644 index 0000000..5d7021e --- /dev/null +++ b/app/accounting/api/engine/department.php @@ -0,0 +1,9 @@ +getAll(); +$answer['success'] = 1; +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/department_stats.php b/app/accounting/api/engine/department_stats.php new file mode 100644 index 0000000..69d0148 --- /dev/null +++ b/app/accounting/api/engine/department_stats.php @@ -0,0 +1,9 @@ +getStats(); +$answer = array_merge(['success' => 1], $stats); +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/manage_account.php b/app/accounting/api/engine/manage_account.php new file mode 100644 index 0000000..39e0845 --- /dev/null +++ b/app/accounting/api/engine/manage_account.php @@ -0,0 +1,22 @@ + '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)); diff --git a/app/accounting/api/engine/manage_department.php b/app/accounting/api/engine/manage_department.php new file mode 100644 index 0000000..3fa331e --- /dev/null +++ b/app/accounting/api/engine/manage_department.php @@ -0,0 +1,22 @@ + '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)); diff --git a/app/accounting/api/engine/manage_period.php b/app/accounting/api/engine/manage_period.php new file mode 100644 index 0000000..34e758a --- /dev/null +++ b/app/accounting/api/engine/manage_period.php @@ -0,0 +1,40 @@ + 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)); diff --git a/app/accounting/api/engine/period.php b/app/accounting/api/engine/period.php new file mode 100644 index 0000000..0480d92 --- /dev/null +++ b/app/accounting/api/engine/period.php @@ -0,0 +1,9 @@ +getAll(); +$answer['success'] = 1; +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/period_stats.php b/app/accounting/api/engine/period_stats.php new file mode 100644 index 0000000..371a7c9 --- /dev/null +++ b/app/accounting/api/engine/period_stats.php @@ -0,0 +1,9 @@ +getStats(); +$answer = array_merge(['success' => 1], $stats); +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/remove_account.php b/app/accounting/api/engine/remove_account.php new file mode 100644 index 0000000..7af9ac2 --- /dev/null +++ b/app/accounting/api/engine/remove_account.php @@ -0,0 +1,16 @@ + 'Missing id'])); +} + +$coa = new ChartOfAccounts($pdo2, $company_id); +$coa->delete($id); + +$answer['success'] = 1; +$answer['message'] = 'Account removed'; +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/remove_department.php b/app/accounting/api/engine/remove_department.php new file mode 100644 index 0000000..f0a1b06 --- /dev/null +++ b/app/accounting/api/engine/remove_department.php @@ -0,0 +1,14 @@ + 'Missing id'])); + +$dept = new DepartmentManager($pdo2, $company_id); +$dept->delete($id); + +$answer['success'] = 1; +$answer['message'] = 'Department removed'; +exit(json_encode($answer)); diff --git a/app/accounting/api/engine/retrieve_account.php b/app/accounting/api/engine/retrieve_account.php new file mode 100644 index 0000000..0b94c86 --- /dev/null +++ b/app/accounting/api/engine/retrieve_account.php @@ -0,0 +1,19 @@ + '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)); diff --git a/app/accounting/api/engine/retrieve_department.php b/app/accounting/api/engine/retrieve_department.php new file mode 100644 index 0000000..41997be --- /dev/null +++ b/app/accounting/api/engine/retrieve_department.php @@ -0,0 +1,15 @@ + '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)); diff --git a/app/accounting/balance_sheet.php b/app/accounting/balance_sheet.php new file mode 100644 index 0000000..da666dc --- /dev/null +++ b/app/accounting/balance_sheet.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Balance Sheet

+

Assets, liabilities, and equity

+
+
+ +
+
+
+ +

Balance Sheet coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/accounting/chart_of_accounts.php b/app/accounting/chart_of_accounts.php new file mode 100644 index 0000000..c2e943a --- /dev/null +++ b/app/accounting/chart_of_accounts.php @@ -0,0 +1,272 @@ + + + + + + +
+
+ + +
+
+
+
+

Chart of Accounts

+

Manage your account code structure

+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ Total Accounts +

—

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Active +

—

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Revenue Accounts +

—

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Expense Accounts +

—

+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+ + + + + + +
+
+ + + + +
+
+ +
+ + + + + + + + + + + + + +
CodeNameTypeLevelStatusActions
+
+ +
+
+
+
+ +
+
+ + + + + + + diff --git a/app/accounting/departments.php b/app/accounting/departments.php new file mode 100644 index 0000000..cbd23be --- /dev/null +++ b/app/accounting/departments.php @@ -0,0 +1,214 @@ + + + + + + +
+
+ + +
+
+
+
+

Departments

+

GL department dimensions for journal entries

+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ Total +

—

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Active +

—

+
+
+
+
+
+
+
+
+
+
+ +
+
+ Inactive +

—

+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+ + + + +
+
+ +
+ + + + + + + + + + + + +
CodeNameDescriptionStatusActions
+
+ +
+
+
+
+ +
+
+ + + + + + + diff --git a/app/accounting/gl_movement.php b/app/accounting/gl_movement.php new file mode 100644 index 0000000..e94b549 --- /dev/null +++ b/app/accounting/gl_movement.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

GL Movement

+

General ledger movement report

+
+
+ +
+
+
+ +

GL Movement coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/accounting/manage_account.php b/app/accounting/manage_account.php new file mode 100644 index 0000000..854ece3 --- /dev/null +++ b/app/accounting/manage_account.php @@ -0,0 +1,242 @@ + + + + + + +
+
+ +
+
+
+ + Back + +
+

Manage Account

+
+
+
+
+ +
+
+
+
+ + + +
+ +
+ + +
1xxx Asset · 2xxx Liability · 3xxx Equity · 4xxx Revenue · 5-9xxx Expense
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
Only parent-level accounts of the same type are shown.
+
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+
+ + +
+
+
+
Account Level Guide
+

+ Parent accounts group items for reporting (e.g. 1000 Current Assets). + They cannot be used in journal entries. +

+

+ Item accounts are the leaf-level codes posted in every journal entry + (e.g. 1110 Cash at Bank). +

+
+

Standard ranges:

+ + + + + + + + +
1xxxAsset
2xxxLiability
3xxxEquity
4xxxRevenue
5–9xxxExpense
+
+
+
+ +
+ +
+
+ + + + + + + diff --git a/app/accounting/manage_department.php b/app/accounting/manage_department.php new file mode 100644 index 0000000..fcaa2e5 --- /dev/null +++ b/app/accounting/manage_department.php @@ -0,0 +1,112 @@ + + + + + + +
+
+ +
+
+
+ + Back + +

Manage Department

+
+
+
+ +
+
+
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+
+
+ +
+
+ + + + + + + diff --git a/app/accounting/periods.php b/app/accounting/periods.php new file mode 100644 index 0000000..abfd4cb --- /dev/null +++ b/app/accounting/periods.php @@ -0,0 +1,274 @@ + + + + + + +
+
+ + +
+
+
+
+

Accounting Periods

+

Open, close, and lock monthly accounting periods

+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
+
Total

—

+
+
+
+
+
+
+
+
+
+ +
+
Open

—

+
+
+
+
+
+
+
+
+
+ +
+
Closed

—

+
+
+
+
+
+
+
+
+
+ +
+
Locked

—

+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + +
PeriodStatusClosed AtLocked AtActions
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/app/accounting/pl_statement.php b/app/accounting/pl_statement.php new file mode 100644 index 0000000..87d9076 --- /dev/null +++ b/app/accounting/pl_statement.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

P&L Statement

+

Profit and loss statement

+
+
+ +
+
+
+ +

P&L Statement coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/accounting/trial_balance.php b/app/accounting/trial_balance.php new file mode 100644 index 0000000..11f7bdd --- /dev/null +++ b/app/accounting/trial_balance.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Trial Balance

+

Period trial balance report

+
+
+ +
+
+
+ +

Trial Balance coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/accounting/vat_report.php b/app/accounting/vat_report.php new file mode 100644 index 0000000..b90a6ae --- /dev/null +++ b/app/accounting/vat_report.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

VAT Report

+

Value added tax report

+
+
+ +
+
+
+ +

VAT Report coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/assets/utils/classes/OrderManager.php b/app/assets/utils/classes/OrderManager.php index 1debcdb..cf027f5 100644 --- a/app/assets/utils/classes/OrderManager.php +++ b/app/assets/utils/classes/OrderManager.php @@ -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( diff --git a/app/assets/utils/classes/PostingManager.php b/app/assets/utils/classes/PostingManager.php new file mode 100644 index 0000000..8c7e808 --- /dev/null +++ b/app/assets/utils/classes/PostingManager.php @@ -0,0 +1,37 @@ +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 +} diff --git a/app/assets/utils/classes_ac/AccountingEngine.php b/app/assets/utils/classes_ac/AccountingEngine.php new file mode 100644 index 0000000..31041d5 --- /dev/null +++ b/app/assets/utils/classes_ac/AccountingEngine.php @@ -0,0 +1,23 @@ +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 +} diff --git a/app/assets/utils/classes_ac/ChartOfAccounts.php b/app/assets/utils/classes_ac/ChartOfAccounts.php new file mode 100644 index 0000000..ee2a3ea --- /dev/null +++ b/app/assets/utils/classes_ac/ChartOfAccounts.php @@ -0,0 +1,141 @@ +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) ?: []; + } +} diff --git a/app/assets/utils/classes_ac/DepartmentManager.php b/app/assets/utils/classes_ac/DepartmentManager.php new file mode 100644 index 0000000..565b717 --- /dev/null +++ b/app/assets/utils/classes_ac/DepartmentManager.php @@ -0,0 +1,91 @@ +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) ?: []; + } +} diff --git a/app/assets/utils/classes_ac/FinancialReports.php b/app/assets/utils/classes_ac/FinancialReports.php new file mode 100644 index 0000000..b3ebdff --- /dev/null +++ b/app/assets/utils/classes_ac/FinancialReports.php @@ -0,0 +1,23 @@ +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 +} diff --git a/app/assets/utils/classes_ac/JournalManager.php b/app/assets/utils/classes_ac/JournalManager.php new file mode 100644 index 0000000..7b3bb7e --- /dev/null +++ b/app/assets/utils/classes_ac/JournalManager.php @@ -0,0 +1,25 @@ +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 +} diff --git a/app/assets/utils/classes_ac/PeriodManager.php b/app/assets/utils/classes_ac/PeriodManager.php new file mode 100644 index 0000000..d74a0d5 --- /dev/null +++ b/app/assets/utils/classes_ac/PeriodManager.php @@ -0,0 +1,120 @@ +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) ?: []; + } +} diff --git a/app/assets/utils/classes_ac/TaxReportManager.php b/app/assets/utils/classes_ac/TaxReportManager.php new file mode 100644 index 0000000..b9c9c1e --- /dev/null +++ b/app/assets/utils/classes_ac/TaxReportManager.php @@ -0,0 +1,23 @@ +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 +} diff --git a/app/assets/utils/classes_ac/posting/InvoicePosting.php b/app/assets/utils/classes_ac/posting/InvoicePosting.php new file mode 100644 index 0000000..7ddf32e --- /dev/null +++ b/app/assets/utils/classes_ac/posting/InvoicePosting.php @@ -0,0 +1,21 @@ +pdo = $pdo; + $this->companyId = $company_id; + } + + // TODO: build(int $invoice_id): array — returns [header, lines] ready for JournalManager +} diff --git a/app/assets/utils/classes_ac/posting/PaymentPosting.php b/app/assets/utils/classes_ac/posting/PaymentPosting.php new file mode 100644 index 0000000..d72996a --- /dev/null +++ b/app/assets/utils/classes_ac/posting/PaymentPosting.php @@ -0,0 +1,21 @@ +pdo = $pdo; + $this->companyId = $company_id; + } + + // TODO: build(int $payment_id): array — returns [header, lines] ready for JournalManager +} diff --git a/app/assets/utils/classes_ac/posting/PurchasePosting.php b/app/assets/utils/classes_ac/posting/PurchasePosting.php new file mode 100644 index 0000000..268cd5f --- /dev/null +++ b/app/assets/utils/classes_ac/posting/PurchasePosting.php @@ -0,0 +1,21 @@ +pdo = $pdo; + $this->companyId = $company_id; + } + + // TODO: build(int $po_id): array — returns [header, lines] ready for JournalManager +} diff --git a/app/assets/utils/classes_ac/posting/ReceiptPosting.php b/app/assets/utils/classes_ac/posting/ReceiptPosting.php new file mode 100644 index 0000000..a99834a --- /dev/null +++ b/app/assets/utils/classes_ac/posting/ReceiptPosting.php @@ -0,0 +1,21 @@ +pdo = $pdo; + $this->companyId = $company_id; + } + + // TODO: build(int $receipt_id): array — returns [header, lines] ready for JournalManager +} diff --git a/app/expense/payment.php b/app/expense/payment.php new file mode 100644 index 0000000..d6fde29 --- /dev/null +++ b/app/expense/payment.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Payments

+

Payments to suppliers

+
+
+ +
+
+
+ +

Payments coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/expense/purchase_invoice.php b/app/expense/purchase_invoice.php new file mode 100644 index 0000000..f339371 --- /dev/null +++ b/app/expense/purchase_invoice.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Purchase Invoices

+

Supplier invoices

+
+
+ +
+
+
+ +

Purchase Invoices coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/expense/purchase_order.php b/app/expense/purchase_order.php new file mode 100644 index 0000000..eda3ad5 --- /dev/null +++ b/app/expense/purchase_order.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Purchase Orders

+

Purchase order management

+
+
+ +
+
+
+ +

Purchase Orders coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/expense/purchase_request.php b/app/expense/purchase_request.php new file mode 100644 index 0000000..b0782bb --- /dev/null +++ b/app/expense/purchase_request.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Purchase Requests

+

Internal purchase requests

+
+
+ +
+
+
+ +

Purchase Requests coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/finance/payment.php b/app/finance/payment.php new file mode 100644 index 0000000..e89857f --- /dev/null +++ b/app/finance/payment.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Payments

+

Cash and bank payments

+
+
+ +
+
+
+ +

Payments coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/finance/receipt.php b/app/finance/receipt.php new file mode 100644 index 0000000..98b16cb --- /dev/null +++ b/app/finance/receipt.php @@ -0,0 +1,36 @@ + + + + + + +
+
+ +
+
+

Receipts

+

Cash and bank receipts

+
+
+ +
+
+
+ +

Receipts coming soon.

+
+
+
+ +
+
+ + + + + diff --git a/app/include_master_sidebar.php b/app/include_master_sidebar.php index 2dbd3f3..c79ed28 100644 --- a/app/include_master_sidebar.php +++ b/app/include_master_sidebar.php @@ -1,7 +1,20 @@ @@ -23,7 +36,7 @@
  • - Products @@ -32,7 +45,7 @@
  • - Warehouse Location @@ -41,22 +54,53 @@
  • - Contact
  • + + + + +
  • + + + Chart of Accounts + +
  • + +
  • + + + Departments + +
  • + +
  • + + + Accounting Periods + +
  • + +
  • - + - Back to App +
  • diff --git a/app/include_sidebar.php b/app/include_sidebar.php index b760cd6..0bbb75a 100644 --- a/app/include_sidebar.php +++ b/app/include_sidebar.php @@ -1,6 +1,12 @@