539 lines
23 KiB
PHP
539 lines
23 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Employee;
|
|
use App\Models\EmployeeSalary;
|
|
use App\Models\SalaryComponent;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Inertia\Inertia;
|
|
|
|
class EmployeeSalaryController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
if (Auth::user()->can('manage-employee-salaries')) {
|
|
// Auto-create salary records for employees who don't have one
|
|
$companyEmployees = User::with('employee')
|
|
->where('type', 'employee')
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->where('status', 'active')
|
|
->get();
|
|
// if (Auth::user()->can('manage-any-employee-salaries')) {
|
|
// foreach ($companyEmployees as $employee) {
|
|
// $exists = EmployeeSalary::where('employee_id', $employee->id)->exists();
|
|
// if (!$exists) {
|
|
// EmployeeSalary::create([
|
|
// 'employee_id' => $employee->id,
|
|
// 'basic_salary' => $employee->employee?->base_salary ?? 0,
|
|
// 'components' => null,
|
|
// 'is_active' => true,
|
|
// 'created_by' => creatorId(),
|
|
// ]);
|
|
// } else {
|
|
// if (is_null($employee->employee->base_salary)) {
|
|
// // If base salary is null in employee table then it will update the employee salary in employee table
|
|
// $getEmployeeBaseSalary = EmployeeSalary::where('employee_id', $employee->employee->user_id)->first();
|
|
// if ($getEmployeeBaseSalary) {
|
|
// $employee->employee->base_salary = $getEmployeeBaseSalary->basic_salary;
|
|
// $employee->employee->save();
|
|
// }
|
|
// } else {
|
|
// // If salary update on employee table it will automatically affect on Employee salary table
|
|
// $getEmployeeBaseSalary = EmployeeSalary::where('employee_id', $employee->employee->user_id)->first();
|
|
// if ($getEmployeeBaseSalary) {
|
|
// $getEmployeeBaseSalary->basic_salary = $employee->employee->base_salary;
|
|
// $getEmployeeBaseSalary->save();
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
if (Auth::user()->can('manage-any-employee-salaries')) {
|
|
foreach ($companyEmployees as $employee) {
|
|
|
|
// Safety check: employee relation must exist
|
|
if (!isset($employee->employee)) {
|
|
continue;
|
|
}
|
|
|
|
$employeeModel = $employee->employee;
|
|
|
|
// Fetch salary record once
|
|
$employeeSalary = EmployeeSalary::where('employee_id', $employee->id)->first();
|
|
|
|
// If salary record does not exist → create
|
|
if (!$employeeSalary) {
|
|
|
|
EmployeeSalary::create([
|
|
'employee_id' => $employee->id,
|
|
'basic_salary' => $employeeModel->base_salary ?? 0,
|
|
'pay_frequency' => 'monthly',
|
|
'components' => null,
|
|
'is_active' => true,
|
|
'created_by' => creatorId(),
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
// If base_salary is NULL in employee table → update employee table
|
|
if (is_null($employeeModel->base_salary)) {
|
|
|
|
if (!is_null($employeeSalary->basic_salary)) {
|
|
$employeeModel->base_salary = $employeeSalary->basic_salary;
|
|
$employeeModel->save();
|
|
}
|
|
|
|
}
|
|
// If base_salary exists → update salary table
|
|
else {
|
|
|
|
if ($employeeSalary->basic_salary != $employeeModel->base_salary) {
|
|
$employeeSalary->basic_salary = $employeeModel->base_salary;
|
|
$employeeSalary->save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$query = EmployeeSalary::with(['employee', 'creator'])->where(function ($q) {
|
|
if (Auth::user()->can('manage-any-employee-salaries')) {
|
|
$q->whereIn('created_by', getCompanyAndUsersId());
|
|
} elseif (Auth::user()->can('manage-own-employee-salaries')) {
|
|
$q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->where('is_active', 1);
|
|
} else {
|
|
$q->whereRaw('1 = 0');
|
|
}
|
|
});
|
|
|
|
// Handle search
|
|
if ($request->has('search') && !empty($request->search)) {
|
|
$query->where(function ($q) use ($request) {
|
|
$q->whereHas('employee', function ($subQ) use ($request) {
|
|
$subQ->where('name', 'like', '%' . $request->search . '%');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle employee filter
|
|
if ($request->has('employee_id') && !empty($request->employee_id) && $request->employee_id !== 'all') {
|
|
$query->where('employee_id', $request->employee_id);
|
|
}
|
|
|
|
|
|
|
|
// Handle active status filter
|
|
if ($request->has('is_active') && !empty($request->is_active) && $request->is_active !== 'all') {
|
|
$query->where('is_active', $request->is_active === 'active');
|
|
}
|
|
|
|
// Handle sorting
|
|
if ($request->has('sort_field') && !empty($request->sort_field)) {
|
|
$sortField = $request->sort_field;
|
|
$sortDirection = $request->sort_direction ?? 'asc';
|
|
|
|
if ($sortField === 'basic_salary') {
|
|
$query->orderBy('basic_salary', $sortDirection);
|
|
} else {
|
|
$query->orderBy('id', 'desc');
|
|
}
|
|
} else {
|
|
$query->orderBy('id', 'desc');
|
|
}
|
|
|
|
$employeeSalaries = $query->paginate($request->per_page ?? 10);
|
|
|
|
// Load component names and types for each salary record
|
|
$employeeSalaries->getCollection()->transform(function ($salary) {
|
|
if ($salary->components) {
|
|
$components = SalaryComponent::whereIn('id', $salary->components)
|
|
->get(['id', 'name', 'type']);
|
|
$salary->component_names = $components->pluck('name')->toArray();
|
|
$salary->component_types = $components->pluck('type')->toArray();
|
|
} else {
|
|
$salary->component_names = [];
|
|
$salary->component_types = [];
|
|
}
|
|
if ($salary->employee) {
|
|
$rawAvatar = $salary->employee->getRawOriginal('avatar');
|
|
$salary->employee->avatar = check_file($rawAvatar)
|
|
? get_file($rawAvatar)
|
|
: get_file('avatars/avatar.png');
|
|
}
|
|
return $salary;
|
|
});
|
|
|
|
|
|
// Get employees for filter dropdown
|
|
$employees = User::where('type', 'employee')
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->get(['id', 'name']);
|
|
|
|
// Get salary components for form
|
|
$salaryComponents = SalaryComponent::where('status', 'active')
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->get(['id', 'name', 'type', 'calculation_type', 'default_amount', 'percentage_of_basic']);
|
|
|
|
return Inertia::render('hr/employee-salaries/index', [
|
|
'employeeSalaries' => $employeeSalaries,
|
|
'employees' => $this->getFilteredEmployees(),
|
|
'salaryComponents' => $salaryComponents,
|
|
'filters' => $request->all(['search', 'employee_id', 'is_active', 'sort_field', 'sort_direction', 'per_page']),
|
|
]);
|
|
} else {
|
|
return redirect()->back()->with('error', __('Permission Denied.'));
|
|
}
|
|
}
|
|
|
|
private function getFilteredEmployees()
|
|
{
|
|
// Get employees for filter dropdown (compatible with getFilteredEmployees logic)
|
|
$employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId());
|
|
|
|
if (Auth::user()->can('manage-own-employee-salaries') && !Auth::user()->can('manage-any-employee-salaries')) {
|
|
$employeeQuery->where(function ($q) {
|
|
$q->where('created_by', Auth::id())->orWhere('user_id', Auth::id());
|
|
});
|
|
}
|
|
|
|
$employees = User::emp()
|
|
->with('employee')
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->where('status', 'active')
|
|
->whereIn('id', $employeeQuery->pluck('user_id'))
|
|
->select('id', 'name')
|
|
->get()
|
|
->map(function ($user) {
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'employee_id' => $user->employee->employee_id ?? '',
|
|
];
|
|
});
|
|
return $employees;
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'employee_id' => 'required|exists:users,id',
|
|
'basic_salary' => 'required|numeric|min:0',
|
|
'pay_frequency' => 'required|in:monthly,semi-monthly,weekly,daily',
|
|
'components' => 'nullable|array',
|
|
'components.*' => 'exists:salary_components,id',
|
|
'sss_fixed' => 'nullable|numeric|min:0',
|
|
'philhealth_fixed' => 'nullable|numeric|min:0',
|
|
'pagibig_fixed' => 'nullable|numeric|min:0',
|
|
'payroll_type' => 'nullable|in:automatic,manual',
|
|
'fixed_overtime' => 'nullable|numeric|min:0',
|
|
'fixed_holiday_pay' => 'nullable|numeric|min:0',
|
|
'fixed_late_deduction' => 'nullable|numeric|min:0',
|
|
'is_time_exempt' => 'boolean|nullable',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
// Check if employee already has salary
|
|
$exists = EmployeeSalary::where('employee_id', $validated['employee_id'])
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return redirect()->back()->with('error', __('Employee already has a salary record. Please update the existing one.'));
|
|
}
|
|
|
|
$validated['created_by'] = creatorId();
|
|
$validated['is_active'] = true;
|
|
|
|
EmployeeSalary::create($validated);
|
|
|
|
// Sync with core employee profile immediately so it doesn't revert on index reload
|
|
if (isset($validated['basic_salary'])) {
|
|
$employeeModel = \App\Models\Employee::where('user_id', $validated['employee_id'])->first();
|
|
if ($employeeModel) {
|
|
$employeeModel->base_salary = $validated['basic_salary'];
|
|
$employeeModel->save();
|
|
}
|
|
}
|
|
|
|
return redirect()->back()->with('success', __('Employee salary created successfully.'));
|
|
}
|
|
|
|
|
|
|
|
public function update(Request $request, $employeeSalaryId)
|
|
{
|
|
$employeeSalary = EmployeeSalary::where('id', $employeeSalaryId)
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->first();
|
|
|
|
if ($employeeSalary) {
|
|
try {
|
|
$validated = $request->validate([
|
|
'employee_id' => 'required|exists:users,id',
|
|
'basic_salary' => 'required|numeric|min:0',
|
|
'pay_frequency' => 'required|in:monthly,semi-monthly,weekly,daily',
|
|
'components' => 'nullable|array',
|
|
'components.*' => 'exists:salary_components,id',
|
|
'sss_fixed' => 'nullable|numeric|min:0',
|
|
'philhealth_fixed' => 'nullable|numeric|min:0',
|
|
'pagibig_fixed' => 'nullable|numeric|min:0',
|
|
'payroll_type' => 'nullable|in:automatic,manual',
|
|
'fixed_overtime' => 'nullable|numeric|min:0',
|
|
'fixed_holiday_pay' => 'nullable|numeric|min:0',
|
|
'fixed_late_deduction' => 'nullable|numeric|min:0',
|
|
'is_active' => 'boolean',
|
|
'is_time_exempt' => 'boolean|nullable',
|
|
'notes' => 'nullable|string',
|
|
]);
|
|
|
|
$employeeSalary->update($validated);
|
|
|
|
// Sync with core employee profile immediately so it doesn't revert on index reload
|
|
if (isset($validated['basic_salary'])) {
|
|
$employeeModel = \App\Models\Employee::where('user_id', $validated['employee_id'])->first();
|
|
if ($employeeModel) {
|
|
$employeeModel->base_salary = $validated['basic_salary'];
|
|
$employeeModel->save();
|
|
}
|
|
}
|
|
|
|
return redirect()->back()->with('success', __('Employee salary updated successfully'));
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee salary'));
|
|
}
|
|
} else {
|
|
return redirect()->back()->with('error', __('Employee salary Not Found.'));
|
|
}
|
|
}
|
|
|
|
public function destroy($employeeSalaryId)
|
|
{
|
|
$employeeSalary = EmployeeSalary::where('id', $employeeSalaryId)
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->first();
|
|
|
|
if ($employeeSalary) {
|
|
try {
|
|
$employeeSalary->delete();
|
|
return redirect()->back()->with('success', __('Employee salary deleted successfully'));
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete employee salary'));
|
|
}
|
|
} else {
|
|
return redirect()->back()->with('error', __('Employee salary Not Found.'));
|
|
}
|
|
}
|
|
|
|
public function toggleStatus($employeeSalaryId)
|
|
{
|
|
$employeeSalary = EmployeeSalary::where('id', $employeeSalaryId)
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->first();
|
|
|
|
if ($employeeSalary) {
|
|
try {
|
|
$employeeSalary->is_active = !$employeeSalary->is_active;
|
|
$employeeSalary->save();
|
|
|
|
return redirect()->back()->with('success', __('Employee salary status updated successfully'));
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee salary status'));
|
|
}
|
|
} else {
|
|
return redirect()->back()->with('error', __('Employee salary Not Found.'));
|
|
}
|
|
}
|
|
|
|
public function showPayroll($employeeSalaryId)
|
|
{
|
|
if (Auth::user()->can('manage-employee-salaries')) {
|
|
try {
|
|
// $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId)
|
|
// ->whereIn('created_by', getCompanyAndUsersId())
|
|
// ->with('employee')
|
|
// ->first();
|
|
|
|
$employeeSalary = EmployeeSalary::with(['employee'])
|
|
->where('id',$employeeSalaryId)
|
|
->where(function ($q) {
|
|
if (Auth::user()->can('manage-any-employee-salaries')) {
|
|
$q->whereIn('created_by', getCompanyAndUsersId());
|
|
} elseif (Auth::user()->can('manage-own-employee-salaries')) {
|
|
$q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->where('is_active', 1);
|
|
} else {
|
|
$q->whereRaw('1 = 0');
|
|
}
|
|
})->first();
|
|
|
|
if (!$employeeSalary) {
|
|
return redirect()->route('hr.employee-salaries.index')
|
|
->with('error', __('Employee salary record not found.'));
|
|
}
|
|
|
|
// Get payroll runs for this employee
|
|
$payrollRuns = \App\Models\PayrollRun::whereIn('created_by', getCompanyAndUsersId())
|
|
->whereHas('payrollEntries', function ($query) use ($employeeSalary) {
|
|
$query->where('employee_id', $employeeSalary->employee_id);
|
|
})
|
|
->orderBy('pay_period_end', 'desc')
|
|
->get(['id', 'title', 'pay_period_start', 'pay_period_end', 'status']);
|
|
|
|
if ($payrollRuns->isEmpty()) {
|
|
return redirect()->route('hr.employee-salaries.index')
|
|
->with('error', __('No payroll runs found for this employee.'));
|
|
}
|
|
|
|
// Get the latest payroll run
|
|
$latestPayrollRun = $payrollRuns->first();
|
|
|
|
return Inertia::render('hr/employee-salaries/payroll-calculation', [
|
|
'employeeSalary' => $employeeSalary,
|
|
'payrollRuns' => $payrollRuns,
|
|
'selectedPayrollRun' => $latestPayrollRun,
|
|
'payrollData' => $this->getPayrollCalculationData($employeeSalary, $latestPayrollRun)
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return redirect()->route('hr.employee-salaries.index')
|
|
->with('error', __('Failed to load payroll calculation.'));
|
|
}
|
|
} else {
|
|
return redirect()->back()->with('error', __('Permission Denied.'));
|
|
}
|
|
}
|
|
|
|
public function getPayrollCalculation($employeeSalaryId, $payrollRunId)
|
|
{
|
|
try {
|
|
$employeeSalary = EmployeeSalary::where('id', $employeeSalaryId)
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->with('employee')
|
|
->first();
|
|
|
|
$payrollRun = \App\Models\PayrollRun::where('id', $payrollRunId)
|
|
->whereIn('created_by', getCompanyAndUsersId())
|
|
->first();
|
|
|
|
if (!$employeeSalary || !$payrollRun) {
|
|
return response()->json(['error' => 'Record not found'], 404);
|
|
}
|
|
|
|
$payrollData = $this->getPayrollCalculationData($employeeSalary, $payrollRun);
|
|
|
|
return response()->json($payrollData);
|
|
} catch (\Exception $e) {
|
|
return response()->json(['error' => 'Failed to calculate payroll'], 500);
|
|
}
|
|
}
|
|
|
|
private function getPayrollCalculationData($employeeSalary, $payrollRun)
|
|
{
|
|
// Get payroll entry for this employee and payroll run
|
|
$payrollEntry = \App\Models\PayrollEntry::where('employee_id', $employeeSalary->employee_id)
|
|
->where('payroll_run_id', $payrollRun->id)
|
|
->first();
|
|
|
|
if (!$payrollEntry) {
|
|
return [
|
|
'payrollEntry' => null,
|
|
'salaryBreakdown' => ['earnings' => [], 'deductions' => []],
|
|
'attendanceSummary' => [],
|
|
'payrollCalculation' => ['net_salary' => 0, 'total_earnings' => 0, 'total_deductions' => 0],
|
|
'attendanceRecords' => []
|
|
];
|
|
}
|
|
|
|
// Get attendance records for the payroll period
|
|
$attendanceRecords = \App\Models\AttendanceRecord::where('employee_id', $employeeSalary->employee_id)
|
|
->whereBetween('date', [$payrollRun->pay_period_start, $payrollRun->pay_period_end])
|
|
->orderBy('date')
|
|
->get();
|
|
|
|
// Calculate attendance summary from payroll entry
|
|
$attendanceSummary = [
|
|
'total_working_days' => $payrollEntry->working_days,
|
|
'present_days' => $payrollEntry->present_days,
|
|
'absent_days' => $payrollEntry->absent_days,
|
|
'half_days' => $payrollEntry->half_days,
|
|
'leave_days' => $payrollEntry->paid_leave_days,
|
|
'holiday_days' => $payrollEntry->holiday_days,
|
|
'overtime_hours' => $payrollEntry->overtime_hours,
|
|
'unpaid_leave_days' => $payrollEntry->unpaid_leave_days,
|
|
'unpaid_leave_from_leave' => $payrollEntry->unpaid_leave_days - $payrollEntry->absent_days - ($payrollEntry->half_days * 0.5)
|
|
];
|
|
|
|
// Get salary breakdown from payroll entry
|
|
$salaryBreakdown = [
|
|
'earnings' => is_array($payrollEntry->earnings_breakdown) ? $payrollEntry->earnings_breakdown : json_decode($payrollEntry->earnings_breakdown ?? '{}', true),
|
|
'deductions' => is_array($payrollEntry->deductions_breakdown) ? $payrollEntry->deductions_breakdown : json_decode($payrollEntry->deductions_breakdown ?? '{}', true)
|
|
];
|
|
|
|
$payrollCalculation = [
|
|
'net_salary' => $payrollEntry->net_pay,
|
|
'total_earnings' => $payrollEntry->total_earnings,
|
|
'total_deductions' => $payrollEntry->total_deductions,
|
|
'per_day_salary' => $payrollEntry->per_day_salary ?? 0,
|
|
'overtime_amount' => $payrollEntry->overtime_amount ?? 0
|
|
];
|
|
|
|
return [
|
|
'payrollEntry' => $payrollEntry,
|
|
'salaryBreakdown' => $salaryBreakdown,
|
|
'attendanceSummary' => $attendanceSummary,
|
|
'payrollCalculation' => $payrollCalculation,
|
|
'attendanceRecords' => $attendanceRecords,
|
|
'currentMonth' => $payrollRun->pay_period_end
|
|
];
|
|
}
|
|
|
|
private function calculateAttendanceSummary($attendanceRecords, $payrollRun)
|
|
{
|
|
$summary = [
|
|
'total_working_days' => 0,
|
|
'present_days' => 0,
|
|
'absent_days' => 0,
|
|
'half_days' => 0,
|
|
'leave_days' => 0,
|
|
'holiday_days' => 0,
|
|
'overtime_hours' => 0,
|
|
'unpaid_leave_days' => 0,
|
|
'unpaid_leave_from_leave' => 0
|
|
];
|
|
|
|
foreach ($attendanceRecords as $record) {
|
|
switch ($record->status) {
|
|
case 'present':
|
|
$summary['present_days']++;
|
|
break;
|
|
case 'absent':
|
|
$summary['absent_days']++;
|
|
break;
|
|
case 'half_day':
|
|
$summary['half_days']++;
|
|
break;
|
|
case 'on_leave':
|
|
$summary['leave_days']++;
|
|
break;
|
|
case 'holiday':
|
|
$summary['holiday_days']++;
|
|
break;
|
|
}
|
|
|
|
if ($record->overtime_hours > 0) {
|
|
$summary['overtime_hours'] += $record->overtime_hours;
|
|
}
|
|
}
|
|
|
|
// Calculate total working days (excluding holidays)
|
|
$summary['total_working_days'] = $summary['present_days'] + $summary['absent_days'] + $summary['half_days'] + $summary['leave_days'];
|
|
|
|
// Calculate unpaid leave days
|
|
$summary['unpaid_leave_days'] = $summary['absent_days'] + ($summary['half_days'] * 0.5);
|
|
|
|
return $summary;
|
|
}
|
|
}
|