| Compliance Details |
| Employee |
Effective Rate |
Calculated Pay |
Compliance Flags |
${report.employeeCalculations.map(emp => `
| ${emp.name} |
$${emp.effectiveRate.toFixed(2)}/hr |
$${emp.totalPay.toFixed(2)} |
${emp.flags.length > 0 ? emp.flags.map(f => `${f.message} ($${f.amount.toFixed(2)})`).join('') : 'Compliant'} |
`).join('')}
| Totals |
$${report.totalPayroll.toFixed(2)} |
$${report.totalRiskAmount.toFixed(2)} |
`;
container.innerHTML = html;
}
// --- Core Logic ---
function runComplianceCheck() {
const { settings, employees } = appState;
const applicableMinWage = Math.max(settings.fedMinWage, settings.stateMinWage);
const otThreshold = 40;
let totalPayroll = 0;
let totalRiskAmount = 0;
const employeeCalculations = [];
employees.forEach(emp => {
let regHours = Math.min(emp.hours, otThreshold);
let otHours = Math.max(0, emp.hours - otThreshold);
let totalPay = 0;
let effectiveRate = 0;
const flags = [];
if (emp.type === 'hourly') {
effectiveRate = emp.rate;
const regPay = regHours * emp.rate;
const otPay = otHours * emp.rate * 1.5;
totalPay = regPay + otPay;
if (emp.rate < applicableMinWage) {
const underpayment = (applicableMinWage - emp.rate) * emp.hours;
flags.push({ message: 'Below minimum wage', amount: underpayment });
totalRiskAmount += underpayment;
}
} else if (emp.type === 'salary') {
// Assuming weekly salary for non-exempt employee
const baseRate = emp.rate / otThreshold;
effectiveRate = emp.rate / Math.max(emp.hours, otThreshold);
if (effectiveRate < applicableMinWage) {
const underpayment = (applicableMinWage * emp.hours) - emp.rate;
flags.push({ message: 'Effective rate below minimum', amount: underpayment });
totalRiskAmount += underpayment;
}
// Salaried non-exempt overtime (half-time method)
const otPremium = otHours * baseRate * 0.5;
totalPay = emp.rate + otPremium;
}
totalPayroll += totalPay;
employeeCalculations.push({ ...emp, totalPay, effectiveRate, flags });
});
appState.report = { totalPayroll, totalRiskAmount, applicableMinWage, employeeCalculations };
}
// --- Event Handlers & Navigation ---
function showTab(tabNumber) {
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
getEl(`tab-${tabNumber}`).classList.remove('hidden');
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.toggle('active', b.dataset.tab === `tab-${tabNumber}`);
b.classList.toggle('inactive', b.dataset.tab !== `tab-${tabNumber}`);
});
currentTab = tabNumber;
updateNavigation();
}
function updateNavigation() {
prevBtn.disabled = currentTab === 1;
nextBtn.textContent = currentTab === totalTabs - 1 ? 'Generate Report' : 'Next';
nextBtn.style.display = currentTab === totalTabs ? 'none' : 'inline-block';
stepIndicator.textContent = `Step ${currentTab} of ${totalTabs}`;
}
getEl('form-add-employee').addEventListener('submit', (e) => {
e.preventDefault();
appState.employees.push({
id: nextEmployeeId++,
name: getEl('empName').value,
type: getEl('payType').value,
rate: parseFloat(getEl('payRate').value),
hours: parseFloat(getEl('hoursWorked').value)
});
e.target.reset();
renderEmployeeTable();
});
getEl('employee-table-body').addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
const id = parseInt(e.target.dataset.id);
appState.employees = appState.employees.filter(emp => emp.id !== id);
renderEmployeeTable();
}
});
stateSelect.addEventListener('change', (e) => {
const state = e.target.value;
if (stateMinWages[state]) {
getEl('stateMinWage').value = stateMinWages[state];
}
});
function generateFullReport() {
if (!getEl('form-settings').checkValidity()) {
getEl('form-settings').reportValidity();
showTab(1);
return;
}
appState.settings.payPeriod = getEl('payPeriod').value;
appState.settings.state = getEl('state').value;
appState.settings.fedMinWage = parseFloat(getEl('fedMinWage').value);
appState.settings.stateMinWage = parseFloat(getEl('stateMinWage').value);
showTab(3);
getEl('report-content').classList.add('hidden');
getEl('report-loading').classList.remove('hidden');
setTimeout(() => {
runComplianceCheck();
renderDashboard();
renderReport();
getEl('report-loading').classList.add('hidden');
getEl('report-content').classList.remove('hidden');
}, 1000);
}
nextBtn.addEventListener('click', () => {
if (currentTab < totalTabs - 1) {
showTab(currentTab + 1);
} else if (currentTab === totalTabs - 1) {
generateFullReport();
}
});
prevBtn.addEventListener('click', () => { if (currentTab > 1) showTab(currentTab - 1); });
getEl('download-pdf-btn').addEventListener('click', () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const report = appState.report;
const settings = appState.settings;
doc.setFontSize(18);
doc.text("Workplace Compensation Compliance Report", 105, 20, { align: 'center' });
doc.setFontSize(11);
doc.text(`Pay Period Ending: ${settings.payPeriod}`, 14, 30);
doc.text(`State: ${settings.state}`, 14, 36);
const tableBody = report.employeeCalculations.map(emp => {
const flagsText = emp.flags.length > 0 ? emp.flags.map(f => `${f.message} ($${f.amount.toFixed(2)})`).join(', ') : 'Compliant';
return [emp.name, `$${emp.effectiveRate.toFixed(2)}`, `$${emp.totalPay.toFixed(2)}`, flagsText];
});
doc.autoTable({
startY: 45,
head: [['Employee', 'Effective Rate', 'Calculated Pay', 'Compliance Flags']],
body: tableBody,
foot: [['Totals', '', `$${report.totalPayroll.toFixed(2)}`, `Risk: $${report.totalRiskAmount.toFixed(2)}`]],
theme: 'grid',
headStyles: { fillColor: [29, 78, 216] },
footStyles: { fontStyle: 'bold' }
});
doc.save(`Compensation-Report-${settings.payPeriod}.pdf`);
});
// --- Initialization ---
renderEmployeeTable();
updateNavigation();
});