| Server IP : 72.60.21.38 / Your IP : 216.73.217.154 Web Server : LiteSpeed System : Linux uk-fast-web1372.main-hosting.eu 4.18.0-553.121.1.lve.el8.x86_64 #1 SMP Thu Apr 30 16:40:41 UTC 2026 x86_64 User : u390967363 ( 390967363) PHP Version : 8.2.30 Disable Function : system, exec, shell_exec, passthru, mysql_list_dbs, ini_alter, dl, symlink, link, chgrp, leak, popen, apache_child_terminate, virtual, mb_send_mail MySQL : OFF | cURL : ON | WGET : ON | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /home/u390967363/domains/aibenproperties.com/public_html/app/ |
Upload File : |
<?php
if (session_status() === PHP_SESSION_NONE) { session_start(); }
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/functions.php';
$role = $_SESSION['user_role'] ?? 'guest';
$roleNorm = strtolower(str_replace([' ', '-'], '_', (string)$role));
$canView = (function_exists('isFinanceTier') && isFinanceTier($role)) || (function_exists('isAdminTier') && isAdminTier($role)) || in_array($roleNorm, ['finance','finance_officer','finance_manager','admin','super_admin','super_admins'], true);
if (!$canView) {
header('Location: dashboard.php');
exit;
}
$companyId = function_exists('getCurrentCompanyId') ? getCurrentCompanyId() : ($_SESSION['company_id'] ?? null);
$companyId = $companyId ? (int)$companyId : null;
$currencySymbol = '₦';
$action = isset($_GET['action']) ? trim((string)$_GET['action']) : '';
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
$type = isset($_GET['type']) ? strtolower(trim((string)$_GET['type'])) : '';
$approval = isset($_GET['approval']) ? strtolower(trim((string)$_GET['approval'])) : '';
$payment = isset($_GET['payment']) ? strtolower(trim((string)$_GET['payment'])) : '';
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 15;
$perPage = $perPage > 0 ? min(50, $perPage) : 15;
function px_money($amount, $currencySymbol = '₦') { return $currencySymbol . number_format((float)$amount, 2); }
function px_safe_date($d) { try { return $d ? date('M j, Y', strtotime((string)$d)) : '—'; } catch (Throwable $e) { return '—'; } }
function px_norm($s) { return strtolower(trim((string)$s)); }
function px_to_date_key($d) { try { return $d ? date('Y-m-d', strtotime((string)$d)) : ''; } catch (Throwable $e) { return ''; } }
function px_badge($text, $cls = 'bg-light text-secondary border') {
$t = trim((string)$text);
if ($t === '') $t = '—';
return '<span class="badge ' . $cls . '" style="font-weight:800; font-size:.72rem">' . htmlspecialchars($t) . '</span>';
}
function px_priority_label($dueDateKey, $hasBalance) {
if (!$hasBalance) return 'Normal';
if ($dueDateKey === '') return 'Normal';
$today = date('Y-m-d');
if ($dueDateKey < $today) return 'Overdue';
$soon = date('Y-m-d', strtotime('+7 days'));
if ($dueDateKey <= $soon) return 'Due Soon';
return 'Normal';
}
function px_workflow_status($approvalKey, $paymentKey, $amountDue, $amountPaid) {
$approvalKey = px_norm($approvalKey);
$paymentKey = px_norm($paymentKey);
$amountDue = (float)$amountDue;
$amountPaid = (float)$amountPaid;
$bal = max(0.0, $amountDue - $amountPaid);
if (in_array($approvalKey, ['rejected','declined','denied','cancelled','canceled'], true)) return 'Rejected';
if ($bal <= 0.00001 && $amountDue > 0) return 'Paid';
if ($amountPaid > 0 && $bal > 0.00001) return 'Partially Paid';
if (in_array($approvalKey, ['pending','pending_approval','awaiting','processing','new','draft'], true)) return 'Pending Approval';
if (in_array($approvalKey, ['approved','confirmed','authorized','accepted','completed'], true) && $paymentKey !== 'paid') return 'Awaiting Settlement';
return 'Draft';
}
$kpi = [
'outstanding' => ['count' => 0, 'amount' => 0.0],
'pending_approval' => ['count' => 0, 'amount' => 0.0],
'awaiting_settlement' => ['count' => 0, 'amount' => 0.0],
'overdue' => ['count' => 0, 'amount' => 0.0],
];
$rows = [];
try {
$hasUsers = false;
$hasVendors = false;
$hasExpenses = false;
$hasManual = false;
$hasPayroll = false;
$hasStaff = false;
$hasCommissions = false;
$hasMaintenance = false;
try { $hasUsers = $pdo->query("SHOW TABLES LIKE 'users'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasVendors = $pdo->query("SHOW TABLES LIKE 'vendors'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasExpenses = $pdo->query("SHOW TABLES LIKE 'expenses'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasManual = $pdo->query("SHOW TABLES LIKE 'expenses_manual'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasPayroll = $pdo->query("SHOW TABLES LIKE 'hr_payroll'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasStaff = $pdo->query("SHOW TABLES LIKE 'hr_staff'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasCommissions = $pdo->query("SHOW TABLES LIKE 'commissions'")->rowCount() > 0; } catch (Throwable $e) {}
try { $hasMaintenance = $pdo->query("SHOW TABLES LIKE 'maintenance_requests'")->rowCount() > 0; } catch (Throwable $e) {}
$maxFetch = 500;
$today = date('Y-m-d');
if ($hasManual && function_exists('tableHasColumn') && tableHasColumn('expenses_manual', 'status') && tableHasColumn('expenses_manual', 'amount')) {
$statusCol = 'status';
$dateCol = tableHasColumn('expenses_manual', 'expense_date') ? 'expense_date' : (tableHasColumn('expenses_manual', 'created_at') ? 'created_at' : 'id');
$updatedCol = tableHasColumn('expenses_manual', 'updated_at') ? 'updated_at' : (tableHasColumn('expenses_manual', 'created_at') ? 'created_at' : null);
$hasPostedAt = tableHasColumn('expenses_manual', 'posted_at');
$hasDept = tableHasColumn('expenses_manual', 'department');
$hasRecId = tableHasColumn('expenses_manual', 'recorded_by_user_id');
$hasVendorId = tableHasColumn('expenses_manual', 'vendor_id');
$join = "";
$selVendor = "NULL AS vendor_name";
if ($hasVendors && $hasVendorId && tableHasColumn('vendors', 'id') && tableHasColumn('vendors', 'name')) {
$join .= " LEFT JOIN vendors v ON v.id = em.vendor_id";
$selVendor = "v.name AS vendor_name";
}
$selUser = "NULL AS requested_by";
if ($hasUsers && $hasRecId && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
if ($userCol && tableHasColumn('users', $userCol)) {
$join .= " LEFT JOIN users u ON u.id = em.recorded_by_user_id";
$selUser = "u.$userCol AS requested_by";
}
}
$where = [];
$params = [];
if (tableHasColumn('expenses_manual', 'is_reversed')) { $where[] = "COALESCE(em.is_reversed,0)=0"; }
if ($companyId && tableHasColumn('expenses_manual', 'company_id')) { $where[] = "(em.company_id = ? OR em.company_id IS NULL)"; $params[] = $companyId; }
$where[] = "LOWER(TRIM(em.$statusCol)) IN ('pending','approved','rejected')";
$wSql = !empty($where) ? (" WHERE " . implode(" AND ", $where)) : "";
$selTitle = tableHasColumn('expenses_manual', 'title') ? "NULLIF(TRIM(em.title),'')" : "NULL";
$selCat = tableHasColumn('expenses_manual', 'category') ? "NULLIF(TRIM(em.category),'')" : "NULL";
$selDept = $hasDept ? "NULLIF(TRIM(em.department),'')" : "NULL";
$selUpdated = $updatedCol ? "em.$updatedCol" : "NULL";
$selPosted = $hasPostedAt ? "em.posted_at" : "NULL";
$sql = "SELECT
'expense' AS payable_type,
em.id AS ref_id,
COALESCE($selVendor, $selTitle, $selCat, 'Expense') AS party_name,
$selVendor,
COALESCE($selCat, 'Expense') AS payable_label,
COALESCE(em.amount,0) AS amount_due,
CASE
WHEN LOWER(TRIM(em.$statusCol))='approved' AND ($selPosted IS NOT NULL AND TRIM(COALESCE($selPosted,''))<>'') THEN COALESCE(em.amount,0)
ELSE 0
END AS amount_paid,
em.$dateCol AS due_date,
em.$statusCol AS approval_status,
CASE
WHEN LOWER(TRIM(em.$statusCol))='pending' THEN 'unpaid'
WHEN LOWER(TRIM(em.$statusCol))='approved' AND ($selPosted IS NULL OR TRIM(COALESCE($selPosted,''))='') THEN 'pending'
WHEN LOWER(TRIM(em.$statusCol))='rejected' THEN 'unpaid'
ELSE 'paid'
END AS payment_status,
$selDept AS department,
$selUser,
$selUpdated AS updated_at,
'expenses_manual' AS source,
em.id AS source_id
FROM expenses_manual em
$join
$wSql
ORDER BY $selUpdated DESC, em.id DESC
LIMIT $maxFetch";
$st = $pdo->prepare($sql);
$st->execute($params);
foreach (($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $r) {
$rows[] = $r;
}
}
if ($hasExpenses && function_exists('tableHasColumn') && tableHasColumn('expenses', 'status')) {
$amountCol = tableHasColumn('expenses', 'amount') ? 'amount' : null;
if ($amountCol) {
$dateCol = tableHasColumn('expenses', 'due_date') ? 'due_date' : null;
$updatedCol = tableHasColumn('expenses', 'updated_at') ? 'updated_at' : (tableHasColumn('expenses', 'created_at') ? 'created_at' : null);
$hasVendorId = tableHasColumn('expenses', 'vendor_id');
$hasDept = tableHasColumn('expenses', 'department');
$hasReqBy = tableHasColumn('expenses', 'requested_by') || tableHasColumn('expenses', 'created_by') || tableHasColumn('expenses', 'recorded_by_user_id');
$join = "";
$selVendor = "NULL AS vendor_name";
if ($hasVendors && $hasVendorId && tableHasColumn('vendors', 'id') && tableHasColumn('vendors', 'name')) {
$join .= " LEFT JOIN vendors v ON v.id = e.vendor_id";
$selVendor = "v.name AS vendor_name";
}
$selUser = "NULL AS requested_by";
if ($hasUsers && $hasReqBy && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
$byCol = tableHasColumn('expenses', 'requested_by') ? 'requested_by' : (tableHasColumn('expenses', 'created_by') ? 'created_by' : (tableHasColumn('expenses', 'recorded_by_user_id') ? 'recorded_by_user_id' : null));
if ($byCol) {
$join .= " LEFT JOIN users u2 ON u2.id = e.$byCol";
$selUser = "u2.$userCol AS requested_by";
}
}
$where = [];
$params = [];
if ($companyId && tableHasColumn('expenses', 'company_id')) { $where[] = "(e.company_id = ? OR e.company_id IS NULL)"; $params[] = $companyId; }
$where[] = "LOWER(TRIM(e.status)) IN ('pending','approved','paid','completed','rejected')";
$wSql = " WHERE " . implode(" AND ", $where);
$selDept = $hasDept ? "NULLIF(TRIM(e.department),'')" : "NULL";
$selUpdated = $updatedCol ? "e.$updatedCol" : "NULL";
$catCol = tableHasColumn('expenses', 'category') ? 'category' : null;
$descCol = tableHasColumn('expenses', 'description') ? 'description' : null;
$labelExpr = $catCol ? "NULLIF(TRIM(e.$catCol),'')" : ($descCol ? "NULLIF(TRIM(e.$descCol),'')" : "'Expense'");
$dueExpr = $dateCol ? "e.$dateCol" : "NULL";
$sql = "SELECT
'expense' AS payable_type,
e.id AS ref_id,
COALESCE($selVendor, $labelExpr, 'Expense') AS party_name,
$selVendor,
COALESCE($labelExpr,'Expense') AS payable_label,
COALESCE(e.$amountCol,0) AS amount_due,
CASE WHEN LOWER(TRIM(e.status)) IN ('paid','completed') THEN COALESCE(e.$amountCol,0) ELSE 0 END AS amount_paid,
$dueExpr AS due_date,
e.status AS approval_status,
CASE
WHEN LOWER(TRIM(e.status)) IN ('paid','completed') THEN 'paid'
WHEN LOWER(TRIM(e.status))='approved' THEN 'pending'
ELSE 'unpaid'
END AS payment_status,
$selDept AS department,
$selUser,
$selUpdated AS updated_at,
'expenses' AS source,
e.id AS source_id
FROM expenses e
$join
$wSql
ORDER BY $selUpdated DESC, e.id DESC
LIMIT $maxFetch";
$st = $pdo->prepare($sql);
$st->execute($params);
foreach (($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $r) {
$rows[] = $r;
}
}
}
if ($hasPayroll && function_exists('tableHasColumn') && tableHasColumn('hr_payroll', 'status')) {
$amountCol = tableHasColumn('hr_payroll', 'net_pay') ? 'net_pay' : (tableHasColumn('hr_payroll', 'base_salary') ? 'base_salary' : null);
if ($amountCol) {
$updatedCol = tableHasColumn('hr_payroll', 'updated_at') ? 'updated_at' : (tableHasColumn('hr_payroll', 'created_at') ? 'created_at' : null);
$join = "";
$selStaff = "CONCAT('Staff #', p.staff_id)";
$deptSel = "NULL";
$reqSel = "NULL AS requested_by";
if ($hasStaff && tableHasColumn('hr_staff', 'id')) {
$staffNameCol = tableHasColumn('hr_staff', 'name') ? 'name' : (tableHasColumn('hr_staff', 'full_name') ? 'full_name' : null);
if ($staffNameCol) {
$join .= " LEFT JOIN hr_staff s ON s.id = p.staff_id";
$selStaff = "COALESCE(NULLIF(TRIM(s.$staffNameCol),''), CONCAT('Staff #', p.staff_id))";
}
$deptCol = tableHasColumn('hr_staff', 'department') ? 'department' : null;
if ($deptCol) { $deptSel = "NULLIF(TRIM(s.$deptCol),'')"; }
}
if ($hasUsers && function_exists('tableHasColumn') && tableHasColumn('hr_payroll', 'prepared_by') && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
$join .= " LEFT JOIN users up ON up.id = p.prepared_by";
$reqSel = "up.$userCol AS requested_by";
}
$where = [];
$params = [];
if ($companyId && tableHasColumn('hr_payroll', 'company_id')) { $where[] = "(p.company_id = ? OR p.company_id IS NULL)"; $params[] = $companyId; }
$where[] = "LOWER(TRIM(p.status)) IN ('pending_approval','approved','paid')";
$wSql = " WHERE " . implode(" AND ", $where);
$selUpdated = $updatedCol ? "p.$updatedCol" : "NULL";
$hasPeriod = tableHasColumn('hr_payroll', 'period_month') && tableHasColumn('hr_payroll', 'period_year');
$dueExpr = $hasPeriod ? "STR_TO_DATE(CONCAT(p.period_year,'-',LPAD(p.period_month,2,'0'),'-01'), '%Y-%m-%d')" : (tableHasColumn('hr_payroll', 'created_at') ? "p.created_at" : "NULL");
$sql = "SELECT
'salary' AS payable_type,
p.id AS ref_id,
$selStaff AS party_name,
NULL AS vendor_name,
'Salary' AS payable_label,
COALESCE(p.$amountCol,0) AS amount_due,
CASE WHEN LOWER(TRIM(p.status))='paid' THEN COALESCE(p.$amountCol,0) ELSE 0 END AS amount_paid,
$dueExpr AS due_date,
p.status AS approval_status,
CASE
WHEN LOWER(TRIM(p.status))='paid' THEN 'paid'
WHEN LOWER(TRIM(p.status))='approved' THEN 'pending'
ELSE 'unpaid'
END AS payment_status,
$deptSel AS department,
$reqSel,
$selUpdated AS updated_at,
'hr_payroll' AS source,
p.id AS source_id
FROM hr_payroll p
$join
$wSql
ORDER BY $selUpdated DESC, p.id DESC
LIMIT $maxFetch";
$st = $pdo->prepare($sql);
$st->execute($params);
foreach (($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $r) {
$rows[] = $r;
}
}
}
if ($hasCommissions && function_exists('tableHasColumn')) {
$statusCol = tableHasColumn('commissions', 'status') ? 'status' : (tableHasColumn('commissions', 'commission_status') ? 'commission_status' : (tableHasColumn('commissions', 'state') ? 'state' : null));
$amountCol = tableHasColumn('commissions', 'amount') ? 'amount' : (tableHasColumn('commissions', 'commission_amount') ? 'commission_amount' : null);
$dateCol = tableHasColumn('commissions', 'due_date') ? 'due_date' : null;
$updatedCol = tableHasColumn('commissions', 'updated_at') ? 'updated_at' : (tableHasColumn('commissions', 'created_at') ? 'created_at' : null);
if ($statusCol && $amountCol) {
$join = "";
$selAgent = "'Agent'";
$hasAgentId = tableHasColumn('commissions', 'agent_id');
if ($hasUsers && $hasAgentId && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
$join .= " LEFT JOIN users ua ON ua.id = c.agent_id";
$selAgent = "COALESCE(NULLIF(TRIM(ua.$userCol),''), 'Agent')";
}
$where = [];
$params = [];
if ($companyId && tableHasColumn('commissions', 'company_id')) { $where[] = "(c.company_id = ? OR c.company_id IS NULL)"; $params[] = $companyId; }
$where[] = "LOWER(TRIM(COALESCE(c.$statusCol,''))) IN ('pending','approved','processing','awaiting','new','confirmed','authorized','accepted','paid','settled','disbursed','completed','rejected','declined','denied')";
$wSql = " WHERE " . implode(" AND ", $where);
$selUpdated = $updatedCol ? "c.$updatedCol" : "NULL";
$dueExpr = $dateCol ? "c.$dateCol" : "NULL";
$sql = "SELECT
'commission' AS payable_type,
c.id AS ref_id,
$selAgent AS party_name,
NULL AS vendor_name,
'Commission' AS payable_label,
COALESCE(c.$amountCol,0) AS amount_due,
CASE WHEN LOWER(TRIM(COALESCE(c.$statusCol,''))) IN ('paid','settled','disbursed','completed') THEN COALESCE(c.$amountCol,0) ELSE 0 END AS amount_paid,
$dueExpr AS due_date,
c.$statusCol AS approval_status,
CASE
WHEN LOWER(TRIM(COALESCE(c.$statusCol,''))) IN ('paid','settled','disbursed','completed') THEN 'paid'
WHEN LOWER(TRIM(COALESCE(c.$statusCol,''))) IN ('approved','confirmed','authorized','accepted') THEN 'pending'
ELSE 'unpaid'
END AS payment_status,
NULL AS department,
NULL AS requested_by,
$selUpdated AS updated_at,
'commissions' AS source,
c.id AS source_id
FROM commissions c
$join
$wSql
ORDER BY $selUpdated DESC, c.id DESC
LIMIT $maxFetch";
$st = $pdo->prepare($sql);
$st->execute($params);
foreach (($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $r) {
$rows[] = $r;
}
}
}
if ($hasMaintenance && function_exists('tableHasColumn') && tableHasColumn('maintenance_requests', 'status') && tableHasColumn('maintenance_requests', 'cost')) {
$dateCol = function_exists('expenseMaintenanceDateColumn') ? expenseMaintenanceDateColumn() : (tableHasColumn('maintenance_requests', 'updated_at') ? 'updated_at' : (tableHasColumn('maintenance_requests', 'created_at') ? 'created_at' : 'id'));
$updatedCol = tableHasColumn('maintenance_requests', 'updated_at') ? 'updated_at' : (tableHasColumn('maintenance_requests', 'created_at') ? 'created_at' : null);
$hasAssigned = tableHasColumn('maintenance_requests', 'assigned_to');
$hasRecId = tableHasColumn('maintenance_requests', 'recorded_by_user_id');
$join = "";
$selAssigned = "NULL AS assigned_name";
$selRecorder = "NULL AS requested_by";
if ($hasUsers && $hasAssigned && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
$join .= " LEFT JOIN users umt ON umt.id = m.assigned_to";
$selAssigned = "umt.$userCol AS assigned_name";
}
if ($hasUsers && $hasRecId && tableHasColumn('users', 'id')) {
$userCol = tableHasColumn('users', 'name') ? 'name' : (tableHasColumn('users', 'full_name') ? 'full_name' : 'email');
$join .= " LEFT JOIN users umr ON umr.id = m.recorded_by_user_id";
$selRecorder = "umr.$userCol AS requested_by";
}
$where = [];
$params = [];
$where[] = "LOWER(TRIM(m.status)) = 'completed'";
$where[] = "m.cost IS NOT NULL AND m.cost > 0";
if ($companyId && tableHasColumn('maintenance_requests', 'company_id')) { $where[] = "(m.company_id = ? OR m.company_id IS NULL)"; $params[] = $companyId; }
$wSql = " WHERE " . implode(" AND ", $where);
$titleExpr = tableHasColumn('maintenance_requests', 'title') ? "NULLIF(TRIM(m.title),'')" : "NULL";
$issueExpr = tableHasColumn('maintenance_requests', 'issue_type') ? "NULLIF(TRIM(m.issue_type),'')" : "NULL";
$selUpdated = $updatedCol ? "m.$updatedCol" : "NULL";
$sql = "SELECT
'maintenance' AS payable_type,
m.id AS ref_id,
COALESCE($selAssigned, 'Maintenance') AS party_name,
NULL AS vendor_name,
COALESCE($titleExpr, $issueExpr, 'Maintenance') AS payable_label,
COALESCE(m.cost,0) AS amount_due,
0 AS amount_paid,
m.$dateCol AS due_date,
m.status AS approval_status,
'pending' AS payment_status,
'Maintenance' AS department,
$selRecorder,
$selUpdated AS updated_at,
'maintenance_requests' AS source,
m.id AS source_id
FROM maintenance_requests m
$join
$wSql
ORDER BY $selUpdated DESC, m.id DESC
LIMIT $maxFetch";
$st = $pdo->prepare($sql);
$st->execute($params);
foreach (($st->fetchAll(PDO::FETCH_ASSOC) ?: []) as $r) {
$rows[] = $r;
}
}
$allRows = [];
$mkHref = function(array $r) {
$src = (string)($r['source'] ?? '');
$id = (int)($r['source_id'] ?? ($r['ref_id'] ?? 0));
if ($src === 'expenses_manual' && file_exists(__DIR__ . '/finance-expense-view.php') && $id > 0) return 'finance-expense-view.php?id=' . $id . '&source=manual';
if ($src === 'expenses' && file_exists(__DIR__ . '/finance-expenses.php')) return 'finance-expenses.php?tab=property&highlight_id=' . $id;
if ($src === 'hr_payroll' && file_exists(__DIR__ . '/hr-payroll.php')) return 'hr-payroll.php?highlight_id=' . $id;
if ($src === 'commissions' && file_exists(__DIR__ . '/commissions.php')) return 'commissions.php?status=all&view_id=' . $id;
if ($src === 'maintenance_requests' && file_exists(__DIR__ . '/maintenance.php')) return 'maintenance.php?view_id=' . $id;
return '';
};
foreach ($rows as $r) {
$amountDue = (float)($r['amount_due'] ?? 0);
$amountPaid = (float)($r['amount_paid'] ?? 0);
$balance = max(0.0, $amountDue - $amountPaid);
$dueKey = px_to_date_key($r['due_date'] ?? '');
$priority = px_priority_label($dueKey, $balance > 0.00001);
$workflow = px_workflow_status($r['approval_status'] ?? '', $r['payment_status'] ?? '', $amountDue, $amountPaid);
$r['balance'] = $balance;
$r['priority'] = $priority;
$r['workflow_status'] = $workflow;
$r['source_type'] = (string)($r['source'] ?? '');
$r['source_id'] = (int)($r['source_id'] ?? ($r['ref_id'] ?? 0));
$r['view_href'] = $mkHref($r);
$allRows[] = $r;
}
$filtered = [];
foreach ($allRows as $r) {
$party = (string)($r['party_name'] ?? '');
$label = (string)($r['payable_label'] ?? '');
$appr = px_norm($r['approval_status'] ?? '');
$paySt = px_norm($r['payment_status'] ?? '');
$tp = px_norm($r['payable_type'] ?? '');
if ($type !== '' && $tp !== $type) continue;
if ($approval !== '' && $appr !== $approval) continue;
if ($payment !== '' && $paySt !== $payment) continue;
if ($q !== '') {
$needle = px_norm($q);
$hay = px_norm($party . ' ' . $label . ' ' . ($r['vendor_name'] ?? '') . ' ' . ($r['department'] ?? '') . ' ' . ($r['requested_by'] ?? ''));
if ($needle !== '' && strpos($hay, $needle) === false) continue;
}
$filtered[] = $r;
}
usort($filtered, function($a, $b){
$ad = (string)($a['updated_at'] ?? '');
$bd = (string)($b['updated_at'] ?? '');
$at = $ad !== '' ? strtotime($ad) : 0;
$bt = $bd !== '' ? strtotime($bd) : 0;
if ($at === $bt) return ((int)($b['ref_id'] ?? 0) <=> (int)($a['ref_id'] ?? 0));
return $bt <=> $at;
});
$kpi = [
'outstanding' => ['count' => 0, 'amount' => 0.0],
'pending_approval' => ['count' => 0, 'amount' => 0.0],
'awaiting_settlement' => ['count' => 0, 'amount' => 0.0],
'overdue' => ['count' => 0, 'amount' => 0.0],
];
foreach ($allRows as $r) {
$bal = (float)($r['balance'] ?? 0);
if ($bal <= 0.00001) continue;
$kpi['outstanding']['count']++;
$kpi['outstanding']['amount'] += $bal;
$appr = px_norm($r['approval_status'] ?? '');
$paySt = px_norm($r['payment_status'] ?? '');
if (in_array($appr, ['pending','pending_approval','awaiting','processing','new'], true)) {
$kpi['pending_approval']['count']++;
$kpi['pending_approval']['amount'] += $bal;
}
if (in_array($appr, ['approved','confirmed','authorized','accepted'], true) && $paySt !== 'paid') {
$kpi['awaiting_settlement']['count']++;
$kpi['awaiting_settlement']['amount'] += $bal;
}
$d = px_to_date_key($r['due_date'] ?? '');
if ($d !== '' && $d < $today && $paySt !== 'paid') {
$kpi['overdue']['count']++;
$kpi['overdue']['amount'] += $bal;
}
}
$intel = [
'upcoming' => ['count' => 0, 'amount' => 0.0],
'payroll_pending' => ['count' => 0, 'amount' => 0.0],
'week_settlements' => ['count' => 0, 'amount' => 0.0],
'most_owed_vendor' => ['name' => null, 'amount' => 0.0],
];
$weekEnd = date('Y-m-d', strtotime('+7 days'));
$vendorBuckets = [];
foreach ($allRows as $r) {
$bal = (float)($r['balance'] ?? 0);
if ($bal <= 0.00001) continue;
$dueKey = px_to_date_key($r['due_date'] ?? '');
if ($dueKey !== '' && $dueKey <= $weekEnd) {
$intel['upcoming']['count']++;
$intel['upcoming']['amount'] += $bal;
$intel['week_settlements']['count']++;
$intel['week_settlements']['amount'] += $bal;
}
if (px_norm($r['source'] ?? '') === 'hr_payroll' && px_norm($r['approval_status'] ?? '') === 'pending_approval') {
$intel['payroll_pending']['count']++;
$intel['payroll_pending']['amount'] += $bal;
}
$vn = trim((string)($r['vendor_name'] ?? ''));
if ($vn !== '') {
if (!isset($vendorBuckets[$vn])) $vendorBuckets[$vn] = 0.0;
$vendorBuckets[$vn] += $bal;
}
}
if (!empty($vendorBuckets)) {
arsort($vendorBuckets);
$topName = array_key_first($vendorBuckets);
$intel['most_owed_vendor']['name'] = $topName;
$intel['most_owed_vendor']['amount'] = (float)$vendorBuckets[$topName];
}
if ($action === 'export') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="payables_' . date('Ymd_His') . '.csv"');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
$out = fopen('php://output', 'w');
fputcsv($out, ['Vendor/Staff','Payable Type','Amount Due','Amount Paid','Balance','Due Date','Workflow','Approval Status','Payment Status','Priority','Department','Requested By','Last Updated','Source Type','Source ID']);
foreach ($filtered as $r) {
$amountDue = (float)($r['amount_due'] ?? 0);
$amountPaid = (float)($r['amount_paid'] ?? 0);
$bal = (float)($r['balance'] ?? max(0, $amountDue - $amountPaid));
fputcsv($out, [
(string)($r['party_name'] ?? ''),
(string)($r['payable_label'] ?? $r['payable_type'] ?? ''),
number_format($amountDue, 2, '.', ''),
number_format($amountPaid, 2, '.', ''),
number_format($bal, 2, '.', ''),
(string)($r['due_date'] ?? ''),
(string)($r['workflow_status'] ?? ''),
(string)($r['approval_status'] ?? ''),
(string)($r['payment_status'] ?? ''),
(string)($r['priority'] ?? ''),
(string)($r['department'] ?? ''),
(string)($r['requested_by'] ?? ''),
(string)($r['updated_at'] ?? ''),
(string)($r['source_type'] ?? $r['source'] ?? ''),
(string)($r['source_id'] ?? $r['ref_id'] ?? ''),
]);
}
fclose($out);
exit;
}
$total = count($filtered);
$totalPages = max(1, (int)ceil($total / $perPage));
$page = min($page, $totalPages);
$offset = ($page - 1) * $perPage;
$paged = array_slice($filtered, $offset, $perPage);
$rows = $paged;
} catch (Throwable $e) {
$rows = [];
}
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid px-4">
<style>
.payables-table-wrap{display:block; width:100%; overflow:auto !important; -webkit-overflow-scrolling:touch}
.payables-table{width:max-content; min-width:100%}
.payables-table thead th{position:sticky; top:0; z-index:2; background:var(--bs-body-bg, #fff)}
.payables-table td,.payables-table th{vertical-align:middle; white-space:nowrap}
.payables-row{cursor:pointer}
.payables-row:hover{background:rgba(2,6,23,.03)}
.payables-table-wrap{touch-action:pan-x pan-y; cursor:grab}
.payables-table-wrap.payables-grabbing{cursor:grabbing}
</style>
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mt-4 mb-3">
<div>
<h1 class="h3 mb-0 text-gray-800">Payables</h1>
<div class="text-muted small mt-1">Accounts payable workspace for obligations, approvals, and settlements.</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<?php $expQuery = ['action'=>'export','q'=>$q,'type'=>$type,'approval'=>$approval,'payment'=>$payment,'per_page'=>$perPage,'page'=>$page]; ?>
<a href="payables.php?<?= htmlspecialchars(http_build_query($expQuery)) ?>" class="btn btn-outline-secondary btn-sm"><i class="fa-solid fa-file-export me-2"></i>Export</a>
<?php if (file_exists(__DIR__ . '/finance-expenses.php')): ?>
<a href="finance-expenses.php" class="btn btn-outline-secondary btn-sm"><i class="fa-solid fa-receipt me-2"></i>Open Expenses</a>
<?php endif; ?>
<?php if (file_exists(__DIR__ . '/hr-payroll.php')): ?>
<a href="hr-payroll.php" class="btn btn-outline-secondary btn-sm"><i class="fa-solid fa-money-check-dollar me-2"></i>Open Payroll</a>
<?php endif; ?>
<?php if (file_exists(__DIR__ . '/commissions.php')): ?>
<a href="commissions.php" class="btn btn-outline-secondary btn-sm"><i class="fa-solid fa-hand-holding-dollar me-2"></i>Open Commissions</a>
<?php endif; ?>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Outstanding Obligations</div>
<div class="h5 fw-bold mb-1"><?= px_money((float)$kpi['outstanding']['amount'], $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)$kpi['outstanding']['count']) ?> items</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Pending Approval</div>
<div class="h5 fw-bold mb-1"><?= px_money((float)$kpi['pending_approval']['amount'], $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)$kpi['pending_approval']['count']) ?> items</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Awaiting Settlement</div>
<div class="h5 fw-bold mb-1"><?= px_money((float)$kpi['awaiting_settlement']['amount'], $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)$kpi['awaiting_settlement']['count']) ?> items</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Overdue</div>
<div class="h5 fw-bold mb-1"><?= px_money((float)$kpi['overdue']['amount'], $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)$kpi['overdue']['count']) ?> items</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Upcoming Due Payments</div>
<div class="h6 fw-bold mb-1"><?= px_money((float)($intel['upcoming']['amount'] ?? 0), $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)($intel['upcoming']['count'] ?? 0)) ?> due within 7 days</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Payroll Pending</div>
<div class="h6 fw-bold mb-1"><?= px_money((float)($intel['payroll_pending']['amount'] ?? 0), $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)($intel['payroll_pending']['count'] ?? 0)) ?> items</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">Most Owed Vendor</div>
<div class="h6 fw-bold mb-1"><?= htmlspecialchars((string)($intel['most_owed_vendor']['name'] ?? '—')) ?></div>
<div class="text-muted small"><?= px_money((float)($intel['most_owed_vendor']['amount'] ?? 0), $currencySymbol) ?></div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-muted small">This Week Settlements</div>
<div class="h6 fw-bold mb-1"><?= px_money((float)($intel['week_settlements']['amount'] ?? 0), $currencySymbol) ?></div>
<div class="text-muted small"><?= number_format((int)($intel['week_settlements']['count'] ?? 0)) ?> items</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0" id="pxPayablesTable">
<div class="card-header py-3 d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="fw-bold">Payables Ledger</div>
<div class="small text-muted">What is owed, what is pending approval, and what needs settlement</div>
</div>
<form class="w-100" method="get">
<input type="hidden" name="per_page" value="<?= (int)$perPage ?>">
<div class="row gx-2 gy-2 align-items-center">
<div class="col-12 col-lg">
<input type="text" name="q" value="<?= htmlspecialchars($q) ?>" class="form-control form-control-sm" placeholder="Search vendor / staff / type">
</div>
<div class="col-6 col-lg-auto">
<select name="type" class="form-select form-select-sm">
<option value="" <?= $type === '' ? 'selected' : '' ?>>All Types</option>
<option value="expense" <?= $type === 'expense' ? 'selected' : '' ?>>Expense / Bill</option>
<option value="salary" <?= $type === 'salary' ? 'selected' : '' ?>>Salary</option>
<option value="commission" <?= $type === 'commission' ? 'selected' : '' ?>>Commission</option>
</select>
</div>
<div class="col-6 col-lg-auto">
<select name="approval" class="form-select form-select-sm">
<option value="" <?= $approval === '' ? 'selected' : '' ?>>All Approvals</option>
<option value="pending" <?= $approval === 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="pending_approval" <?= $approval === 'pending_approval' ? 'selected' : '' ?>>Pending Approval</option>
<option value="approved" <?= $approval === 'approved' ? 'selected' : '' ?>>Approved</option>
<option value="rejected" <?= $approval === 'rejected' ? 'selected' : '' ?>>Rejected</option>
</select>
</div>
<div class="col-6 col-lg-auto">
<select name="payment" class="form-select form-select-sm">
<option value="" <?= $payment === '' ? 'selected' : '' ?>>All Payments</option>
<option value="unpaid" <?= $payment === 'unpaid' ? 'selected' : '' ?>>Unpaid</option>
<option value="pending" <?= $payment === 'pending' ? 'selected' : '' ?>>Awaiting Settlement</option>
<option value="paid" <?= $payment === 'paid' ? 'selected' : '' ?>>Paid</option>
</select>
</div>
<div class="col-6 col-lg-auto d-grid">
<button class="btn btn-sm btn-dark" type="submit"><i class="fa-solid fa-magnifying-glass me-2"></i>Filter</button>
</div>
<div class="col-6 col-lg-auto d-grid">
<a class="btn btn-sm btn-outline-secondary" href="payables.php"><i class="fa-solid fa-rotate-left me-2"></i>Reset</a>
</div>
</div>
</form>
</div>
<div class="card-body p-0">
<div class="d-sm-none px-3 py-2 small text-muted border-bottom">Swipe left/right to see all columns.</div>
<div class="table-responsive payables-table-wrap" style="<?= empty($rows) ? '' : 'max-height:560px' ?>">
<table class="table mb-0 payables-table">
<thead>
<tr>
<th>Vendor/Staff</th>
<th>Payable Type</th>
<th class="text-end">Amount Due</th>
<th class="text-end">Amount Paid</th>
<th class="text-end">Balance</th>
<th>Due Date</th>
<th>Workflow</th>
<th>Approval Status</th>
<th>Payment Status</th>
<th>Priority</th>
<th>Department</th>
<th>Requested By</th>
<th>Last Updated</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td colspan="14" class="text-muted fst-italic py-4 px-3">No payable obligations found for the selected filters.</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $r): ?>
<?php
$src = (string)($r['source_type'] ?? $r['source'] ?? '');
$refId = (int)($r['source_id'] ?? $r['ref_id'] ?? 0);
$party = trim((string)($r['party_name'] ?? '—'));
$label = trim((string)($r['payable_label'] ?? '—'));
$amt = (float)($r['amount_due'] ?? 0);
$paidAmt = (float)($r['amount_paid'] ?? 0);
$bal = (float)($r['balance'] ?? max(0, $amt - $paidAmt));
$dueRaw = (string)($r['due_date'] ?? '');
$dueLabel = px_safe_date($dueRaw);
$appr = trim((string)($r['approval_status'] ?? '—'));
$paySt = trim((string)($r['payment_status'] ?? '—'));
$priority = (string)($r['priority'] ?? 'Normal');
$dept = trim((string)($r['department'] ?? ''));
$req = trim((string)($r['requested_by'] ?? ''));
$upd = trim((string)($r['updated_at'] ?? ''));
$viewHref = (string)($r['view_href'] ?? '');
$workflow = (string)($r['workflow_status'] ?? '');
$canApprove = false;
if ($src === 'expenses_manual' && px_norm($appr) === 'pending') $canApprove = true;
if ($src === 'hr_payroll' && px_norm($appr) === 'pending_approval') $canApprove = true;
?>
<tr class="payables-row" <?= $viewHref !== '' ? ('data-href="' . htmlspecialchars($viewHref) . '"') : '' ?>>
<td class="fw-semibold"><?= htmlspecialchars($party !== '' ? $party : '—') ?></td>
<td class="text-muted"><?= htmlspecialchars($label !== '' ? $label : '—') ?></td>
<td class="text-end fw-bold"><?= px_money($amt, $currencySymbol) ?></td>
<td class="text-end"><?= px_money($paidAmt, $currencySymbol) ?></td>
<td class="text-end fw-bold"><?= px_money($bal, $currencySymbol) ?></td>
<td><?= htmlspecialchars($dueLabel) ?></td>
<td><?= px_badge($workflow) ?></td>
<td><?= px_badge(ucwords(str_replace('_',' ', strtolower($appr)))) ?></td>
<td><?= px_badge(ucwords(str_replace('_',' ', strtolower($paySt)))) ?></td>
<td><?= px_badge($priority) ?></td>
<td class="text-muted"><?= htmlspecialchars($dept !== '' ? $dept : '—') ?></td>
<td class="text-muted"><?= htmlspecialchars($req !== '' ? $req : '—') ?></td>
<td class="text-muted"><?= htmlspecialchars($upd !== '' ? px_safe_date($upd) : '—') ?></td>
<td class="text-end">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Actions</button>
<ul class="dropdown-menu dropdown-menu-end">
<?php if ($viewHref !== ''): ?>
<li><a class="dropdown-item" href="<?= htmlspecialchars($viewHref) ?>">View</a></li>
<?php else: ?>
<li><button class="dropdown-item" type="button" disabled>View</button></li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li>
<?php if ($canApprove && $src === 'expenses_manual' && file_exists(__DIR__ . '/finance-expense-view.php') && $refId > 0): ?>
<li>
<form method="post" action="finance-expense-view.php?id=<?= (int)$refId ?>&source=manual" class="px-3 py-1">
<input type="hidden" name="approve_manual_id" value="<?= (int)$refId ?>">
<button class="btn btn-sm btn-outline-primary w-100" type="submit">Approve</button>
</form>
</li>
<?php elseif ($canApprove && $src === 'hr_payroll' && file_exists(__DIR__ . '/hr-payroll.php') && $refId > 0): ?>
<li>
<form method="post" action="hr-payroll.php" class="px-3 py-1">
<input type="hidden" name="approve_payroll_id" value="<?= (int)$refId ?>">
<button class="btn btn-sm btn-outline-primary w-100" type="submit">Approve</button>
</form>
</li>
<?php else: ?>
<li><button class="dropdown-item" type="button" disabled>Approve</button></li>
<?php endif; ?>
<li><button class="dropdown-item" type="button" disabled>Partial Payment</button></li>
<li><button class="dropdown-item" type="button" disabled>Mark as Paid</button></li>
<li><button class="dropdown-item" type="button" disabled>Upload Receipt</button></li>
<li><button class="dropdown-item" type="button" disabled>Generate Voucher</button></li>
<li><button class="dropdown-item" type="button" disabled>View Approval Trail</button></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="payables.php?<?= htmlspecialchars(http_build_query($expQuery)) ?>">Export</a></li>
</ul>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php
$mkUrl = function($p) use ($q, $type, $approval, $payment, $perPage) {
$arr = ['q'=>$q,'type'=>$type,'approval'=>$approval,'payment'=>$payment,'per_page'=>$perPage,'page'=>$p];
$arr = array_filter($arr, fn($v) => $v !== '' && $v !== null);
return 'payables.php?' . http_build_query($arr);
};
?>
<?php if (!empty($totalPages) && $totalPages > 1): ?>
<div class="card-footer bg-white border-0 d-flex align-items-center justify-content-between flex-wrap gap-2">
<div class="small px-muted">Page <?= (int)$page ?> of <?= (int)$totalPages ?></div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>"><a class="page-link" href="<?= htmlspecialchars($mkUrl(max(1, $page - 1))) ?>">Prev</a></li>
<li class="page-item <?= $page >= (int)$totalPages ? 'disabled' : '' ?>"><a class="page-link" href="<?= htmlspecialchars($mkUrl(min((int)$totalPages, $page + 1))) ?>">Next</a></li>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
</div>
<script>
(function(){
var rows = document.querySelectorAll('tr.payables-row[data-href]');
if (!rows.length) return;
rows.forEach(function(tr){
tr.addEventListener('click', function(ev){
var t = ev.target;
if (!t) return;
if (t.closest('a,button,input,select,textarea,label,.dropdown-menu')) return;
var href = tr.getAttribute('data-href') || '';
if (href) window.location.href = href;
});
});
})();
</script>
<script>
(function(){
var el = document.querySelector('.payables-table-wrap');
if (!el) return;
var isDown = false;
var startX = 0;
var scrollLeft = 0;
el.addEventListener('mousedown', function(e){
if (e.button !== 0) return;
if (e.target && e.target.closest('a,button,input,select,textarea,label,.dropdown-menu')) return;
isDown = true;
el.classList.add('payables-grabbing');
startX = e.pageX;
scrollLeft = el.scrollLeft;
});
document.addEventListener('mouseup', function(){
if (!isDown) return;
isDown = false;
el.classList.remove('payables-grabbing');
});
document.addEventListener('mousemove', function(e){
if (!isDown) return;
var walk = (e.pageX - startX);
el.scrollLeft = scrollLeft - walk;
});
})();
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>