diff --git a/models/EmployeePerk.js b/models/EmployeePerk.js
new file mode 100644
index 00000000..fcef9bfb
--- /dev/null
+++ b/models/EmployeePerk.js
@@ -0,0 +1,99 @@
+const mongoose = require('mongoose');
+
+/**
+ * EmployeePerk Model
+ * Manages non-cash benefits and perquisites for employees
+ */
+const employeePerkSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ employeeId: {
+ type: String,
+ required: true,
+ index: true
+ },
+ employeeName: String,
+ perkType: {
+ type: String,
+ enum: [
+ 'company_car',
+ 'housing',
+ 'meal_vouchers',
+ 'health_insurance',
+ 'life_insurance',
+ 'stock_options',
+ 'club_membership',
+ 'phone_allowance',
+ 'internet_allowance',
+ 'education_allowance',
+ 'relocation_assistance',
+ 'other'
+ ],
+ required: true
+ },
+ perkName: {
+ type: String,
+ required: true
+ },
+ description: String,
+ monetaryValue: {
+ type: Number,
+ default: 0
+ },
+ taxableValue: {
+ type: Number,
+ default: 0
+ },
+ isTaxable: {
+ type: Boolean,
+ default: true
+ },
+ frequency: {
+ type: String,
+ enum: ['one_time', 'monthly', 'quarterly', 'annual'],
+ default: 'monthly'
+ },
+ effectiveFrom: {
+ type: Date,
+ required: true
+ },
+ effectiveTo: Date,
+ provider: {
+ name: String,
+ contactDetails: String
+ },
+ documents: [{
+ documentType: String,
+ documentUrl: String,
+ uploadedAt: Date
+ }],
+ utilizationTracking: {
+ isTracked: {
+ type: Boolean,
+ default: false
+ },
+ utilizationLimit: Number,
+ utilizationUsed: {
+ type: Number,
+ default: 0
+ }
+ },
+ status: {
+ type: String,
+ enum: ['active', 'suspended', 'expired', 'cancelled'],
+ default: 'active'
+ },
+ notes: String
+}, {
+ timestamps: true
+});
+
+// Indexes
+employeePerkSchema.index({ userId: 1, employeeId: 1, status: 1 });
+employeePerkSchema.index({ perkType: 1, effectiveFrom: 1 });
+
+module.exports = mongoose.model('EmployeePerk', employeePerkSchema);
diff --git a/models/PayrollRun.js b/models/PayrollRun.js
new file mode 100644
index 00000000..c83b18ef
--- /dev/null
+++ b/models/PayrollRun.js
@@ -0,0 +1,153 @@
+const mongoose = require('mongoose');
+
+/**
+ * PayrollRun Model
+ * Manages batch payroll processing for a specific period
+ */
+const payrollEntrySchema = new mongoose.Schema({
+ employeeId: {
+ type: String,
+ required: true
+ },
+ employeeName: String,
+ salaryStructureId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'SalaryStructure'
+ },
+ earnings: [{
+ componentName: String,
+ amount: Number
+ }],
+ deductions: [{
+ componentName: String,
+ amount: Number
+ }],
+ reimbursements: [{
+ componentName: String,
+ amount: Number
+ }],
+ grossPay: {
+ type: Number,
+ required: true
+ },
+ totalDeductions: {
+ type: Number,
+ default: 0
+ },
+ netPay: {
+ type: Number,
+ required: true
+ },
+ taxDeducted: {
+ type: Number,
+ default: 0
+ },
+ professionalTax: {
+ type: Number,
+ default: 0
+ },
+ providentFund: {
+ type: Number,
+ default: 0
+ },
+ esi: {
+ type: Number,
+ default: 0
+ },
+ paymentStatus: {
+ type: String,
+ enum: ['pending', 'processed', 'failed', 'reversed'],
+ default: 'pending'
+ },
+ paymentDate: Date,
+ paymentReference: String,
+ remarks: String
+}, { _id: false });
+
+const payrollRunSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ runId: {
+ type: String,
+ unique: true,
+ required: true
+ },
+ payrollPeriod: {
+ month: {
+ type: Number,
+ required: true,
+ min: 1,
+ max: 12
+ },
+ year: {
+ type: Number,
+ required: true
+ }
+ },
+ periodStart: {
+ type: Date,
+ required: true
+ },
+ periodEnd: {
+ type: Date,
+ required: true
+ },
+ entries: [payrollEntrySchema],
+ summary: {
+ totalEmployees: {
+ type: Number,
+ default: 0
+ },
+ totalGrossPay: {
+ type: Number,
+ default: 0
+ },
+ totalDeductions: {
+ type: Number,
+ default: 0
+ },
+ totalNetPay: {
+ type: Number,
+ default: 0
+ },
+ totalTax: {
+ type: Number,
+ default: 0
+ }
+ },
+ status: {
+ type: String,
+ enum: ['draft', 'pending_approval', 'approved', 'processing', 'completed', 'failed'],
+ default: 'draft'
+ },
+ approvedBy: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ approvedAt: Date,
+ processedAt: Date,
+ notes: String
+}, {
+ timestamps: true
+});
+
+// Pre-save hook to calculate summary
+payrollRunSchema.pre('save', function (next) {
+ this.summary.totalEmployees = this.entries.length;
+ this.summary.totalGrossPay = this.entries.reduce((sum, e) => sum + e.grossPay, 0);
+ this.summary.totalDeductions = this.entries.reduce((sum, e) => sum + e.totalDeductions, 0);
+ this.summary.totalNetPay = this.entries.reduce((sum, e) => sum + e.netPay, 0);
+ this.summary.totalTax = this.entries.reduce((sum, e) => sum + e.taxDeducted, 0);
+
+ next();
+});
+
+// Indexes
+payrollRunSchema.index({ userId: 1, 'payrollPeriod.year': 1, 'payrollPeriod.month': 1 });
+payrollRunSchema.index({ status: 1, createdAt: -1 });
+
+module.exports = mongoose.model('PayrollRun', payrollRunSchema);
diff --git a/models/SalaryStructure.js b/models/SalaryStructure.js
new file mode 100644
index 00000000..d632e0bd
--- /dev/null
+++ b/models/SalaryStructure.js
@@ -0,0 +1,124 @@
+const mongoose = require('mongoose');
+
+/**
+ * SalaryStructure Model
+ * Defines flexible component-based salary structure for employees
+ */
+const salaryComponentSchema = new mongoose.Schema({
+ componentName: {
+ type: String,
+ required: true
+ },
+ componentType: {
+ type: String,
+ enum: ['earning', 'deduction', 'reimbursement'],
+ required: true
+ },
+ calculationType: {
+ type: String,
+ enum: ['fixed', 'percentage', 'formula'],
+ default: 'fixed'
+ },
+ amount: {
+ type: Number,
+ default: 0
+ },
+ percentage: {
+ type: Number,
+ default: 0
+ },
+ baseComponent: {
+ type: String,
+ default: null
+ },
+ isTaxable: {
+ type: Boolean,
+ default: true
+ },
+ isStatutory: {
+ type: Boolean,
+ default: false
+ }
+}, { _id: false });
+
+const salaryStructureSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ employeeId: {
+ type: String,
+ required: true,
+ index: true
+ },
+ employeeName: {
+ type: String,
+ required: true
+ },
+ designation: String,
+ department: String,
+ effectiveFrom: {
+ type: Date,
+ required: true
+ },
+ effectiveTo: Date,
+ components: [salaryComponentSchema],
+ ctc: {
+ type: Number,
+ required: true
+ },
+ grossSalary: {
+ type: Number,
+ default: 0
+ },
+ netSalary: {
+ type: Number,
+ default: 0
+ },
+ paymentFrequency: {
+ type: String,
+ enum: ['monthly', 'bi-weekly', 'weekly'],
+ default: 'monthly'
+ },
+ bankDetails: {
+ accountNumber: String,
+ ifscCode: String,
+ bankName: String,
+ accountHolderName: String
+ },
+ taxRegime: {
+ type: String,
+ enum: ['old', 'new'],
+ default: 'new'
+ },
+ isActive: {
+ type: Boolean,
+ default: true
+ }
+}, {
+ timestamps: true
+});
+
+// Pre-save hook to calculate gross and net salary
+salaryStructureSchema.pre('save', function (next) {
+ const earnings = this.components
+ .filter(c => c.componentType === 'earning')
+ .reduce((sum, c) => sum + c.amount, 0);
+
+ const deductions = this.components
+ .filter(c => c.componentType === 'deduction')
+ .reduce((sum, c) => sum + c.amount, 0);
+
+ this.grossSalary = earnings;
+ this.netSalary = earnings - deductions;
+
+ next();
+});
+
+// Indexes
+salaryStructureSchema.index({ userId: 1, employeeId: 1 });
+salaryStructureSchema.index({ effectiveFrom: 1, isActive: 1 });
+
+module.exports = mongoose.model('SalaryStructure', salaryStructureSchema);
diff --git a/public/expensetracker.css b/public/expensetracker.css
index a03a8883..0ca9e194 100644
--- a/public/expensetracker.css
+++ b/public/expensetracker.css
@@ -9633,333 +9633,361 @@ input:checked + .toggle-slider::before {
.checkbox-container input { width: 16px; height: 16px; }
/* ============================================
- STRATEGIC TREASURY & LIQUIDITY MANAGEMENT
- Issue #590: Enterprise Treasury Suite
+ GLOBAL PAYROLL MANAGEMENT
+ Issue #589: Payroll & Statutory Engine
============================================ */
-.treasury-header {
+.payroll-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
margin-bottom: 30px;
}
-.header-metrics {
+.header-actions {
+ display: flex;
+ gap: 15px;
+}
+
+.stats-grid {
display: grid;
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: repeat(4, 1fr);
gap: 20px;
- margin-top: 20px;
+ margin-bottom: 30px;
}
-.metric-card {
+.stat-card {
+ display: flex;
+ align-items: center;
+ gap: 15px;
padding: 20px;
- text-align: center;
}
-.metric-card label {
+.stat-icon {
+ width: 50px;
+ height: 50px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+}
+
+.stat-content label {
font-size: 0.75rem;
color: var(--text-secondary);
- text-transform: uppercase;
+ display: block;
}
-.metric-card h2 {
- font-size: 2rem;
- margin: 10px 0;
- color: var(--text-primary);
+.stat-content h2 {
+ font-size: 1.8rem;
+ margin: 5px 0 0 0;
}
-.metric-trend {
- font-size: 0.8rem;
- display: inline-flex;
- align-items: center;
- gap: 5px;
+.payroll-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 25px;
+ margin-bottom: 25px;
}
-.metric-trend.positive { color: #64ffda; }
-.metric-trend.negative { color: #ff6b6b; }
-.metric-trend.warning { color: #ff9f43; }
-
-.health-bar {
- width: 100%;
- height: 6px;
- background: rgba(255,255,255,0.1);
- border-radius: 3px;
- overflow: hidden;
- margin-top: 10px;
+.filter-tabs {
+ display: flex;
+ gap: 10px;
}
-.health-fill {
- height: 100%;
- transition: width 0.5s ease, background-color 0.5s ease;
+.tab-btn {
+ padding: 5px 15px;
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 6px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: all 0.3s;
}
-.treasury-grid {
- display: grid;
- grid-template-columns: 350px 1fr;
- gap: 25px;
+.tab-btn.active {
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+ border-color: var(--accent-primary);
}
-.treasury-sidebar {
+.payroll-runs-list, .employees-list {
display: flex;
flex-direction: column;
- gap: 20px;
+ gap: 15px;
+ max-height: 600px;
+ overflow-y: auto;
}
-.vaults-list, .thresholds-list, .hedges-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
+.payroll-run-card {
+ padding: 20px;
+ cursor: pointer;
+ transition: transform 0.2s;
}
-.vault-card {
- padding: 15px;
+.payroll-run-card:hover {
+ transform: translateY(-2px);
}
-.vault-header {
+.run-header {
display: flex;
+ justify-content: space-between;
align-items: center;
- gap: 12px;
margin-bottom: 15px;
}
-.vault-icon {
- width: 40px;
- height: 40px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2rem;
+.run-period strong {
+ display: block;
+ font-size: 1.1rem;
}
-.vault-icon.operating { background: rgba(100, 255, 218, 0.15); color: #64ffda; }
-.vault-icon.reserve { background: rgba(72, 219, 251, 0.15); color: #48dbfb; }
-.vault-icon.investment { background: rgba(255, 159, 67, 0.15); color: #ff9f43; }
-.vault-icon.forex { background: rgba(255, 107, 107, 0.15); color: #ff6b6b; }
+.run-id {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
-.vault-info strong { display: block; font-size: 0.9rem; }
-.vault-info span { font-size: 0.7rem; color: var(--text-secondary); }
+.run-summary {
+ display: flex;
+ gap: 20px;
+ padding: 15px 0;
+ border-top: 1px solid rgba(255,255,255,0.05);
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+}
-.vault-balance {
- margin: 15px 0;
+.summary-item {
+ flex: 1;
}
-.vault-balance label { font-size: 0.7rem; color: var(--text-secondary); }
-.vault-balance h3 { margin: 5px 0; font-size: 1.4rem; color: var(--accent-primary); }
+.summary-item label {
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+ display: block;
+}
-.vault-stats {
- display: flex;
- justify-content: space-between;
- padding-top: 10px;
- border-top: 1px solid rgba(255,255,255,0.05);
+.summary-item strong {
+ font-size: 1rem;
}
-.vault-stats .stat label { font-size: 0.65rem; color: var(--text-secondary); display: block; }
-.vault-stats .stat span { font-size: 0.85rem; }
+.run-actions {
+ margin-top: 15px;
+ display: flex;
+ gap: 10px;
+}
-.threshold-item, .hedge-item {
- padding: 12px;
- background: rgba(255,255,255,0.02);
- border-radius: 8px;
- border-left: 3px solid var(--accent-primary);
+.employee-card {
+ padding: 15px;
}
-.threshold-info {
+.emp-header {
display: flex;
- justify-content: space-between;
align-items: center;
- margin-bottom: 5px;
+ gap: 12px;
+ margin-bottom: 15px;
}
-.threshold-info strong { font-size: 0.85rem; }
+.emp-avatar {
+ width: 45px;
+ height: 45px;
+ border-radius: 50%;
+ background: rgba(100, 255, 218, 0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent-primary);
+}
-.severity-pill {
- font-size: 0.65rem;
- padding: 2px 8px;
- border-radius: 4px;
- text-transform: uppercase;
+.emp-info {
+ flex: 1;
}
-.severity-pill.info { background: rgba(72, 219, 251, 0.2); color: #48dbfb; }
-.severity-pill.warning { background: rgba(255, 159, 67, 0.2); color: #ff9f43; }
-.severity-pill.critical { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; }
-.severity-pill.emergency { background: rgba(255, 0, 0, 0.3); color: #ff0000; }
+.emp-info strong {
+ display: block;
+ font-size: 0.95rem;
+}
-.threshold-value {
- font-size: 0.9rem;
+.emp-info span {
+ font-size: 0.7rem;
color: var(--text-secondary);
+ display: block;
}
-.hedge-pair {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
+.emp-designation {
+ margin-top: 2px;
}
-.hedge-type {
- font-size: 0.7rem;
- color: var(--text-secondary);
- text-transform: capitalize;
+.status-pill {
+ font-size: 0.65rem;
+ padding: 3px 10px;
+ border-radius: 12px;
+ text-transform: uppercase;
+}
+
+.status-pill.active {
+ background: rgba(100, 255, 218, 0.2);
+ color: #64ffda;
}
-.hedge-details {
+.status-pill.inactive {
+ background: rgba(255, 107, 107, 0.2);
+ color: #ff6b6b;
+}
+
+.emp-salary {
display: flex;
gap: 15px;
+ padding-top: 12px;
+ border-top: 1px solid rgba(255,255,255,0.05);
}
-.hedge-details .detail {
+.salary-item {
flex: 1;
}
-.hedge-details .detail label {
+.salary-item label {
font-size: 0.65rem;
color: var(--text-secondary);
display: block;
}
-.hedge-details .detail span {
- font-size: 0.85rem;
-}
-
-.hedge-details .detail.positive { color: #64ffda; }
-.hedge-details .detail.negative { color: #ff6b6b; }
-
-.forecast-controls {
- display: flex;
- gap: 10px;
+.salary-item strong {
+ font-size: 0.9rem;
}
-.forecast-controls select {
- background: rgba(255,255,255,0.05);
- border: 1px solid rgba(255,255,255,0.1);
- color: white;
- padding: 5px 10px;
+.status-badge {
+ font-size: 0.7rem;
+ padding: 4px 12px;
border-radius: 4px;
- font-size: 0.8rem;
+ text-transform: uppercase;
}
-.chart-container {
- height: 350px;
- padding: 20px 0;
+.status-badge.draft {
+ background: rgba(136, 146, 176, 0.2);
+ color: #8892b0;
}
-.chart-container-small {
- height: 250px;
- padding: 15px 0;
+.status-badge.pending_approval {
+ background: rgba(255, 159, 67, 0.2);
+ color: #ff9f43;
}
-.forecast-insights {
- margin-top: 20px;
- display: flex;
- flex-direction: column;
- gap: 10px;
+.status-badge.approved {
+ background: rgba(72, 219, 251, 0.2);
+ color: #48dbfb;
}
-.insight-card {
- display: flex;
- align-items: center;
- gap: 15px;
- padding: 15px;
- background: rgba(255,255,255,0.02);
- border-radius: 8px;
- border-left: 4px solid;
+.status-badge.processing {
+ background: rgba(255, 159, 67, 0.2);
+ color: #ff9f43;
}
-.insight-card.critical { border-left-color: #ff6b6b; }
-.insight-card.warning { border-left-color: #ff9f43; }
-.insight-card.positive { border-left-color: #64ffda; }
-.insight-card.info { border-left-color: #48dbfb; }
-
-.insight-icon {
- font-size: 1.5rem;
+.status-badge.completed {
+ background: rgba(100, 255, 218, 0.2);
+ color: #64ffda;
}
-.insight-card.critical .insight-icon { color: #ff6b6b; }
-.insight-card.warning .insight-icon { color: #ff9f43; }
-.insight-card.positive .insight-icon { color: #64ffda; }
-
-.insight-content {
- flex: 1;
+.status-badge.failed {
+ background: rgba(255, 107, 107, 0.2);
+ color: #ff6b6b;
}
-.insight-content strong {
- display: block;
- margin-bottom: 5px;
+.modal-large {
+ max-width: 900px;
}
-.insight-content p {
- font-size: 0.8rem;
- color: var(--text-secondary);
- margin: 0;
+.form-section {
+ margin-bottom: 25px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
}
-.severity-badge {
- font-size: 0.7rem;
- padding: 3px 10px;
- border-radius: 12px;
- text-transform: uppercase;
+.form-section h4 {
+ margin-bottom: 15px;
+ color: var(--accent-primary);
}
-.severity-badge.low { background: rgba(100, 255, 218, 0.2); color: #64ffda; }
-.severity-badge.medium { background: rgba(255, 159, 67, 0.2); color: #ff9f43; }
-.severity-badge.high { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; }
-.severity-badge.emergency { background: rgba(255, 0, 0, 0.3); color: #ff0000; }
+.component-row {
+ margin-bottom: 10px;
+}
-.analytics-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
+.search-input {
+ padding: 8px 15px;
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 6px;
+ color: white;
+ width: 250px;
}
-.portfolio-metrics {
- display: flex;
- flex-direction: column;
- gap: 15px;
- padding: 10px 0;
+.payroll-details {
+ padding: 20px 0;
}
-.metric-item {
+.details-header {
display: flex;
justify-content: space-between;
align-items: center;
+ margin-bottom: 20px;
}
-.metric-item label {
- font-size: 0.8rem;
+.details-summary {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 15px;
+ margin-bottom: 25px;
+}
+
+.summary-card {
+ padding: 15px;
+ background: rgba(255,255,255,0.02);
+ border-radius: 8px;
+ text-align: center;
+}
+
+.summary-card label {
+ font-size: 0.7rem;
color: var(--text-secondary);
+ display: block;
+ margin-bottom: 8px;
}
-.metric-item strong {
- font-size: 1.1rem;
- color: var(--accent-primary);
+.summary-card h3 {
+ font-size: 1.5rem;
+ margin: 0;
}
-.violations-alert {
- padding: 20px;
- margin-bottom: 20px;
- border-left: 4px solid #ff6b6b;
+.entries-table {
+ overflow-x: auto;
}
-.alert-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 15px;
- color: #ff6b6b;
+.entries-table table {
+ width: 100%;
+ border-collapse: collapse;
}
-.violation-item {
- display: flex;
- justify-content: space-between;
- padding: 10px;
+.entries-table th {
+ text-align: left;
+ padding: 12px;
background: rgba(255,255,255,0.02);
- border-radius: 4px;
- margin-bottom: 8px;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
}
-.violation-item.critical { border-left: 3px solid #ff6b6b; }
-.violation-item.warning { border-left: 3px solid #ff9f43; }
+.entries-table td {
+ padding: 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+}
-.no-insights {
- text-align: center;
+.entries-table td small {
color: var(--text-secondary);
- padding: 20px;
+ font-size: 0.75rem;
+}
+
+.btn-success {
+ background: linear-gradient(135deg, #64ffda, #48dbfb);
+ color: var(--bg-primary);
+}
+
+.text-accent {
+ color: var(--accent-primary);
}
diff --git a/public/js/payroll-controller.js b/public/js/payroll-controller.js
new file mode 100644
index 00000000..65ba5da5
--- /dev/null
+++ b/public/js/payroll-controller.js
@@ -0,0 +1,496 @@
+/**
+ * Payroll Management Controller
+ */
+
+let payrollTrendsChart = null;
+let currentPayrollRuns = [];
+let currentEmployees = [];
+
+document.addEventListener('DOMContentLoaded', () => {
+ initializeYearDropdown();
+ loadPayrollDashboard();
+ loadPayrollRuns();
+ loadEmployees();
+ setupForms();
+});
+
+function initializeYearDropdown() {
+ const yearSelect = document.getElementById('payroll-year');
+ const currentYear = new Date().getFullYear();
+
+ for (let i = currentYear; i >= currentYear - 5; i--) {
+ const option = document.createElement('option');
+ option.value = i;
+ option.textContent = i;
+ if (i === currentYear) option.selected = true;
+ yearSelect.appendChild(option);
+ }
+
+ // Set current month
+ const monthSelect = document.getElementById('payroll-month');
+ monthSelect.value = new Date().getMonth() + 1;
+}
+
+async function loadPayrollDashboard() {
+ try {
+ const res = await fetch('/api/payroll/dashboard', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ updateDashboardStats(data);
+ renderPayrollTrends(data.monthlyTrends);
+ } catch (err) {
+ console.error('Failed to load payroll dashboard:', err);
+ }
+}
+
+function updateDashboardStats(data) {
+ document.getElementById('active-employees').textContent = data.activeEmployeeCount || 0;
+ document.getElementById('pending-approvals').textContent = data.pendingApprovals || 0;
+
+ if (data.currentPayroll) {
+ document.getElementById('monthly-payout').textContent =
+ `₹${data.currentPayroll.summary.totalNetPay.toLocaleString()}`;
+ document.getElementById('tax-deducted').textContent =
+ `₹${data.currentPayroll.summary.totalTax.toLocaleString()}`;
+ } else {
+ document.getElementById('monthly-payout').textContent = '₹0';
+ document.getElementById('tax-deducted').textContent = '₹0';
+ }
+}
+
+function renderPayrollTrends(trends) {
+ const ctx = document.getElementById('payrollTrendsChart').getContext('2d');
+
+ if (payrollTrendsChart) {
+ payrollTrendsChart.destroy();
+ }
+
+ const labels = trends.map(t => `${getMonthName(t.month)} ${t.year}`);
+ const netPayData = trends.map(t => t.totalNetPay);
+ const taxData = trends.map(t => t.totalTax);
+
+ payrollTrendsChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels,
+ datasets: [
+ {
+ label: 'Net Payout',
+ data: netPayData,
+ borderColor: '#64ffda',
+ backgroundColor: 'rgba(100, 255, 218, 0.1)',
+ fill: true,
+ tension: 0.4
+ },
+ {
+ label: 'Tax Deducted',
+ data: taxData,
+ borderColor: '#ff9f43',
+ backgroundColor: 'rgba(255, 159, 67, 0.1)',
+ fill: true,
+ tension: 0.4
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ labels: { color: '#8892b0' }
+ }
+ },
+ scales: {
+ x: {
+ ticks: { color: '#8892b0' },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ },
+ y: {
+ ticks: {
+ color: '#8892b0',
+ callback: value => '₹' + value.toLocaleString()
+ },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ }
+ }
+ }
+ });
+}
+
+async function loadPayrollRuns() {
+ try {
+ const res = await fetch('/api/payroll/runs', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ currentPayrollRuns = data;
+ renderPayrollRuns(data);
+ } catch (err) {
+ console.error('Failed to load payroll runs:', err);
+ }
+}
+
+function renderPayrollRuns(runs) {
+ const list = document.getElementById('payroll-runs-list');
+
+ if (!runs || runs.length === 0) {
+ list.innerHTML = '
No payroll runs found.
';
+ return;
+ }
+
+ list.innerHTML = runs.map(run => `
+
+
+
+
+
+ ${run.summary.totalEmployees}
+
+
+
+ ₹${run.summary.totalGrossPay.toLocaleString()}
+
+
+
+ ₹${run.summary.totalNetPay.toLocaleString()}
+
+
+ ${run.status === 'draft' || run.status === 'pending_approval' ? `
+
+
+
+ ` : ''}
+ ${run.status === 'approved' ? `
+
+
+
+ ` : ''}
+
+ `).join('');
+}
+
+async function loadEmployees() {
+ try {
+ const res = await fetch('/api/payroll/salary-structures', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ currentEmployees = data;
+ renderEmployees(data);
+ } catch (err) {
+ console.error('Failed to load employees:', err);
+ }
+}
+
+function renderEmployees(employees) {
+ const list = document.getElementById('employees-list');
+
+ if (!employees || employees.length === 0) {
+ list.innerHTML = 'No employees found.
';
+ return;
+ }
+
+ list.innerHTML = employees.map(emp => `
+
+
+
+
+
+ ₹${emp.ctc.toLocaleString()}/yr
+
+
+
+ ₹${Math.round(emp.grossSalary).toLocaleString()}
+
+
+
+ ₹${Math.round(emp.netSalary).toLocaleString()}
+
+
+
+ `).join('');
+}
+
+async function viewPayrollDetails(runId) {
+ try {
+ const res = await fetch(`/api/payroll/runs/${runId}`, {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ const modal = document.getElementById('payroll-details-modal');
+ const content = document.getElementById('payroll-details-content');
+
+ content.innerHTML = `
+
+
+
+
+
+
${data.summary.totalEmployees}
+
+
+
+
₹${data.summary.totalGrossPay.toLocaleString()}
+
+
+
+
₹${data.summary.totalDeductions.toLocaleString()}
+
+
+
+
₹${data.summary.totalNetPay.toLocaleString()}
+
+
+
+
+
+
+ | Employee |
+ Gross |
+ Deductions |
+ Net Pay |
+ Status |
+
+
+
+ ${data.entries.map(entry => `
+
+
+ ${entry.employeeName}
+ ${entry.employeeId}
+ |
+ ₹${entry.grossPay.toLocaleString()} |
+ ₹${entry.totalDeductions.toLocaleString()} |
+ ₹${entry.netPay.toLocaleString()} |
+ ${entry.paymentStatus} |
+
+ `).join('')}
+
+
+
+
+ `;
+
+ modal.classList.remove('hidden');
+ } catch (err) {
+ console.error('Failed to load payroll details:', err);
+ }
+}
+
+async function approvePayroll(event, runId) {
+ event.stopPropagation();
+
+ if (!confirm('Approve this payroll run?')) return;
+
+ try {
+ const res = await fetch(`/api/payroll/runs/${runId}/approve`, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ if (res.ok) {
+ loadPayrollRuns();
+ loadPayrollDashboard();
+ }
+ } catch (err) {
+ console.error('Failed to approve payroll:', err);
+ }
+}
+
+async function processPayroll(event, runId) {
+ event.stopPropagation();
+
+ if (!confirm('Process payments for this payroll run?')) return;
+
+ try {
+ const res = await fetch(`/api/payroll/runs/${runId}/process`, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ if (res.ok) {
+ loadPayrollRuns();
+ loadPayrollDashboard();
+ }
+ } catch (err) {
+ console.error('Failed to process payroll:', err);
+ }
+}
+
+function filterRuns(filter) {
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
+ event.target.classList.add('active');
+
+ let filtered = currentPayrollRuns;
+
+ if (filter === 'pending') {
+ filtered = currentPayrollRuns.filter(r =>
+ r.status === 'draft' || r.status === 'pending_approval' || r.status === 'approved'
+ );
+ } else if (filter === 'completed') {
+ filtered = currentPayrollRuns.filter(r => r.status === 'completed');
+ }
+
+ renderPayrollRuns(filtered);
+}
+
+function getMonthName(month) {
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ return months[month - 1];
+}
+
+// Modal Functions
+function openGeneratePayrollModal() {
+ document.getElementById('generate-payroll-modal').classList.remove('hidden');
+}
+
+function closeGeneratePayrollModal() {
+ document.getElementById('generate-payroll-modal').classList.add('hidden');
+}
+
+function openSalaryStructureModal() {
+ document.getElementById('salary-structure-modal').classList.remove('hidden');
+}
+
+function closeSalaryStructureModal() {
+ document.getElementById('salary-structure-modal').classList.add('hidden');
+}
+
+function closePayrollDetailsModal() {
+ document.getElementById('payroll-details-modal').classList.add('hidden');
+}
+
+function addComponent() {
+ const container = document.getElementById('components-container');
+ const componentDiv = document.createElement('div');
+ componentDiv.className = 'component-row';
+ componentDiv.innerHTML = `
+
+ `;
+ container.appendChild(componentDiv);
+}
+
+function setupForms() {
+ // Generate Payroll Form
+ document.getElementById('generate-payroll-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const month = parseInt(document.getElementById('payroll-month').value);
+ const year = parseInt(document.getElementById('payroll-year').value);
+
+ try {
+ const res = await fetch('/api/payroll/generate', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ month, year })
+ });
+
+ if (res.ok) {
+ closeGeneratePayrollModal();
+ loadPayrollRuns();
+ loadPayrollDashboard();
+ }
+ } catch (err) {
+ console.error('Failed to generate payroll:', err);
+ }
+ });
+
+ // Salary Structure Form
+ document.getElementById('salary-structure-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const components = [];
+ document.querySelectorAll('.component-row').forEach(row => {
+ components.push({
+ componentName: row.querySelector('.comp-name').value,
+ componentType: row.querySelector('.comp-type').value,
+ calculationType: 'fixed',
+ amount: parseFloat(row.querySelector('.comp-amount').value),
+ isTaxable: true
+ });
+ });
+
+ const structureData = {
+ employeeId: document.getElementById('emp-id').value,
+ employeeName: document.getElementById('emp-name').value,
+ designation: document.getElementById('emp-designation').value,
+ department: document.getElementById('emp-department').value,
+ ctc: parseFloat(document.getElementById('emp-ctc').value),
+ taxRegime: document.getElementById('emp-tax-regime').value,
+ effectiveFrom: new Date(),
+ components,
+ bankDetails: {
+ accountNumber: document.getElementById('emp-account').value,
+ ifscCode: document.getElementById('emp-ifsc').value
+ }
+ };
+
+ try {
+ const res = await fetch('/api/payroll/salary-structures', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(structureData)
+ });
+
+ if (res.ok) {
+ closeSalaryStructureModal();
+ loadEmployees();
+ loadPayrollDashboard();
+ }
+ } catch (err) {
+ console.error('Failed to create salary structure:', err);
+ }
+ });
+}
diff --git a/public/payroll-management.html b/public/payroll-management.html
new file mode 100644
index 00000000..a4bf2110
--- /dev/null
+++ b/public/payroll-management.html
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+ Global Payroll Management - ExpenseFlow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
₹0
+
+
+
+
+
+
+
+
+
₹0
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
+
+
Loading payroll runs...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/routes/payroll.js b/routes/payroll.js
new file mode 100644
index 00000000..020a8957
--- /dev/null
+++ b/routes/payroll.js
@@ -0,0 +1,247 @@
+const express = require('express');
+const router = express.Router();
+const auth = require('../middleware/auth');
+const payrollService = require('../services/payrollService');
+const SalaryStructure = require('../models/SalaryStructure');
+const PayrollRun = require('../models/PayrollRun');
+const EmployeePerk = require('../models/EmployeePerk');
+
+/**
+ * Get Payroll Dashboard
+ */
+router.get('/dashboard', auth, async (req, res) => {
+ try {
+ const dashboard = await payrollService.getPayrollDashboard(req.user._id);
+ res.json({ success: true, data: dashboard });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Generate Payroll for a Period
+ */
+router.post('/generate', auth, async (req, res) => {
+ try {
+ const { month, year } = req.body;
+ const payrollRun = await payrollService.generatePayroll(req.user._id, month, year);
+ res.json({ success: true, data: payrollRun });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Payroll Runs
+ */
+router.get('/runs', auth, async (req, res) => {
+ try {
+ const runs = await PayrollRun.find({ userId: req.user._id })
+ .sort({ createdAt: -1 })
+ .limit(50);
+ res.json({ success: true, data: runs });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Specific Payroll Run
+ */
+router.get('/runs/:id', auth, async (req, res) => {
+ try {
+ const run = await PayrollRun.findOne({
+ _id: req.params.id,
+ userId: req.user._id
+ });
+
+ if (!run) {
+ return res.status(404).json({ success: false, error: 'Payroll run not found' });
+ }
+
+ res.json({ success: true, data: run });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Approve Payroll Run
+ */
+router.post('/runs/:id/approve', auth, async (req, res) => {
+ try {
+ const payrollRun = await payrollService.approvePayroll(req.params.id, req.user._id);
+ res.json({ success: true, data: payrollRun });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Process Payroll Run (Disburse Payments)
+ */
+router.post('/runs/:id/process', auth, async (req, res) => {
+ try {
+ const payrollRun = await payrollService.processPayroll(req.params.id);
+ res.json({ success: true, data: payrollRun });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Employee Payslip
+ */
+router.get('/runs/:id/payslip/:employeeId', auth, async (req, res) => {
+ try {
+ const payslip = await payrollService.getPayslip(req.params.id, req.params.employeeId);
+ res.json({ success: true, data: payslip });
+ } catch (err) {
+ res.status(404).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Create Salary Structure
+ */
+router.post('/salary-structures', auth, async (req, res) => {
+ try {
+ const salaryStructure = new SalaryStructure({
+ ...req.body,
+ userId: req.user._id
+ });
+ await salaryStructure.save();
+ res.json({ success: true, data: salaryStructure });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Salary Structures
+ */
+router.get('/salary-structures', auth, async (req, res) => {
+ try {
+ const structures = await SalaryStructure.find({ userId: req.user._id });
+ res.json({ success: true, data: structures });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Update Salary Structure
+ */
+router.patch('/salary-structures/:id', auth, async (req, res) => {
+ try {
+ const structure = await SalaryStructure.findOneAndUpdate(
+ { _id: req.params.id, userId: req.user._id },
+ req.body,
+ { new: true, runValidators: true }
+ );
+
+ if (!structure) {
+ return res.status(404).json({ success: false, error: 'Salary structure not found' });
+ }
+
+ res.json({ success: true, data: structure });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Deactivate Salary Structure
+ */
+router.delete('/salary-structures/:id', auth, async (req, res) => {
+ try {
+ const structure = await SalaryStructure.findOneAndUpdate(
+ { _id: req.params.id, userId: req.user._id },
+ { isActive: false, effectiveTo: new Date() },
+ { new: true }
+ );
+
+ if (!structure) {
+ return res.status(404).json({ success: false, error: 'Salary structure not found' });
+ }
+
+ res.json({ success: true, data: structure });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Create Employee Perk
+ */
+router.post('/perks', auth, async (req, res) => {
+ try {
+ const perk = new EmployeePerk({
+ ...req.body,
+ userId: req.user._id
+ });
+ await perk.save();
+ res.json({ success: true, data: perk });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Perks
+ */
+router.get('/perks', auth, async (req, res) => {
+ try {
+ const { employeeId } = req.query;
+ const query = { userId: req.user._id };
+
+ if (employeeId) {
+ query.employeeId = employeeId;
+ }
+
+ const perks = await EmployeePerk.find(query);
+ res.json({ success: true, data: perks });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Update Perk
+ */
+router.patch('/perks/:id', auth, async (req, res) => {
+ try {
+ const perk = await EmployeePerk.findOneAndUpdate(
+ { _id: req.params.id, userId: req.user._id },
+ req.body,
+ { new: true, runValidators: true }
+ );
+
+ if (!perk) {
+ return res.status(404).json({ success: false, error: 'Perk not found' });
+ }
+
+ res.json({ success: true, data: perk });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Employee YTD Statistics
+ */
+router.get('/ytd/:employeeId', auth, async (req, res) => {
+ try {
+ const year = parseInt(req.query.year) || new Date().getFullYear();
+ const ytd = await payrollService.getEmployeeYTD(
+ req.user._id,
+ req.params.employeeId,
+ year
+ );
+ res.json({ success: true, data: ytd });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index 30738c49..6a787fe0 100644
--- a/server.js
+++ b/server.js
@@ -277,6 +277,8 @@ console.log('Collaboration handler initialized');
app.use('/api/auth', require('./middleware/rateLimiter').authLimiter, authRoutes);
app.use('/api/expenses', expenseRoutes); // Expense management
app.use('/api/currency', require('./routes/currency'));
+app.use('/api/treasury', require('./routes/treasury'));
+app.use('/api/payroll', require('./routes/payroll'));
app.use('/api/groups', require('./routes/groups'));
app.use('/api/splits', require('./routes/splits'));
app.use('/api/workspaces', require('./routes/workspaces'));
diff --git a/services/cronJobs.js b/services/cronJobs.js
index 5f9c5227..26d7d710 100644
--- a/services/cronJobs.js
+++ b/services/cronJobs.js
@@ -344,6 +344,40 @@ class CronJobs {
await this.retrainCategorizationModels();
});
+ // Monthly payroll generation - 1st day of month at 12 AM UTC
+ cron.schedule('0 0 1 * *', async () => {
+ try {
+ console.log('[CronJobs] Running automated monthly payroll generation...');
+ const payrollService = require('./payrollService');
+ const User = require('../models/User');
+
+ const currentDate = new Date();
+ const month = currentDate.getMonth() + 1;
+ const year = currentDate.getFullYear();
+
+ // Get all users with active salary structures
+ const SalaryStructure = require('../models/SalaryStructure');
+ const usersWithPayroll = await SalaryStructure.distinct('userId', { isActive: true });
+
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const userId of usersWithPayroll) {
+ try {
+ await payrollService.generatePayroll(userId, month, year);
+ successCount++;
+ } catch (err) {
+ console.error(`[CronJobs] Failed to generate payroll for user ${userId}:`, err.message);
+ failCount++;
+ }
+ }
+
+ console.log(`[CronJobs] Payroll generation completed: ${successCount} success, ${failCount} failed`);
+ } catch (err) {
+ console.error('[CronJobs] Error in monthly payroll generation:', err);
+ }
+ });
+
console.log('Cron jobs initialized successfully');
}
diff --git a/services/deductionEngine.js b/services/deductionEngine.js
new file mode 100644
index 00000000..97cf4cb7
--- /dev/null
+++ b/services/deductionEngine.js
@@ -0,0 +1,271 @@
+/**
+ * Deduction Engine
+ * Calculates statutory deductions including TDS, Professional Tax, PF, ESI
+ */
+
+class DeductionEngine {
+ /**
+ * Calculate Income Tax (TDS) based on tax regime and salary
+ */
+ calculateIncomeTax(annualIncome, taxRegime = 'new', deductions = {}) {
+ if (taxRegime === 'new') {
+ return this.calculateNewRegimeTax(annualIncome);
+ } else {
+ return this.calculateOldRegimeTax(annualIncome, deductions);
+ }
+ }
+
+ /**
+ * New Tax Regime (FY 2023-24 onwards)
+ */
+ calculateNewRegimeTax(annualIncome) {
+ let tax = 0;
+
+ // Tax slabs for new regime
+ const slabs = [
+ { limit: 300000, rate: 0 },
+ { limit: 600000, rate: 0.05 },
+ { limit: 900000, rate: 0.10 },
+ { limit: 1200000, rate: 0.15 },
+ { limit: 1500000, rate: 0.20 },
+ { limit: Infinity, rate: 0.30 }
+ ];
+
+ let previousLimit = 0;
+ for (const slab of slabs) {
+ if (annualIncome > previousLimit) {
+ const taxableInSlab = Math.min(annualIncome, slab.limit) - previousLimit;
+ tax += taxableInSlab * slab.rate;
+ previousLimit = slab.limit;
+ } else {
+ break;
+ }
+ }
+
+ // Add cess (4% of tax)
+ tax += tax * 0.04;
+
+ // Rebate under section 87A (if income <= 7 lakhs)
+ if (annualIncome <= 700000) {
+ tax = Math.max(0, tax - 25000);
+ }
+
+ return Math.round(tax);
+ }
+
+ /**
+ * Old Tax Regime with deductions
+ */
+ calculateOldRegimeTax(annualIncome, deductions = {}) {
+ // Standard deduction
+ const standardDeduction = 50000;
+
+ // Section 80C (max 150000)
+ const section80C = Math.min(deductions.section80C || 0, 150000);
+
+ // Section 80D (Health insurance - max 25000 for self, 50000 if senior citizen)
+ const section80D = Math.min(deductions.section80D || 0, 25000);
+
+ // HRA exemption
+ const hraExemption = deductions.hraExemption || 0;
+
+ // Calculate taxable income
+ let taxableIncome = annualIncome - standardDeduction - section80C - section80D - hraExemption;
+ taxableIncome = Math.max(0, taxableIncome);
+
+ let tax = 0;
+
+ // Tax slabs for old regime
+ const slabs = [
+ { limit: 250000, rate: 0 },
+ { limit: 500000, rate: 0.05 },
+ { limit: 1000000, rate: 0.20 },
+ { limit: Infinity, rate: 0.30 }
+ ];
+
+ let previousLimit = 0;
+ for (const slab of slabs) {
+ if (taxableIncome > previousLimit) {
+ const taxableInSlab = Math.min(taxableIncome, slab.limit) - previousLimit;
+ tax += taxableInSlab * slab.rate;
+ previousLimit = slab.limit;
+ } else {
+ break;
+ }
+ }
+
+ // Add cess (4% of tax)
+ tax += tax * 0.04;
+
+ // Rebate under section 87A (if taxable income <= 5 lakhs)
+ if (taxableIncome <= 500000) {
+ tax = Math.max(0, tax - 12500);
+ }
+
+ return Math.round(tax);
+ }
+
+ /**
+ * Calculate Professional Tax (State-specific)
+ * Using Maharashtra rates as example
+ */
+ calculateProfessionalTax(monthlySalary, state = 'Maharashtra') {
+ const stateTaxRates = {
+ 'Maharashtra': [
+ { limit: 7500, tax: 0 },
+ { limit: 10000, tax: 175 },
+ { limit: Infinity, tax: 200 }
+ ],
+ 'Karnataka': [
+ { limit: 15000, tax: 0 },
+ { limit: Infinity, tax: 200 }
+ ],
+ 'West Bengal': [
+ { limit: 10000, tax: 0 },
+ { limit: 15000, tax: 110 },
+ { limit: 25000, tax: 130 },
+ { limit: 40000, tax: 150 },
+ { limit: Infinity, tax: 200 }
+ ]
+ };
+
+ const rates = stateTaxRates[state] || stateTaxRates['Maharashtra'];
+
+ for (const bracket of rates) {
+ if (monthlySalary <= bracket.limit) {
+ return bracket.tax;
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Calculate Provident Fund (PF)
+ * Employee contribution: 12% of Basic + DA
+ * Employer contribution: 12% of Basic + DA (3.67% to EPF, 8.33% to EPS)
+ */
+ calculateProvidentFund(basicSalary, daAllowance = 0) {
+ const pfBase = basicSalary + daAllowance;
+ const pfWageLimit = 15000; // PF wage ceiling
+
+ const contributoryWage = Math.min(pfBase, pfWageLimit);
+ const employeeContribution = Math.round(contributoryWage * 0.12);
+ const employerContribution = Math.round(contributoryWage * 0.12);
+
+ return {
+ employeeContribution,
+ employerContribution,
+ totalContribution: employeeContribution + employerContribution,
+ contributoryWage
+ };
+ }
+
+ /**
+ * Calculate Employee State Insurance (ESI)
+ * Applicable if gross salary <= 21,000 per month
+ * Employee: 0.75%, Employer: 3.25%
+ */
+ calculateESI(grossSalary) {
+ const esiLimit = 21000;
+
+ if (grossSalary > esiLimit) {
+ return {
+ employeeContribution: 0,
+ employerContribution: 0,
+ totalContribution: 0,
+ isApplicable: false
+ };
+ }
+
+ const employeeContribution = Math.round(grossSalary * 0.0075);
+ const employerContribution = Math.round(grossSalary * 0.0325);
+
+ return {
+ employeeContribution,
+ employerContribution,
+ totalContribution: employeeContribution + employerContribution,
+ isApplicable: true
+ };
+ }
+
+ /**
+ * Calculate HRA exemption (for old tax regime)
+ */
+ calculateHRAExemption(basicSalary, hraReceived, rentPaid, isMetro = false) {
+ // HRA exemption is minimum of:
+ // 1. Actual HRA received
+ // 2. 50% of basic (metro) or 40% of basic (non-metro)
+ // 3. Rent paid - 10% of basic
+
+ const metroPercentage = isMetro ? 0.50 : 0.40;
+
+ const option1 = hraReceived;
+ const option2 = basicSalary * metroPercentage;
+ const option3 = Math.max(0, rentPaid - (basicSalary * 0.10));
+
+ const exemption = Math.min(option1, option2, option3);
+
+ return Math.max(0, exemption);
+ }
+
+ /**
+ * Calculate LTA (Leave Travel Allowance) exemption
+ */
+ calculateLTAExemption(ltaReceived, actualTravelExpense) {
+ // LTA exemption is minimum of actual LTA received and actual travel expense
+ // Limited to 2 journeys in a block of 4 years
+ return Math.min(ltaReceived, actualTravelExpense);
+ }
+
+ /**
+ * Calculate total monthly deductions for an employee
+ */
+ calculateMonthlyDeductions(salaryComponents, taxRegime = 'new', state = 'Maharashtra') {
+ const basic = salaryComponents.basic || 0;
+ const hra = salaryComponents.hra || 0;
+ const da = salaryComponents.da || 0;
+ const grossSalary = salaryComponents.gross || 0;
+ const annualIncome = grossSalary * 12;
+
+ // Calculate annual tax
+ const annualTax = this.calculateIncomeTax(annualIncome, taxRegime);
+ const monthlyTDS = Math.round(annualTax / 12);
+
+ // Professional Tax
+ const professionalTax = this.calculateProfessionalTax(grossSalary, state);
+
+ // PF
+ const pf = this.calculateProvidentFund(basic, da);
+
+ // ESI
+ const esi = this.calculateESI(grossSalary);
+
+ return {
+ tds: monthlyTDS,
+ professionalTax,
+ providentFund: pf.employeeContribution,
+ esi: esi.employeeContribution,
+ totalDeductions: monthlyTDS + professionalTax + pf.employeeContribution + esi.employeeContribution,
+ breakdown: {
+ annualTax,
+ pf,
+ esi
+ }
+ };
+ }
+
+ /**
+ * Calculate take-home salary
+ */
+ calculateTakeHome(grossSalary, deductions) {
+ const totalDeductions = deductions.tds +
+ deductions.professionalTax +
+ deductions.providentFund +
+ deductions.esi;
+
+ return grossSalary - totalDeductions;
+ }
+}
+
+module.exports = new DeductionEngine();
diff --git a/services/payrollService.js b/services/payrollService.js
new file mode 100644
index 00000000..02aa1d89
--- /dev/null
+++ b/services/payrollService.js
@@ -0,0 +1,331 @@
+const SalaryStructure = require('../models/SalaryStructure');
+const PayrollRun = require('../models/PayrollRun');
+const EmployeePerk = require('../models/EmployeePerk');
+const deductionEngine = require('./deductionEngine');
+
+class PayrollService {
+ /**
+ * Generate payroll for a specific period
+ */
+ async generatePayroll(userId, month, year) {
+ const periodStart = new Date(year, month - 1, 1);
+ const periodEnd = new Date(year, month, 0);
+
+ // Get all active salary structures
+ const salaryStructures = await SalaryStructure.find({
+ userId,
+ isActive: true,
+ effectiveFrom: { $lte: periodEnd }
+ });
+
+ if (salaryStructures.length === 0) {
+ throw new Error('No active salary structures found');
+ }
+
+ // Generate payroll entries
+ const entries = [];
+
+ for (const structure of salaryStructures) {
+ const entry = await this.generatePayrollEntry(structure, month, year);
+ entries.push(entry);
+ }
+
+ // Create payroll run
+ const runId = `PR-${year}${String(month).padStart(2, '0')}-${Date.now()}`;
+
+ const payrollRun = new PayrollRun({
+ userId,
+ runId,
+ payrollPeriod: { month, year },
+ periodStart,
+ periodEnd,
+ entries,
+ status: 'draft'
+ });
+
+ await payrollRun.save();
+ return payrollRun;
+ }
+
+ /**
+ * Generate individual payroll entry
+ */
+ async generatePayrollEntry(salaryStructure, month, year) {
+ // Extract components
+ const earnings = salaryStructure.components.filter(c => c.componentType === 'earning');
+ const deductions = salaryStructure.components.filter(c => c.componentType === 'deduction');
+ const reimbursements = salaryStructure.components.filter(c => c.componentType === 'reimbursement');
+
+ // Calculate component amounts
+ const calculatedEarnings = this.calculateComponents(earnings, salaryStructure);
+ const calculatedDeductions = this.calculateComponents(deductions, salaryStructure);
+ const calculatedReimbursements = this.calculateComponents(reimbursements, salaryStructure);
+
+ // Calculate gross pay
+ const grossPay = calculatedEarnings.reduce((sum, e) => sum + e.amount, 0);
+
+ // Get basic and HRA for statutory calculations
+ const basic = calculatedEarnings.find(e => e.componentName.toLowerCase().includes('basic'))?.amount || grossPay * 0.4;
+ const hra = calculatedEarnings.find(e => e.componentName.toLowerCase().includes('hra'))?.amount || 0;
+ const da = calculatedEarnings.find(e => e.componentName.toLowerCase().includes('da'))?.amount || 0;
+
+ // Calculate statutory deductions
+ const statutory = deductionEngine.calculateMonthlyDeductions({
+ basic,
+ hra,
+ da,
+ gross: grossPay
+ }, salaryStructure.taxRegime);
+
+ // Add perks taxable value
+ const perks = await EmployeePerk.find({
+ userId: salaryStructure.userId,
+ employeeId: salaryStructure.employeeId,
+ status: 'active',
+ effectiveFrom: { $lte: new Date(year, month - 1, 1) }
+ });
+
+ const perksTaxableValue = perks
+ .filter(p => p.isTaxable && p.frequency === 'monthly')
+ .reduce((sum, p) => sum + p.taxableValue, 0);
+
+ // Combine all deductions
+ const allDeductions = [
+ ...calculatedDeductions,
+ { componentName: 'TDS', amount: statutory.tds },
+ { componentName: 'Professional Tax', amount: statutory.professionalTax },
+ { componentName: 'Provident Fund', amount: statutory.providentFund },
+ { componentName: 'ESI', amount: statutory.esi }
+ ];
+
+ const totalDeductions = allDeductions.reduce((sum, d) => sum + d.amount, 0);
+ const netPay = grossPay - totalDeductions;
+
+ return {
+ employeeId: salaryStructure.employeeId,
+ employeeName: salaryStructure.employeeName,
+ salaryStructureId: salaryStructure._id,
+ earnings: calculatedEarnings,
+ deductions: allDeductions,
+ reimbursements: calculatedReimbursements,
+ grossPay,
+ totalDeductions,
+ netPay,
+ taxDeducted: statutory.tds,
+ professionalTax: statutory.professionalTax,
+ providentFund: statutory.providentFund,
+ esi: statutory.esi,
+ paymentStatus: 'pending'
+ };
+ }
+
+ /**
+ * Calculate component amounts based on calculation type
+ */
+ calculateComponents(components, salaryStructure) {
+ return components.map(component => {
+ let amount = 0;
+
+ switch (component.calculationType) {
+ case 'fixed':
+ amount = component.amount;
+ break;
+
+ case 'percentage':
+ if (component.baseComponent) {
+ const baseComp = salaryStructure.components.find(
+ c => c.componentName === component.baseComponent
+ );
+ amount = baseComp ? (baseComp.amount * component.percentage / 100) : 0;
+ } else {
+ // Percentage of CTC
+ amount = salaryStructure.ctc * component.percentage / 100;
+ }
+ break;
+
+ case 'formula':
+ // For complex formulas, implement custom logic
+ amount = component.amount;
+ break;
+ }
+
+ return {
+ componentName: component.componentName,
+ amount: Math.round(amount)
+ };
+ });
+ }
+
+ /**
+ * Approve payroll run
+ */
+ async approvePayroll(payrollRunId, approverId) {
+ const payrollRun = await PayrollRun.findById(payrollRunId);
+
+ if (!payrollRun) {
+ throw new Error('Payroll run not found');
+ }
+
+ if (payrollRun.status !== 'draft' && payrollRun.status !== 'pending_approval') {
+ throw new Error('Payroll run cannot be approved in current status');
+ }
+
+ payrollRun.status = 'approved';
+ payrollRun.approvedBy = approverId;
+ payrollRun.approvedAt = new Date();
+
+ await payrollRun.save();
+ return payrollRun;
+ }
+
+ /**
+ * Process payroll (mark as processing/completed)
+ */
+ async processPayroll(payrollRunId) {
+ const payrollRun = await PayrollRun.findById(payrollRunId);
+
+ if (!payrollRun) {
+ throw new Error('Payroll run not found');
+ }
+
+ if (payrollRun.status !== 'approved') {
+ throw new Error('Payroll must be approved before processing');
+ }
+
+ payrollRun.status = 'processing';
+ await payrollRun.save();
+
+ // Simulate payment processing
+ // In real implementation, integrate with payment gateway
+
+ for (const entry of payrollRun.entries) {
+ entry.paymentStatus = 'processed';
+ entry.paymentDate = new Date();
+ entry.paymentReference = `PAY-${Date.now()}-${entry.employeeId}`;
+ }
+
+ payrollRun.status = 'completed';
+ payrollRun.processedAt = new Date();
+ await payrollRun.save();
+
+ return payrollRun;
+ }
+
+ /**
+ * Get payroll dashboard statistics
+ */
+ async getPayrollDashboard(userId) {
+ const currentDate = new Date();
+ const currentMonth = currentDate.getMonth() + 1;
+ const currentYear = currentDate.getFullYear();
+
+ // Get current month payroll
+ const currentPayroll = await PayrollRun.findOne({
+ userId,
+ 'payrollPeriod.month': currentMonth,
+ 'payrollPeriod.year': currentYear
+ });
+
+ // Get all active employees
+ const activeEmployees = await SalaryStructure.find({ userId, isActive: true });
+
+ // Get last 6 months payroll history
+ const sixMonthsAgo = new Date();
+ sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
+
+ const payrollHistory = await PayrollRun.find({
+ userId,
+ periodStart: { $gte: sixMonthsAgo }
+ }).sort({ periodStart: -1 });
+
+ // Calculate trends
+ const monthlyTrends = payrollHistory.map(pr => ({
+ month: pr.payrollPeriod.month,
+ year: pr.payrollPeriod.year,
+ totalNetPay: pr.summary.totalNetPay,
+ totalTax: pr.summary.totalTax,
+ employeeCount: pr.summary.totalEmployees
+ }));
+
+ return {
+ currentPayroll,
+ activeEmployeeCount: activeEmployees.length,
+ monthlyTrends,
+ pendingApprovals: await PayrollRun.countDocuments({
+ userId,
+ status: { $in: ['draft', 'pending_approval'] }
+ })
+ };
+ }
+
+ /**
+ * Get employee payslip
+ */
+ async getPayslip(payrollRunId, employeeId) {
+ const payrollRun = await PayrollRun.findById(payrollRunId);
+
+ if (!payrollRun) {
+ throw new Error('Payroll run not found');
+ }
+
+ const entry = payrollRun.entries.find(e => e.employeeId === employeeId);
+
+ if (!entry) {
+ throw new Error('Employee not found in this payroll run');
+ }
+
+ return {
+ payrollPeriod: payrollRun.payrollPeriod,
+ employee: {
+ id: entry.employeeId,
+ name: entry.employeeName
+ },
+ earnings: entry.earnings,
+ deductions: entry.deductions,
+ reimbursements: entry.reimbursements,
+ grossPay: entry.grossPay,
+ totalDeductions: entry.totalDeductions,
+ netPay: entry.netPay,
+ paymentStatus: entry.paymentStatus,
+ paymentDate: entry.paymentDate,
+ paymentReference: entry.paymentReference
+ };
+ }
+
+ /**
+ * Calculate year-to-date (YTD) statistics for an employee
+ */
+ async getEmployeeYTD(userId, employeeId, year) {
+ const payrollRuns = await PayrollRun.find({
+ userId,
+ 'payrollPeriod.year': year,
+ status: 'completed'
+ });
+
+ let ytdGross = 0;
+ let ytdDeductions = 0;
+ let ytdNet = 0;
+ let ytdTax = 0;
+
+ for (const run of payrollRuns) {
+ const entry = run.entries.find(e => e.employeeId === employeeId);
+ if (entry) {
+ ytdGross += entry.grossPay;
+ ytdDeductions += entry.totalDeductions;
+ ytdNet += entry.netPay;
+ ytdTax += entry.taxDeducted;
+ }
+ }
+
+ return {
+ year,
+ ytdGross,
+ ytdDeductions,
+ ytdNet,
+ ytdTax,
+ monthsProcessed: payrollRuns.length
+ };
+ }
+}
+
+module.exports = new PayrollService();