`;
});
};
const renderTimesheetLog = () => {
const log = document.getElementById('timesheetLog');
const date = document.getElementById('timesheetDate').value;
log.innerHTML = '';
if (timesheets[date]) {
Object.entries(timesheets[date]).forEach(([empId, hours]) => {
const emp = employees.find(e => e.id == empId);
log.innerHTML += `
${emp.name}: ${hours} hours
`;
});
}
};
// --- PAYROLL LOGIC ---
app.runPayroll = () => {
const period = document.getElementById('payrollPeriod').value;
if (!period) { alert('Please select a payroll period.'); return; }
const [year, month] = period.split('-').map(Number);
const payrollData = employees.map(emp => {
let grossPay = 0;
if (emp.payType === 'salary') {
grossPay = emp.rate / 12;
} else {
Object.entries(timesheets).forEach(([date, entries]) => {
const d = new Date(date);
if (d.getFullYear() === year && d.getMonth() + 1 === month && entries[emp.id]) {
grossPay += entries[emp.id] * emp.rate;
}
});
}
const deductions = grossPay * 0.15; // Mock 15% tax
const netPay = grossPay - deductions;
return { ...emp, grossPay, deductions, netPay };
}).filter(p => p.grossPay > 0);
renderPayrollDashboard(payrollData);
};
const renderPayrollDashboard = (payrollData) => {
const totalGross = payrollData.reduce((sum, p) => sum + p.grossPay, 0);
const totalDeductions = payrollData.reduce((sum, p) => sum + p.deductions, 0);
const totalNet = payrollData.reduce((sum, p) => sum + p.netPay, 0);
document.getElementById('totalGrossPay').textContent = `$${totalGross.toFixed(2)}`;
document.getElementById('totalDeductions').textContent = `$${totalDeductions.toFixed(2)}`;
document.getElementById('totalNetPay').textContent = `$${totalNet.toFixed(2)}`;
document.getElementById('employeesPaid').textContent = payrollData.length;
const tableBody = document.getElementById('payrollTable');
tableBody.innerHTML = '';
payrollData.forEach(p => {
tableBody.innerHTML += `
| ${p.name} | $${p.grossPay.toFixed(2)} | $${p.deductions.toFixed(2)} | $${p.netPay.toFixed(2)} | |
`;
});
if (payrollChart) payrollChart.destroy();
const ctx = document.getElementById('payrollChart').getContext('2d');
payrollChart = new Chart(ctx, {
type: 'bar',
data: {
labels: payrollData.map(p => p.name),
datasets: [
{ label: 'Gross Pay', data: payrollData.map(p => p.grossPay), backgroundColor: '#3B82F6' },
{ label: 'Net Pay', data: payrollData.map(p => p.netPay), backgroundColor: '#10B981' }
]
}
});
};
// --- TIMESHEET LOGIC ---
app.logHours = () => {
const date = document.getElementById('timesheetDate').value;
const employeeId = document.getElementById('timesheetEmployee').value;
const hours = parseFloat(document.getElementById('timesheetHours').value);
if (!date || !employeeId || !hours) { alert('Please fill all fields.'); return; }
if (!timesheets[date]) timesheets[date] = {};
timesheets[date][employeeId] = (timesheets[date][employeeId] || 0) + hours;
saveTimesheets();
renderTimesheetLog();
};
app.deleteLogEntry = (date, empId) => {
delete timesheets[date][empId];
if (Object.keys(timesheets[date]).length === 0) delete timesheets[date];
saveTimesheets();
renderTimesheetLog();
};
// --- EMPLOYEE MANAGEMENT ---
app.showEmployeeModal = (id = null) => {
const modal = document.getElementById('employeeModal');
const title = document.getElementById('employeeModalTitle');
const idInput = document.getElementById('employeeIdInput');
const nameInput = document.getElementById('employeeNameInput');
const typeSelect = document.getElementById('payTypeSelect');
const rateInput = document.getElementById('payRateInput');
if (id) {
const emp = employees.find(e => e.id === id);
title.textContent = 'Edit Employee';
idInput.value = emp.id;
nameInput.value = emp.name;
typeSelect.value = emp.payType;
rateInput.value = emp.rate;
} else {
title.textContent = 'Add Employee';
idInput.value = '';
nameInput.value = '';
typeSelect.value = 'hourly';
rateInput.value = '';
}
modal.classList.remove('hidden');
};
app.saveEmployee = () => {
const id = document.getElementById('employeeIdInput').value;
const name = document.getElementById('employeeNameInput').value;
if (!name.trim()) { alert('Name is required.'); return; }
const empData = {
name,
payType: document.getElementById('payTypeSelect').value,
rate: parseFloat(document.getElementById('payRateInput').value)
};
if (id) {
const emp = employees.find(e => e.id == id);
Object.assign(emp, empData);
} else {
employees.push({ id: Date.now(), ...empData });
}
saveEmployees();
renderEmployeeList();
updateTimesheetEmployeeSelect();
app.hideModal('employeeModal');
};
app.deleteEmployee = (id) => {
if (confirm('Are you sure? This will delete the employee and their timesheet data.')) {
employees = employees.filter(e => e.id !== id);
Object.keys(timesheets).forEach(date => {
delete timesheets[date][id];
});
saveEmployees();
saveTimesheets();
renderEmployeeList();
updateTimesheetEmployeeSelect();
}
};
// --- UTILS & HELPERS ---
app.hideModal = (id) => document.getElementById(id).classList.add('hidden');
app.changeTab = (tabIndex) => {
document.querySelectorAll('[id^="tabContent-"]').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.replace('tab-active', 'tab-inactive'));
document.getElementById(`tabContent-${tabIndex}`).classList.remove('hidden');
document.getElementById(`tab-${tabIndex}`).classList.replace('tab-inactive', 'tab-active');
};
const updateTimesheetEmployeeSelect = () => {
const select = document.getElementById('timesheetEmployee');
select.innerHTML = '
';
employees.filter(e => e.payType === 'hourly').forEach(emp => {
select.innerHTML += `
`;
});
};
app.downloadPayslip = (payroll) => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const period = document.getElementById('payrollPeriod').value;
doc.setFontSize(20);
doc.text('Payslip', 105, 20, { align: 'center' });
doc.setFontSize(12);
doc.text(`Employee: ${payroll.name}`, 15, 40);
doc.text(`Pay Period: ${period}`, 15, 47);
doc.autoTable({
startY: 60,
head: [['Description', 'Amount']],
body: [
['Gross Pay', `$${payroll.grossPay.toFixed(2)}`],
['Deductions (Tax)', `-$${payroll.deductions.toFixed(2)}`],
['Net Pay', `$${payroll.netPay.toFixed(2)}`],
],
foot: [['Total Net Pay', `$${payroll.netPay.toFixed(2)}`]],
theme: 'striped',
footStyles: { fontStyle: 'bold' }
});
doc.save(`Payslip_${payroll.name.replace(' ','_')}_${period}.pdf`);
};
// --- INITIALIZATION ---
loadData();
renderEmployeeList();
updateTimesheetEmployeeSelect();
const now = new Date();
document.getElementById('timesheetDate').value = now.toISOString().split('T')[0];
document.getElementById('payrollPeriod').value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
renderTimesheetLog();
});