View file File name : run-payroll.php Content :<?php session_start(); if (!isset($_SESSION['user_id'])) { header('Location: index.php'); exit(); } require_once '_helpers.php'; require_once '_payroll_helpers.php'; $conn = get_db_connection(); $business = $conn->query("SELECT * FROM business_settings LIMIT 1")->fetch_assoc(); try { $payroll_result = $conn->query("SELECT * FROM payroll ORDER BY employeeName ASC"); $payroll_list_raw = []; if($payroll_result) { while($row = $payroll_result->fetch_assoc()) { $payroll_list_raw[] = $row; } } $payroll_list = array_map('calculate_payroll', $payroll_list_raw); $totals = array_reduce($payroll_list, function($carry, $item) { $carry['grossSalary'] += $item['grossSalary'] ?? 0; $carry['socialSecurity'] += $item['socialSecurity'] ?? 0; $carry['employerSocialSecurity'] += $item['employerSocialSecurity'] ?? 0; $carry['paye'] += $item['paye'] ?? 0; $carry['advanceDeduction'] += $item['advanceDeduction'] ?? 0; $carry['netSalary'] += $item['netSalary'] ?? 0; return $carry; }, ['grossSalary' => 0, 'socialSecurity' => 0, 'employerSocialSecurity' => 0, 'paye' => 0, 'advanceDeduction' => 0, 'netSalary' => 0]); $conn->close(); } catch (Exception $e) { $payroll_list = []; $totals = []; } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Run Payroll - <?php echo htmlspecialchars($business['name']); ?></title> <script src="https://cdn.tailwindcss.com"></script> <style>:root { --primary: <?php echo htmlspecialchars($business['primary_color'] ?? '217 91% 60%'); ?>; } .bg-primary { background-color: hsl(var(--primary)); } .text-primary { color: hsl(var(--primary)); }</style> </head> <body class="bg-slate-50"> <div class="flex h-screen"> <?php include '_sidebar.php'; ?> <div class="flex-1 flex flex-col overflow-hidden lg:ml-64"> <?php include '_header.php'; ?> <main class="flex-1 overflow-x-hidden overflow-y-auto bg-slate-50 p-6"> <div class="flex flex-col gap-8"> <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> <div> <h1 class="text-3xl font-bold tracking-tight">Run Payroll</h1> <p class="text-slate-500">For the period of <?php echo date('F Y'); ?></p> </div> <div class="flex items-center gap-2 self-end sm:self-auto"> <button class="px-4 py-2 border rounded-full text-sm font-medium hover:bg-slate-100 flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>Export PDF</button> <button id="mark-paid-btn" class="px-4 py-2 bg-primary text-white rounded-full text-sm font-semibold hover:bg-opacity-90 flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>Mark all as Paid</button> </div> </div> <div class="bg-white rounded-lg shadow-sm border p-4"> <div class="overflow-x-auto"> <table class="w-full text-sm" id="run-payroll-table"> <thead class="bg-slate-50"> <tr> <th class="p-2 text-left">Employee</th> <th class="p-2 text-left">Gross</th> <th class="p-2 text-left">Bonus</th> <th class="p-2 text-left">Employee SSB</th> <th class="p-2 text-left">Employer SSB</th> <th class="p-2 text-left">PAYE</th> <th class="p-2 text-left">Advance</th> <th class="p-2 text-left">Net Salary</th> <th class="p-2 text-left">Status</th> <th class="p-2 text-left">Payslip</th> </tr> </thead> <tbody> <?php foreach($payroll_list as $p): ?> <tr class="border-t payroll-row" data-id="<?php echo htmlspecialchars($p['id']); ?>" data-base-salary="<?php echo $p['baseSalary']; ?>" data-pay-frequency="<?php echo $p['payFrequency']; ?>"> <td class="p-2 font-medium employee-name"><?php echo htmlspecialchars($p['employeeName']); ?></td> <td class="p-2 gross-salary"><?php echo format_currency($p['grossSalary']); ?></td> <td class="p-2"><input type="number" name="bonus" value="<?php echo htmlspecialchars($p['bonus']); ?>" class="w-24 border rounded p-1 bonus-input"></td> <td class="p-2 employee-ssb"><?php echo format_currency($p['socialSecurity']); ?></td> <td class="p-2 employer-ssb"><?php echo format_currency($p['employerSocialSecurity']); ?></td> <td class="p-2 paye"><?php echo format_currency($p['paye']); ?></td> <td class="p-2"><input type="number" name="advanceDeduction" value="<?php echo htmlspecialchars($p['advanceDeduction']); ?>" class="w-24 border rounded p-1 advance-input"></td> <td class="p-2 font-semibold net-salary"><?php echo format_currency($p['netSalary']); ?></td> <td class="p-2 status"><span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-slate-100 text-slate-800 status-badge"><?php echo htmlspecialchars($p['status']); ?></span></td> <td class="p-2"><a href="payslip.php?id=<?php echo htmlspecialchars($p['id']); ?>" class="text-primary hover:underline"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg></a></td> </tr> <?php endforeach; ?> </tbody> <tfoot class="font-bold bg-slate-50"> <tr class="border-t-2"> <td class="p-2">Total</td> <td class="p-2" id="total-gross"><?php echo format_currency($totals['grossSalary']); ?></td> <td class="p-2"></td> <td class="p-2" id="total-employee-ssb"><?php echo format_currency($totals['socialSecurity']); ?></td> <td class="p-2" id="total-employer-ssb"><?php echo format_currency($totals['employerSocialSecurity']); ?></td> <td class="p-2" id="total-paye"><?php echo format_currency($totals['paye']); ?></td> <td class="p-2" id="total-advance"><?php echo format_currency($totals['advanceDeduction']); ?></td> <td class="p-2" id="total-net"><?php echo format_currency($totals['netSalary']); ?></td> <td class="p-2" colspan="2"></td> </tr> </tfoot> </table> </div> </div> </div> </main> </div> </div> <script> const ssbBrackets = <?php echo json_encode($ssb_brackets); ?>; const PAYE_ANNUAL_ALLOWANCE = <?php echo PAYE_ANNUAL_ALLOWANCE; ?>; const PAYE_RATE = <?php echo PAYE_RATE; ?>; function formatCurrency(amount) { return 'BZ$' + parseFloat(amount).toFixed(2); } function getAnnualBaseSalary(baseSalary, payFrequency) { switch (payFrequency) { case 'weekly': return baseSalary * 52; case 'bi-weekly': return baseSalary * 26; case 'monthly': return baseSalary * 12; default: return 0; } } function getSsbContributions(weeklySalary) { const bracket = ssbBrackets.find(b => weeklySalary >= b.min && weeklySalary <= b.max); return bracket ? { employee: bracket.employee, employer: bracket.employer } : { employee: 0, employer: 0 }; } function calculateRow(row) { const baseSalary = parseFloat(row.dataset.baseSalary); const payFrequency = row.dataset.payFrequency; const bonus = parseFloat(row.querySelector('.bonus-input').value) || 0; const advanceDeduction = parseFloat(row.querySelector('.advance-input').value) || 0; const grossSalary = baseSalary + bonus; const annualBaseSalary = getAnnualBaseSalary(baseSalary, payFrequency); const weeklyBaseSalary = annualBaseSalary > 0 ? annualBaseSalary / 52 : 0; const weeklySsb = getSsbContributions(weeklyBaseSalary); let periodEmployeeSsb = 0, periodEmployerSsb = 0; if (payFrequency === 'weekly') { periodEmployeeSsb = weeklySsb.employee; periodEmployerSsb = weeklySsb.employer; } if (payFrequency === 'bi-weekly') { periodEmployeeSsb = weeklySsb.employee * 2; periodEmployerSsb = weeklySsb.employer * 2; } if (payFrequency === 'monthly') { periodEmployeeSsb = weeklySsb.employee * 4; periodEmployerSsb = weeklySsb.employer * 4; } const periodsPerYear = { monthly: 12, 'bi-weekly': 26, weekly: 52 }[payFrequency] || 1; const annualGross = grossSalary * periodsPerYear; const taxableIncome = Math.max(0, annualGross - PAYE_ANNUAL_ALLOWANCE); const annualPaye = taxableIncome * PAYE_RATE; const periodPaye = periodsPerYear > 0 ? annualPaye / periodsPerYear : 0; const totalDeductions = periodEmployeeSsb + periodPaye + advanceDeduction; const netSalary = grossSalary - totalDeductions; // Update UI row.querySelector('.gross-salary').textContent = formatCurrency(grossSalary); row.querySelector('.employee-ssb').textContent = formatCurrency(periodEmployeeSsb); row.querySelector('.employer-ssb').textContent = formatCurrency(periodEmployerSsb); row.querySelector('.paye').textContent = formatCurrency(periodPaye); row.querySelector('.net-salary').textContent = formatCurrency(netSalary); } function calculateTotals() { const rows = document.querySelectorAll('.payroll-row'); let totals = { gross: 0, empSsb: 0, emplSsb: 0, paye: 0, advance: 0, net: 0 }; rows.forEach(row => { const base = parseFloat(row.dataset.baseSalary); const bonus = parseFloat(row.querySelector('.bonus-input').value) || 0; totals.gross += base + bonus; totals.empSsb += parseFloat(row.querySelector('.employee-ssb').textContent.replace('BZ$', '')); totals.emplSsb += parseFloat(row.querySelector('.employer-ssb').textContent.replace('BZ$', '')); totals.paye += parseFloat(row.querySelector('.paye').textContent.replace('BZ$', '')); totals.advance += parseFloat(row.querySelector('.advance-input').value) || 0; totals.net += parseFloat(row.querySelector('.net-salary').textContent.replace('BZ$', '')); }); document.getElementById('total-gross').textContent = formatCurrency(totals.gross); document.getElementById('total-employee-ssb').textContent = formatCurrency(totals.empSsb); document.getElementById('total-employer-ssb').textContent = formatCurrency(totals.emplSsb); document.getElementById('total-paye').textContent = formatCurrency(totals.paye); document.getElementById('total-advance').textContent = formatCurrency(totals.advance); document.getElementById('total-net').textContent = formatCurrency(totals.net); } document.getElementById('run-payroll-table').addEventListener('input', e => { if (e.target.classList.contains('bonus-input') || e.target.classList.contains('advance-input')) { const row = e.target.closest('.payroll-row'); calculateRow(row); calculateTotals(); } }); document.getElementById('mark-paid-btn').addEventListener('click', () => { if (!confirm('Are you sure you want to process payroll and mark all as paid? This will save the current values.')) return; const payrollData = Array.from(document.querySelectorAll('.payroll-row')).map(row => ({ id: row.dataset.id, bonus: parseFloat(row.querySelector('.bonus-input').value) || 0, advanceDeduction: parseFloat(row.querySelector('.advance-input').value) || 0, })); const formData = new FormData(); formData.append('action', 'run_payroll'); formData.append('payrollData', JSON.stringify(payrollData)); fetch('api.php', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { alert(data.data.message); window.location.href = 'hr.php'; } else { alert('Error: ' + data.message); } }) .catch(err => { console.error(err); alert('An error occurred.'); }); }); </script> </body> </html>