fix: employee self-service - open controller access and clock-in/out

- LeaveApplicationController: add employee branch to bypass can() check
- AttendanceRecordController: add employee calendar view (own records only)
  + open clockIn/clockOut to employee type (no permission required)
- PayslipController: add employee branch to show own payslips only
- routes/web.php: remove permission:clock-in-out middleware from clock routes
- employee-dashboard.tsx: always show clock in/out buttons for employees
- Rebuild frontend assets
This commit is contained in:
2026-06-30 13:51:02 +08:00
parent c11ee26183
commit 75ee8cacb8
417 changed files with 14497 additions and 14275 deletions

View File

@@ -22,6 +22,14 @@ class AttendanceRecordController extends Controller
{
public function index(Request $request)
{
$user = Auth::user();
// Employee self-service: redirect to calendar but pass their own records only
if ($user->type === 'employee') {
// For employees, show only their own attendance in calendar view
return redirect()->route('hr.attendance-records.calendar', $request->all());
}
// Default to calendar view unless explicitly requesting list view
// Added check to prevent redirecting if already on a sub-path or if it's not a GET request
if ($request->isMethod('GET') && (!$request->has('view') || $request->input('view') !== 'list')) {
@@ -460,7 +468,8 @@ class AttendanceRecordController extends Controller
public function clockIn(Request $request)
{
if (Auth::user()->can('clock-in-out')) {
// Allow employees with the permission OR any employee type user
if (Auth::user()->can('clock-in-out') || Auth::user()->type === 'employee') {
try {
$validated = $request->validate([
'employee_id' => 'required|exists:users,id',
@@ -571,7 +580,8 @@ class AttendanceRecordController extends Controller
public function clockOut(Request $request)
{
if (Auth::user()->can('clock-in-out')) {
// Allow employees with the permission OR any employee type user
if (Auth::user()->can('clock-in-out') || Auth::user()->type === 'employee') {
try {
$validated = $request->validate([
'employee_id' => 'required|exists:users,id',
@@ -843,6 +853,157 @@ class AttendanceRecordController extends Controller
public function calendar(Request $request)
{
$user = Auth::user();
// Employee self-service: show only their own calendar
if ($user->type === 'employee') {
$month = $request->input('month', date('n'));
$year = $request->input('year', date('Y'));
$startDate = \Carbon\Carbon::createFromDate($year, $month, 1)->startOfMonth();
$endDate = \Carbon\Carbon::createFromDate($year, $month, 1)->endOfMonth();
$dates = [];
for ($d = $startDate->copy(); $d->lte($endDate); $d->addDay()) {
$dates[] = [
'date' => $d->format('Y-m-d'),
'day' => $d->format('d'),
'day_name' => $d->format('D'),
];
}
$dailyRecords = \Illuminate\Support\Facades\DB::table('attendance_records')
->leftJoin('shifts', 'attendance_records.shift_id', '=', 'shifts.id')
->where('attendance_records.employee_id', $user->id)
->whereBetween('attendance_records.date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->select(
'attendance_records.id',
'attendance_records.employee_id as user_id',
'attendance_records.date',
'attendance_records.clock_in',
'attendance_records.clock_out',
'attendance_records.notes',
'attendance_records.status',
'attendance_records.is_rest_day',
'attendance_records.is_holiday',
'attendance_records.is_late',
'attendance_records.is_early_departure',
'attendance_records.overtime_hours',
'attendance_records.shift_id',
'shifts.name as shift_name',
'shifts.start_time as shift_start_time',
'shifts.end_time as shift_end_time',
'shifts.grace_period as shift_grace_period',
'shifts.is_night_shift'
)
->get()
->groupBy('user_id');
$approvedLeaves = LeaveApplication::whereIn('status', ['approved', 'pending'])
->where('employee_id', $user->id)
->where(function($q) use ($startDate, $endDate) {
$q->whereBetween('start_date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->orWhereBetween('end_date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]);
})
->get()
->groupBy('employee_id');
$approvedOvertimes = \App\Models\OvertimeApplication::where('status', 'approved')
->where('user_id', $user->id)
->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->get()
->groupBy('user_id');
$holidays = \App\Models\Holiday::where(function($q) use ($startDate, $endDate) {
$q->whereBetween('start_date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->orWhereBetween('end_date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]);
})->get();
$globalSettings = settings();
$workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[1,2,3,4,5]', true);
// Build a single-user array using same mapping logic
$employeeModel = \App\Models\Employee::where('user_id', $user->id)->with(['shift', 'designation', 'department'])->first();
$singleUser = (object)[
'id' => $user->id,
'name' => $user->name,
'employee' => $employeeModel,
];
// Reuse the same calendar logic by passing through the existing pipeline for one user
$usersRaw = collect([$singleUser]);
$users = $usersRaw->map(function($u) use ($dates, $dailyRecords, $approvedLeaves, $approvedOvertimes, $workingDaysIndices, $holidays) {
$grid = [];
$userRecords = $dailyRecords->has($u->id) ? $dailyRecords[$u->id]->keyBy('date') : collect();
foreach ($dates as $d) {
$dateKey = $d['date'];
$dateObj = \Carbon\Carbon::parse($dateKey);
$dateIndex = $dateObj->dayOfWeek;
$isRestDay = !in_array($dateIndex, $workingDaysIndices);
$status = $isRestDay ? 'RD' : '--';
$label = $isRestDay ? 'Rest Day' : 'Not Marked';
$recordDetail = null;
if ($userRecords->has($dateKey)) {
$record = $userRecords[$dateKey];
$recordDetail = [
'id' => $record->id,
'clock_in' => $record->clock_in ? \Carbon\Carbon::parse($record->clock_in)->format('H:i') : null,
'clock_out' => $record->clock_out ? \Carbon\Carbon::parse($record->clock_out)->format('H:i') : null,
'notes' => $record->notes,
'status' => $record->is_rest_day ? 'rest_day' : $record->status,
'shift_name' => $record->shift_name ?? null,
'shift_start_time' => $record->shift_start_time ?? null,
'shift_end_time' => $record->shift_end_time ?? null,
];
$status = $record->is_late ? 'L' : ($record->clock_in ? 'P' : ($isRestDay ? 'RD' : 'A'));
$label = $record->is_late ? 'Late' : ($record->clock_in ? 'Present' : ($isRestDay ? 'Rest Day' : 'Absent'));
}
// Leave overlay
if ((!$recordDetail || empty($recordDetail['clock_in'])) && $approvedLeaves->has($u->id)) {
foreach ($approvedLeaves[$u->id] as $leave) {
if ($dateObj->gte($leave->start_date) && $dateObj->lte($leave->end_date)) {
$status = 'LV';
$label = 'On Leave (' . ($leave->leaveType->name ?? '') . ')';
$recordDetail = ['status' => 'on_leave', 'leave_application_id' => $leave->id];
break;
}
}
}
$grid[$dateKey] = ['status' => $status, 'label' => $label, 'record' => $recordDetail];
}
return [
'id' => $u->id,
'name' => $u->name,
'employee_code' => $u->employee->employee_code ?? '',
'designation' => substr($u->employee->designation->name ?? 'Staff', 0, 15),
'department' => $u->employee->department->name ?? 'General',
'shift' => $u->employee && $u->employee->shift ? [
'name' => $u->employee->shift->name,
'start_time' => $u->employee->shift->start_time,
'end_time' => $u->employee->shift->end_time,
] : null,
'records' => $grid,
];
});
return Inertia::render('hr/attendance-records/calendar', [
'users' => $users,
'dates' => $dates,
'departments'=> [],
'branches' => [],
'leaveTypes' => \App\Models\LeaveType::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->get(['id', 'name']),
'filters' => $request->all(['month', 'year']),
]);
}
if (Auth::user()->can('manage-attendance-records')) {
$month = $request->input('month', date('n'));
$year = $request->input('year', date('Y'));