year; $currentMonth = now()->month; $year = $request->get('year', $currentYear); $month = $request->get('month', $currentMonth); // Statistics for the selected month/year $financials = EmployeeFinancial::with(['employee', 'currency']) ->where('tenant_id', $this->getTenantId()) ->where('month', $month) ->where('year', $year) ->where('is_settlement', false) ->orderBy('employee_id') ->orderBy('type') ->paginate(30) ->appends($request->query()); // Calculate totals for the month $allMonthFinancials = EmployeeFinancial::where('tenant_id', $this->getTenantId()) ->where('month', $month) ->where('year', $year) ->where('is_settlement', false) ->get(); $totalGross = $allMonthFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $totalDeductions = $allMonthFinancials->where('type', 'deduction')->sum('amount'); $totalAdvances = $allMonthFinancials->where('type', 'advance')->sum('amount'); $totalNet = $totalGross - $totalDeductions - $totalAdvances; $totalPaid = $allMonthFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $totalUnpaid = $totalGross - $totalPaid; // Employee count with financials this month $activeEmployeeCount = $allMonthFinancials->pluck('employee_id')->unique()->count(); $unpaidEmployeeCount = $allMonthFinancials->where('is_paid', false)->pluck('employee_id')->unique()->count(); $employees = Employee::where('tenant_id', $this->getTenantId()) ->where('status', 'active') ->get(); $months = range(1, 12); $years = range($currentYear - 2, $currentYear + 1); // Persian month names $persianMonths = [ 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند' ]; return view('payroll.index', compact( 'financials', 'employees', 'months', 'years', 'year', 'month', 'totalGross', 'totalDeductions', 'totalAdvances', 'totalNet', 'totalPaid', 'totalUnpaid', 'activeEmployeeCount', 'unpaidEmployeeCount', 'persianMonths' )); } /** * Show payroll calculation form. */ public function calculate(Request $request) { $employees = Employee::where('tenant_id', $this->getTenantId()) ->where('status', 'active') ->with(['financials' => function ($q) { $q->where('month', now()->month)->where('year', now()->year); }]) ->get(); $currencies = Currency::where('is_active', true)->get(); $currentMonth = now()->month; $currentYear = now()->year; return view('payroll.calculate', compact('employees', 'currencies', 'currentMonth', 'currentYear')); } /** * Auto-calculate payroll for a month. * Generates salary entries for all active employees based on base_salary + work logs. */ public function autoCalculate(Request $request) { $validated = $request->validate([ 'month' => 'required|integer|min:1|max:12', 'year' => 'required|integer|min:1400|max:1500', 'employee_ids' => 'nullable|array', 'employee_ids.*' => 'exists:employees,id', 'include_worklogs' => 'nullable|boolean', ]); $tenantId = $this->getTenantId(); $month = $validated['month']; $year = $validated['year']; // Get employees to process $query = Employee::where('tenant_id', $tenantId)->where('status', 'active'); if (!empty($validated['employee_ids'])) { $query->whereIn('id', $validated['employee_ids']); } $employees = $query->get(); $created = 0; $skipped = 0; foreach ($employees as $employee) { // Check if salary already exists for this month $existingSalary = EmployeeFinancial::where('tenant_id', $tenantId) ->where('employee_id', $employee->id) ->where('type', 'salary') ->where('month', $month) ->where('year', $year) ->where('is_settlement', false) ->first(); if ($existingSalary) { $skipped++; continue; } // Calculate overtime from work logs $overtimeHours = 0; $regularHours = 0; if (!empty($validated['include_worklogs'])) { $workLogs = EmployeeWorkLog::where('employee_id', $employee->id) ->whereMonth('date', $month) ->whereYear('date', $year) ->get(); $regularHours = $workLogs->sum('hours'); $overtimeHours = $workLogs->sum('overtime_hours'); } // Get currency $currencyId = $employee->currency_id ?? Currency::where('is_default', true)->value('id'); // Create salary entry if ($employee->base_salary > 0) { EmployeeFinancial::create([ 'tenant_id' => $tenantId, 'employee_id' => $employee->id, 'type' => 'salary', 'amount' => $employee->base_salary, 'currency_id' => $currencyId, 'effective_date' => now(), 'month' => $month, 'year' => $year, 'is_paid' => false, 'created_by' => Auth::id(), 'description' => __('payroll.auto_salary_description', ['month' => $month, 'year' => $year]), ]); $created++; } // Create overtime entry if there are overtime hours if ($overtimeHours > 0 && $employee->base_salary > 0) { $hourlyRate = $employee->base_salary / 200; // rough estimate: 200 working hours/month $overtimeAmount = round($overtimeHours * $hourlyRate * 1.4); // 1.4x for overtime EmployeeFinancial::create([ 'tenant_id' => $tenantId, 'employee_id' => $employee->id, 'type' => 'overtime', 'amount' => $overtimeAmount, 'currency_id' => $currencyId, 'effective_date' => now(), 'month' => $month, 'year' => $year, 'is_paid' => false, 'created_by' => Auth::id(), 'description' => __('payroll.auto_overtime_description', ['hours' => $overtimeHours]), ]); } } $message = __('payroll.auto_calculated', ['created' => $created, 'skipped' => $skipped]); return redirect()->route('payroll.index', ['month' => $month, 'year' => $year]) ->with('success', $message); } /** * Store a new payroll entry. */ public function store(Request $request) { $validated = $request->validate([ 'employee_id' => 'required|exists:employees,id', 'type' => 'required|in:salary,overtime,bonus,deduction,advance,allowance', 'amount' => 'required|numeric|min:0', 'currency_id' => 'nullable|exists:currencies,id', 'description' => 'nullable|string', 'effective_date' => 'required|date', 'month' => 'required|integer|min:1|max:12', 'year' => 'required|integer|min:1400|max:1500', 'notes' => 'nullable|string', ]); $validated['tenant_id'] = $this->getTenantId(); $validated['created_by'] = Auth::id(); $validated['is_paid'] = false; // Convert jalali date if needed if (!empty($validated['effective_date'])) { $validated['effective_date'] = \App\Helpers\CalendarHelper::parseDate($validated['effective_date']); } EmployeeFinancial::create($validated); return redirect() ->route('payroll.index', ['month' => $validated['month'], 'year' => $validated['year']]) ->with('success', __('payroll.created_successfully')); } /** * Show employee payroll detail. */ public function show(Employee $employee, Request $request) { $year = $request->get('year', now()->year); $financials = EmployeeFinancial::with(['currency']) ->where('employee_id', $employee->id) ->where('year', $year) ->orderBy('month') ->orderBy('type') ->get() ->groupBy('month'); // Calculate yearly totals $allYearFinancials = EmployeeFinancial::where('employee_id', $employee->id) ->where('year', $year) ->get(); $yearlyGross = $allYearFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $yearlyDeductions = $allYearFinancials->where('type', 'deduction')->sum('amount'); $yearlyAdvances = $allYearFinancials->where('type', 'advance')->sum('amount'); $yearlyNet = $yearlyGross - $yearlyDeductions - $yearlyAdvances; $yearlyPaid = $allYearFinancials->where('is_paid', true)->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $yearlyUnpaid = $yearlyGross - $yearlyPaid; // Work logs for the year $workLogs = EmployeeWorkLog::where('employee_id', $employee->id) ->whereYear('date', $year) ->get() ->groupBy(function ($item) { return \App\Helpers\CalendarHelper::formatDate($item->date, 'n'); }); $persianMonths = [ 1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد', 4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور', 7 => 'مهر', 8 => 'آبان', 9 => 'آذر', 10 => 'دی', 11 => 'بهمن', 12 => 'اسفند' ]; return view('payroll.show', compact( 'employee', 'financials', 'year', 'persianMonths', 'yearlyGross', 'yearlyDeductions', 'yearlyAdvances', 'yearlyNet', 'yearlyPaid', 'yearlyUnpaid' )); } /** * Mark a financial entry as paid. */ public function pay(EmployeeFinancial $financial) { $financial->update([ 'is_paid' => true, 'paid_at' => now(), ]); return redirect()->back() ->with('success', __('payroll.marked_as_paid')); } /** * Bulk pay financial entries. */ public function payBulk(Request $request) { $validated = $request->validate([ 'financial_ids' => 'required|array', 'financial_ids.*' => 'exists:employee_financials,id', ]); $count = EmployeeFinancial::whereIn('id', $validated['financial_ids']) ->where('is_paid', false) ->update([ 'is_paid' => true, 'paid_at' => now(), ]); return redirect()->back() ->with('success', __('payroll.bulk_paid_successfully') . " ($count)"); } /** * Delete a financial entry. */ public function destroy(EmployeeFinancial $financial) { if ($financial->is_paid) { return redirect()->back() ->with('error', __('payroll.cannot_delete_paid')); } $financial->delete(); return redirect()->back() ->with('success', __('payroll.deleted_successfully')); } /** * Show settlement form. */ public function settlement() { $employees = Employee::where('tenant_id', $this->getTenantId()) ->whereIn('status', ['active', 'inactive']) ->get(); $currencies = Currency::where('is_active', true)->get(); return view('payroll.settlement', compact('employees', 'currencies')); } /** * Preview settlement calculation. */ public function previewSettlement(Request $request) { $validated = $request->validate([ 'employee_id' => 'required|exists:employees,id', 'settlement_type' => 'required|in:resignation,termination,end_of_contract', 'settlement_date' => 'required|date', 'last_working_date' => 'nullable|date', ]); $employee = Employee::findOrFail($validated['employee_id']); $settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']); // Get all unpaid financials $unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id) ->where('is_paid', false) ->with('currency') ->get(); $totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount'); $totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount'); $netPayable = $totalPayable - $totalDeductions - $totalAdvances; // Unused vacation days calculation (rough: 2.5 days per month) $monthsWorked = 0; if ($employee->hire_date) { $hireDate = \Carbon\Carbon::parse($employee->hire_date); $settlementCarbon = \Carbon\Carbon::parse($settlementDate); $monthsWorked = $hireDate->diffInMonths($settlementCarbon); } $vacationDays = min($monthsWorked * 2.5, 26); // max 26 days return response()->json([ 'employee' => $employee->only(['id', 'first_name', 'last_name', 'base_salary']), 'unpaid_financials' => $unpaidFinancials->map(function ($f) { return [ 'id' => $f->id, 'type' => $f->type, 'amount' => $f->amount, 'month' => $f->month, 'year' => $f->year, 'description' => $f->description, ]; }), 'total_payable' => $totalPayable, 'total_deductions' => $totalDeductions, 'total_advances' => $totalAdvances, 'net_payable' => $netPayable, 'months_worked' => $monthsWorked, 'vacation_days' => $vacationDays, 'financial_count' => $unpaidFinancials->count(), ]); } /** * Process settlement. */ public function processSettlement(Request $request) { $validated = $request->validate([ 'employee_id' => 'required|exists:employees,id', 'settlement_type' => 'required|in:resignation,termination,end_of_contract', 'settlement_date' => 'required|date', 'last_working_date' => 'nullable|date', 'notes' => 'nullable|string', ]); $employee = Employee::findOrFail($validated['employee_id']); $tenantId = $this->getTenantId(); $settlementDate = \App\Helpers\CalendarHelper::parseDate($validated['settlement_date']); DB::transaction(function () use ($employee, $validated, $tenantId, $settlementDate) { // Get all unpaid financials $unpaidFinancials = EmployeeFinancial::where('employee_id', $employee->id) ->where('is_paid', false) ->get(); $totalPayable = $unpaidFinancials->whereIn('type', ['salary', 'overtime', 'bonus', 'allowance'])->sum('amount'); $totalDeductions = $unpaidFinancials->where('type', 'deduction')->sum('amount'); $totalAdvances = $unpaidFinancials->where('type', 'advance')->sum('amount'); $netPayable = $totalPayable - $totalDeductions - $totalAdvances; // Create settlement record $settlement = \App\Models\EmployeeSettlement::create([ 'tenant_id' => $tenantId, 'employee_id' => $employee->id, 'settlement_type' => $validated['settlement_type'], 'settlement_date' => $settlementDate, 'last_working_date' => !empty($validated['last_working_date']) ? \App\Helpers\CalendarHelper::parseDate($validated['last_working_date']) : null, 'total_payable' => $totalPayable, 'total_deductions' => $totalDeductions + $totalAdvances, 'net_payable' => $netPayable, 'currency_id' => $employee->currency_id, 'notes' => $validated['notes'] ?? null, 'processed_by' => Auth::id(), 'processed_at' => now(), ]); // Mark all unpaid as paid and link to settlement $unpaidFinancials->each->update([ 'is_paid' => true, 'paid_at' => now(), 'is_settlement' => true, 'settlement_id' => $settlement->id, ]); // Update employee status $employee->update([ 'status' => 'terminated', 'termination_date' => $settlementDate, ]); }); return redirect()->route('payroll.index') ->with('success', __('payroll.settlement_processed')); } }