diff --git a/app/Http/Controllers/AttendanceRegularizationController.php b/app/Http/Controllers/AttendanceRegularizationController.php index 6c3830b7f..65698a76b 100644 --- a/app/Http/Controllers/AttendanceRegularizationController.php +++ b/app/Http/Controllers/AttendanceRegularizationController.php @@ -15,8 +15,14 @@ class AttendanceRegularizationController extends Controller public function index(Request $request) { if (Auth::user()->can('manage-attendance-regularizations')) { - $query = AttendanceRegularization::with(['employee', 'attendanceRecord', 'approver', 'creator'])->where(function ($q) { - if (Auth::user()->can('manage-any-attendance-regularizations')) { + $scopedBranchId = authBranchId(); + $query = AttendanceRegularization::with(['employee', 'attendanceRecord', 'approver', 'creator'])->where(function ($q) use ($scopedBranchId) { + if ($scopedBranchId) { + $q->whereIn('created_by', getCompanyAndUsersId()) + ->whereHas('employee.employee', function ($eq) use ($scopedBranchId) { + $eq->where('branch_id', $scopedBranchId); + }); + } elseif (Auth::user()->can('manage-any-attendance-regularizations')) { $q->whereIn('created_by', getCompanyAndUsersId()); } elseif (Auth::user()->can('manage-own-attendance-regularizations')) { $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); @@ -80,21 +86,29 @@ class AttendanceRegularizationController extends Controller return $record; }); - $employees = User::where('type', 'employee') - ->whereIn('created_by', getCompanyAndUsersId()) - ->get(['id', 'name']); - // Get attendance records for form dropdown - $attendanceRecords = AttendanceRecord::whereIn('created_by', getCompanyAndUsersId()) + $attQuery = AttendanceRecord::whereIn('created_by', getCompanyAndUsersId()) ->with('employee') - ->orderBy('date', 'desc') - ->take(50) - ->get(); + ->orderBy('date', 'desc'); + if ($scopedBranchId) { + $attQuery->where(function($aq) use ($scopedBranchId) { + $aq->where('branch_id', $scopedBranchId) + ->orWhereHas('employee', function($eq) use ($scopedBranchId) { + $eq->where('branch_id', $scopedBranchId); + }); + }); + } + $attendanceRecords = $attQuery->take(50)->get(); $companyUserIds = getCompanyAndUsersId(); - $statsQuery = AttendanceRegularization::where(function ($q) { - if (Auth::user()->can('manage-any-attendance-regularizations')) { + $statsQuery = AttendanceRegularization::where(function ($q) use ($scopedBranchId) { + if ($scopedBranchId) { + $q->whereIn('created_by', getCompanyAndUsersId()) + ->whereHas('employee.employee', function ($eq) use ($scopedBranchId) { + $eq->where('branch_id', $scopedBranchId); + }); + } elseif (Auth::user()->can('manage-any-attendance-regularizations')) { $q->whereIn('created_by', getCompanyAndUsersId()); } elseif (Auth::user()->can('manage-own-attendance-regularizations')) { $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); @@ -125,6 +139,10 @@ class AttendanceRegularizationController extends Controller { // Get employees for filter dropdown (compatible with getFilteredEmployees logic) $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + $scopedBranchId = authBranchId(); + if ($scopedBranchId) { + $employeeQuery->where('branch_id', $scopedBranchId); + } if (Auth::user()->can('manage-own-attendance-regularizations') && !Auth::user()->can('manage-any-attendance-regularizations')) { $employeeQuery->where(function ($q) { diff --git a/app/Http/Controllers/OvertimeApplicationController.php b/app/Http/Controllers/OvertimeApplicationController.php index 50a8561e8..2740a0d70 100644 --- a/app/Http/Controllers/OvertimeApplicationController.php +++ b/app/Http/Controllers/OvertimeApplicationController.php @@ -24,6 +24,14 @@ class OvertimeApplicationController extends Controller if (!in_array(Auth::user()->type, ['admin', 'hr', 'company'])) { abort(403); } + + $scopedBranchId = authBranchId(); + if ($scopedBranchId) { + $targetEmp = \App\Models\Employee::where('user_id', $userId)->first(); + if ($targetEmp && (int)$targetEmp->branch_id !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to view attendance details for an employee outside your assigned branch.'); + } + } } $record = AttendanceRecord::with('employee.user', 'employee.shift') @@ -85,19 +93,30 @@ class OvertimeApplicationController extends Controller public function index() { $user = Auth::user(); + $scopedBranchId = authBranchId(); $query = OvertimeApplication::with('user.employee'); // If not admin/hr, only show own requests if (!in_array($user->type, ['admin', 'hr', 'company'])) { $query->where('user_id', $user->id); + } elseif ($scopedBranchId) { + $query->whereHas('user.employee', function($q) use ($scopedBranchId) { + $q->where('branch_id', $scopedBranchId); + }); } $applications = $query->orderBy('date', 'desc')->get(); $users = []; if (in_array($user->type, ['admin', 'hr', 'company'])) { - $users = \App\Models\User::where('type', 'employee')->select('id', 'name')->get(); + $usersQuery = \App\Models\User::where('type', 'employee'); + if ($scopedBranchId) { + $usersQuery->whereHas('employee', function($q) use ($scopedBranchId) { + $q->where('branch_id', $scopedBranchId); + }); + } + $users = $usersQuery->select('id', 'name')->get(); } return Inertia::render('hr/overtime/index', [ @@ -117,10 +136,17 @@ class OvertimeApplicationController extends Controller ]); $userId = Auth::id(); + $scopedBranchId = authBranchId(); // If an admin/hr specifies a user_id, use it. if ($request->has('user_id') && $request->user_id != Auth::id()) { if (in_array(Auth::user()->type, ['admin', 'hr', 'company'])) { + if ($scopedBranchId) { + $targetEmp = \App\Models\Employee::where('user_id', $request->user_id)->first(); + if ($targetEmp && (int)$targetEmp->branch_id !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to apply overtime for an employee outside your assigned branch.'); + } + } $userId = $request->user_id; } else { abort(403, 'Unauthorized to apply overtime for another employee.'); @@ -160,6 +186,14 @@ class OvertimeApplicationController extends Controller abort(403); } + $scopedBranchId = authBranchId(); + if ($scopedBranchId) { + $applicantEmp = $overtimeApplication->user?->employee; + if ($applicantEmp && (int)$applicantEmp->branch_id !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to update overtime status for an employee outside your assigned branch.'); + } + } + $request->validate([ 'status' => 'required|in:approved,rejected', 'approved_hours' => 'nullable|required_if:status,approved|numeric|min:0', diff --git a/app/Http/Controllers/ThirteenthMonthController.php b/app/Http/Controllers/ThirteenthMonthController.php index 46a3abe39..86907ac2a 100644 --- a/app/Http/Controllers/ThirteenthMonthController.php +++ b/app/Http/Controllers/ThirteenthMonthController.php @@ -31,11 +31,16 @@ class ThirteenthMonthController extends Controller $endYear = $request->input('end_year') ? (int) $request->input('end_year') : $currentYear; $endMonth = $request->input('end_month') ? (int) $request->input('end_month') : 12; - $selectedBranchId = $request->input('branch_id') ? (int) $request->input('branch_id') : null; + $scopedBranchId = authBranchId(); + $selectedBranchId = $scopedBranchId ?: ($request->input('branch_id') ? (int) $request->input('branch_id') : null); // Available years for back-year filtering (last 5 years up to next year) $availableYears = range($currentYear - 5, $currentYear + 1); - $branches = Branch::all(['id', 'name']); + $branchesQuery = Branch::query(); + if ($scopedBranchId) { + $branchesQuery->where('id', $scopedBranchId); + } + $branches = $branchesQuery->get(['id', 'name']); // Check if an existing run matches these range parameters $existingRun = ThirteenthMonthRun::where('start_year', $startYear) @@ -56,10 +61,15 @@ class ThirteenthMonthController extends Controller } // Fetch historical runs list - $pastRuns = ThirteenthMonthRun::with(['branch', 'creator']) + $pastRunsQuery = ThirteenthMonthRun::with(['branch', 'creator']) ->orderBy('year', 'desc') - ->orderBy('created_at', 'desc') - ->get(); + ->orderBy('created_at', 'desc'); + + if ($scopedBranchId) { + $pastRunsQuery->where('branch_id', $scopedBranchId); + } + + $pastRuns = $pastRunsQuery->get(); return Inertia::render('hr/thirteenth-month/index', [ 'activeRun' => $existingRun, @@ -93,7 +103,9 @@ class ThirteenthMonthController extends Controller $startMonth = (int) $request->start_month; $endYear = (int) $request->end_year; $endMonth = (int) $request->end_month; - $branchId = $request->branch_id ? (int) $request->branch_id : null; + + $scopedBranchId = authBranchId(); + $branchId = $scopedBranchId ?: ($request->branch_id ? (int) $request->branch_id : null); $run = $this->service->generateRun($startYear, $startMonth, $endYear, $endMonth, $branchId, null, Auth::id()); @@ -111,7 +123,14 @@ class ThirteenthMonthController extends Controller 'notes' => 'nullable|string|max:255', ]); - $entry = ThirteenthMonthEntry::findOrFail($id); + $entry = ThirteenthMonthEntry::with(['employee', 'run'])->findOrFail($id); + $scopedBranchId = authBranchId(); + if ($scopedBranchId) { + $empBranchId = $entry->employee?->branch_id ?? $entry->run?->branch_id; + if ($empBranchId && (int)$empBranchId !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to update 13th Month entry for an employee outside your assigned branch.'); + } + } if ($request->has('adjustment_amount')) { $entry->adjustment_amount = (float) $request->adjustment_amount; @@ -152,6 +171,11 @@ class ThirteenthMonthController extends Controller public function approveRun(Request $request, $id) { $run = ThirteenthMonthRun::findOrFail($id); + $scopedBranchId = authBranchId(); + if ($scopedBranchId && $run->branch_id && (int)$run->branch_id !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to approve 13th Month run for another branch.'); + } + $run->status = 'approved'; $run->save(); @@ -164,6 +188,10 @@ class ThirteenthMonthController extends Controller public function exportCsv($id) { $run = ThirteenthMonthRun::with(['entries.employee.user', 'entries.employee.department', 'branch'])->findOrFail($id); + $scopedBranchId = authBranchId(); + if ($scopedBranchId && $run->branch_id && (int)$run->branch_id !== (int)$scopedBranchId) { + abort(403, 'Unauthorized to export 13th Month run for another branch.'); + } $filename = "13th_Month_Pay_{$run->start_year}_{$run->start_month}_to_{$run->end_year}_{$run->end_month}_Export.csv"; $headers = [ diff --git a/tests/Feature/BranchScopedAccessTest.php b/tests/Feature/BranchScopedAccessTest.php index 7f65bf501..994b68880 100644 --- a/tests/Feature/BranchScopedAccessTest.php +++ b/tests/Feature/BranchScopedAccessTest.php @@ -10,6 +10,9 @@ use App\Models\Employee; use App\Models\PayrollRun; use App\Models\PayrollEntry; use App\Models\Payslip; +use App\Models\OvertimeApplication; +use App\Models\ThirteenthMonthRun; +use App\Models\ThirteenthMonthEntry; use Illuminate\Foundation\Testing\RefreshDatabase; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; @@ -30,6 +33,7 @@ class BranchScopedAccessTest extends TestCase Permission::firstOrCreate(['name' => 'manage-any-employees']); Permission::firstOrCreate(['name' => 'manage-payslips']); Permission::firstOrCreate(['name' => 'manage-any-payslips']); + Permission::firstOrCreate(['name' => 'manage-payroll-runs']); } public function test_auth_branch_id_helper_resolution() @@ -318,5 +322,152 @@ class BranchScopedAccessTest extends TestCase $response->assertSee('PAY-ALPHA-001'); $response->assertDontSee('PAY-BETA-002'); } + + public function test_branch_scoped_user_only_sees_assigned_branch_overtime_requests() + { + $company = User::create([ + 'name' => 'Company Admin', + 'email' => 'company_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'company', + ]); + + $branchA = Branch::create([ + 'name' => 'Branch Alpha', + 'created_by' => $company->id, + ]); + + $branchB = Branch::create([ + 'name' => 'Branch Beta', + 'created_by' => $company->id, + ]); + + // Employee in Branch A + $userA = User::create([ + 'name' => 'Employee Alpha User', + 'email' => 'alpha_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'employee', + 'created_by' => $company->id, + ]); + Employee::create([ + 'user_id' => $userA->id, + 'employee_id' => 'EMP-A-' . rand(100, 999), + 'branch_id' => $branchA->id, + 'created_by' => $company->id, + ]); + + // Employee in Branch B + $userB = User::create([ + 'name' => 'Employee Beta User', + 'email' => 'beta_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'employee', + 'created_by' => $company->id, + ]); + Employee::create([ + 'user_id' => $userB->id, + 'employee_id' => 'EMP-B-' . rand(100, 999), + 'branch_id' => $branchB->id, + 'created_by' => $company->id, + ]); + + // Overtime requests + OvertimeApplication::create([ + 'user_id' => $userA->id, + 'date' => '2026-05-10', + 'requested_hours' => 2.5, + 'reason' => 'Alpha Overtime Work', + 'status' => 'pending', + ]); + + OvertimeApplication::create([ + 'user_id' => $userB->id, + 'date' => '2026-05-10', + 'requested_hours' => 3.0, + 'reason' => 'Beta Overtime Work', + 'status' => 'pending', + ]); + + $hrUser = User::create([ + 'name' => 'HR Branch A User', + 'email' => 'hr_a_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'hr', + 'branch_id' => $branchA->id, + 'created_by' => $company->id, + ]); + + $response = $this->actingAs($hrUser)->get(route('hr.overtime.index')); + $response->assertStatus(200); + $response->assertSee('Alpha Overtime Work'); + $response->assertDontSee('Beta Overtime Work'); + } + + public function test_branch_scoped_user_only_sees_assigned_branch_thirteenth_month() + { + $company = User::create([ + 'name' => 'Company Admin', + 'email' => 'company_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'company', + ]); + + $branchA = Branch::create([ + 'name' => 'Branch Alpha', + 'created_by' => $company->id, + ]); + + $branchB = Branch::create([ + 'name' => 'Branch Beta', + 'created_by' => $company->id, + ]); + + // Employee in Branch A + $userA = User::create([ + 'name' => 'Employee Alpha User', + 'email' => 'alpha_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'employee', + 'created_by' => $company->id, + ]); + Employee::create([ + 'user_id' => $userA->id, + 'employee_id' => 'EMP-A-' . rand(100, 999), + 'branch_id' => $branchA->id, + 'created_by' => $company->id, + ]); + + // Employee in Branch B + $userB = User::create([ + 'name' => 'Employee Beta User', + 'email' => 'beta_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'employee', + 'created_by' => $company->id, + ]); + Employee::create([ + 'user_id' => $userB->id, + 'employee_id' => 'EMP-B-' . rand(100, 999), + 'branch_id' => $branchB->id, + 'created_by' => $company->id, + ]); + + $hrUser = User::create([ + 'name' => 'HR Branch A User', + 'email' => 'hr_a_' . uniqid() . '@test.com', + 'password' => bcrypt('password'), + 'type' => 'hr', + 'branch_id' => $branchA->id, + 'created_by' => $company->id, + ]); + $hrUser->givePermissionTo('manage-payroll-runs'); + + $response = $this->actingAs($hrUser)->get(route('hr.thirteenth-month.index')); + $response->assertStatus(200); + $response->assertSee('Employee Alpha User'); + $response->assertDontSee('Employee Beta User'); + } } +