vernova/app/Http/Controllers/PayrollController.php
2026-07-26 19:58:27 +03:30

468 lines
18 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Employee;
use App\Models\EmployeeFinancial;
use App\Models\EmployeeWorkLog;
use App\Models\Currency;
use App\Models\PayrollPeriod;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class PayrollController extends Controller
{
/**
* Display payroll dashboard with stats.
*/
public function index(Request $request)
{
$currentYear = now()->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'));
}
}