isMethod('GET') && (!$request->has('view') || $request->input('view') !== 'list')) { if ($request->routeIs('hr.attendance-records.index')) { return redirect()->route('hr.attendance-records.calendar', $request->all()); } } if (Auth::user()->can('manage-attendance-records')) { $query = AttendanceRecord::with(['employee', 'shift', 'attendancePolicy', 'creator']) ->where(function ($q) { if (Auth::user()->can('manage-any-attendance-records')) { $q->whereIn('created_by', getCompanyAndUsersId()); } elseif (Auth::user()->can('manage-own-attendance-records')) { $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); } 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 status filter if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { $query->where('status', $request->status); } // Handle date range filter if ($request->has('date_from') && ! empty($request->date_from)) { $query->where('date', '>=', $request->date_from); } if ($request->has('date_to') && ! empty($request->date_to)) { $query->where('date', '<=', $request->date_to); } // Handle sorting if ($request->has('sort_field') && ! empty($request->sort_field)) { $sortField = $request->sort_field; $sortDirection = $request->sort_direction ?? 'asc'; if ($sortField === 'date') { $query->orderBy('date', $sortDirection); } else { $query->orderBy('id', 'desc'); } } else { $query->orderBy('date', 'desc'); } $attendanceRecords = $query->paginate($request->per_page ?? 9); $attendanceRecords->getCollection()->transform(function ($record) { if ($record->employee) { $rawAvatar = $record->employee->getRawOriginal('avatar'); $record->employee->avatar = check_file($rawAvatar) ? get_file($rawAvatar) : asset('storage/avatars/avatar.png'); } // Add leave type information for on_leave records if ($record->status === 'on_leave') { $leaveApplication = LeaveApplication::where('employee_id', $record->employee_id) ->whereDate('start_date', '<=', $record->date) ->whereDate('end_date', '>=', $record->date) ->where('status', 'approved') ->with('leaveType') ->first(); $record->leave_type = $leaveApplication?->leaveType; } return $record; }); // Get employees for filter dropdown $employees = User::where('type', 'employee') ->whereIn('created_by', getCompanyAndUsersId()) ->get(['id', 'name']); $companyUserIds = getCompanyAndUsersId(); if (isDemo()) { $statsRecords = AttendanceRecord::whereIn('created_by', $companyUserIds)->get(); $todayStats = [ 'present' => $statsRecords->where('status', 'present')->count(), 'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) { $q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds); })->where('status', 'approved')->count(), 'late_arrivals' => $statsRecords->where('is_late', true)->count(), 'overtime' => $statsRecords->where('overtime_hours', '>', 0)->count(), ]; } else { $today = Carbon::today(); $todayRecords = AttendanceRecord::whereIn('created_by', $companyUserIds) ->whereDate('date', $today) ->get(); $todayStats = [ 'present' => $todayRecords->where('status', 'present')->count(), 'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) { $q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds); })->where('status', 'approved')->whereDate('start_date', '<=', $today)->whereDate('end_date', '>=', $today)->count(), 'late_arrivals' => $todayRecords->where('is_late', true)->count(), 'overtime' => $todayRecords->where('overtime_hours', '>', 0)->count(), ]; } return Inertia::render('hr/attendance-records/index', [ 'attendanceRecords' => $attendanceRecords, 'employees' => $this->getFilteredEmployees(), 'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-attendance-record.xlsx')), 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), 'todayStats' => $todayStats, 'leaveTypes' => \App\Models\LeaveType::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->get(['id', 'name']), ]); } 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-attendance-records') && ! Auth::user()->can('manage-any-attendance-records')) { $employeeQuery->where(function ($q) { $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); }); } $employees = User::emp() ->with(['employee.shift']) ->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 ?? '', 'shift' => $user->employee->shift ? [ 'start_time' => $user->employee->shift->start_time, 'end_time' => $user->employee->shift->end_time, ] : null, ]; }); return $employees; } public function store(Request $request) { $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', 'date' => 'required|date', 'clock_in' => 'required_if:status,present,half_day', 'clock_out' => 'required_if:status,present,half_day', 'status' => 'required|in:present,absent,half_day,on_leave,holiday', 'is_holiday' => 'boolean', 'notes' => 'nullable|string', ]); // Get employee with shift and policy $employee = Employee::where('user_id', $validated['employee_id'])->first(); if (!$employee || !$employee->shift_id) { return redirect()->back()->with('error', __('Cannot process attendance: Employee has no shift assigned.')); } // Working days check removed here to allow admins/HR to create // records on weekends (e.g. for overtime or special absent cases). $dateIndex = Carbon::parse($validated['date'])->dayOfWeek; // Check if employee has approved leave for this date $approvedLeave = LeaveApplication::where('employee_id', $validated['employee_id']) ->where('status', 'approved') ->whereDate('start_date', '<=', $validated['date']) ->whereDate('end_date', '>=', $validated['date']) ->first(); // If a formal leave exists and user is trying to mark it as anything BUT on_leave, // we might want to warn or prevent. But if they ARE marking on_leave, link it. if ($approvedLeave && $validated['status'] === 'on_leave') { $validated['leave_application_id'] = $approvedLeave->id; $validated['leave_type_id'] = $approvedLeave->leave_type_id; } // Handle case where admin MANUALLY sets on_leave and expects deduction if ($validated['status'] === 'on_leave' && isset($request->leave_type_id)) { $leaveLimit = $this->checkAndDeductLeaveBalance($validated['employee_id'], $request->leave_type_id, $validated['date']); if (!$leaveLimit['success']) { return redirect()->back()->with('error', $leaveLimit['message']); } $validated['leave_type_id'] = $request->leave_type_id; // Create a formal leave application record for tracking $validated['leave_application_id'] = $this->createLeaveApplicationFromAttendance( $validated['employee_id'], $request->leave_type_id, $validated['date'], $validated['notes'] ?? null ); // Notes will store the deduction info $validated['notes'] = ($validated['notes'] ?? '') . " [Auto-deducted Leave: " . ($leaveLimit['type'] ?? 'Leave') . "]"; } // Check if record already exists $exists = AttendanceRecord::where('employee_id', $validated['employee_id']) ->where('date', $validated['date']) ->whereIn('created_by', getCompanyAndUsersId()) ->exists(); if ($exists) { return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.')); } // Use employee's assigned shift and policy, or get defaults $shift = $employee && $employee->shift_id ? Shift::find($employee->shift_id) : Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); $policy = $employee && $employee->attendance_policy_id ? AttendancePolicy::find($employee->attendance_policy_id) : AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); $validated['shift_id'] = $shift?->id; $validated['attendance_policy_id'] = $policy?->id; $validated['created_by'] = creatorId(); $validated['is_holiday'] = $validated['is_holiday'] ?? false; $validated['break_hours'] = $validated['break_hours'] ?? 0; // Set weekend flag $validated['is_weekend'] = Carbon::parse($validated['date'])->isWeekend(); $record = AttendanceRecord::create($validated); // Process complete attendance calculation but keep manual status $record->fresh(); // Reload to get relationships $record->processAttendance(false); return redirect()->back()->with('success', __('Attendance record created successfully.')); } public function update(Request $request, $attendanceRecordId) { $attendanceRecord = AttendanceRecord::where('id', $attendanceRecordId) ->whereIn('created_by', getCompanyAndUsersId()) ->first(); // For updates, we allow HR to correct any existing records regardless // of whether it falls on a working day or not. This is crucial for correcting // bad automated data or entering exceptional overtime/absences. $dateIndex = Carbon::parse($request->date)->dayOfWeek; // Check if employee has approved leave for this date $approvedLeave = LeaveApplication::where('employee_id', $request->employee_id) ->where('status', 'approved') ->whereDate('start_date', '<=', $request->date) ->whereDate('end_date', '>=', $request->date) ->first(); if ($approvedLeave && $request->status !== 'on_leave') { // If they are on approved leave but HR is marking them as something else, we let it happen // but the Leave Application remains the primary document. } if ($attendanceRecord) { try { $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', 'date' => 'required|date', 'clock_in' => 'required_if:status,present,half_day', 'clock_out' => 'required_if:status,present,half_day', 'break_hours' => 'nullable|numeric|min:0', 'is_holiday' => 'boolean', 'status' => 'required|in:present,absent,half_day,on_leave,holiday', 'leave_type_id' => 'required_if:status,on_leave|exists:leave_types,id', 'notes' => 'nullable|string', ]); // Check if employee or date changed and if duplicate exists if ($attendanceRecord->employee_id != $validated['employee_id'] || $attendanceRecord->date != $validated['date']) { $exists = AttendanceRecord::where('employee_id', $validated['employee_id']) ->where('date', $validated['date']) ->where('id', '!=', $attendanceRecordId) ->exists(); if ($exists) { return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.')); } } // Get employee with shift and policy $employee = \App\Models\Employee::where('user_id', $validated['employee_id'])->first(); if (!$employee || !$employee->shift_id) { return redirect()->back()->with('error', __('Cannot process attendance: Employee has no shift assigned.')); } $companyUserIds = getCompanyAndUsersId(); // Use employee's assigned shift and policy $shift = Shift::find($employee->shift_id); $policy = $employee && $employee->attendance_policy_id ? AttendancePolicy::find($employee->attendance_policy_id) : AttendancePolicy::whereIn('created_by', $companyUserIds)->where('status', 'active')->first(); $validated['shift_id'] = $shift?->id; $validated['attendance_policy_id'] = $policy?->id; // Handle balance deduction/restoration if status changed if ($validated['status'] === 'on_leave' && ($attendanceRecord->status !== 'on_leave' || $attendanceRecord->leave_type_id != $validated['leave_type_id'])) { // Deduct for new leave $leaveLimit = $this->checkAndDeductLeaveBalance($validated['employee_id'], $validated['leave_type_id'], $validated['date']); if (!$leaveLimit['success']) { return redirect()->back()->with('error', $leaveLimit['message']); } // Cleanup old application if it was changed if ($attendanceRecord->leave_application_id) { $this->deleteLeaveApplicationFromAttendance($attendanceRecord->leave_application_id); } // Create new application $validated['leave_application_id'] = $this->createLeaveApplicationFromAttendance( $validated['employee_id'], $validated['leave_type_id'], $validated['date'], $validated['notes'] ?? null ); // If old status was on_leave, restore the old balance if ($attendanceRecord->status === 'on_leave' && $attendanceRecord->leave_type_id) { $this->checkAndRestoreLeaveBalance($attendanceRecord->employee_id, $attendanceRecord->leave_type_id, $attendanceRecord->date); } } elseif ($attendanceRecord->status === 'on_leave' && $validated['status'] !== 'on_leave') { // Restore balance since it's no longer on_leave $this->checkAndRestoreLeaveBalance($attendanceRecord->employee_id, $attendanceRecord->leave_type_id, $attendanceRecord->date); // Delete linked application if ($attendanceRecord->leave_application_id) { $this->deleteLeaveApplicationFromAttendance($attendanceRecord->leave_application_id); $validated['leave_application_id'] = null; } } $attendanceRecord->update($validated); // Reload necessary relationships if not already fresh $attendanceRecord->loadMissing(['shift', 'attendancePolicy']); $attendanceRecord->processAttendance(false); return redirect()->back()->with('success', __('Attendance record updated successfully')); } catch (\Exception $e) { return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update attendance record')); } } else { return redirect()->back()->with('error', __('Attendance record Not Found.')); } } public function destroy($attendanceRecordId) { $attendanceRecord = AttendanceRecord::where('id', $attendanceRecordId) ->whereIn('created_by', getCompanyAndUsersId()) ->first(); if ($attendanceRecord) { try { // If it was on_leave, restore the balance first if ($attendanceRecord->status === 'on_leave' && $attendanceRecord->leave_type_id) { $this->checkAndRestoreLeaveBalance($attendanceRecord->employee_id, $attendanceRecord->leave_type_id, $attendanceRecord->date); // Also delete linked leave application if ($attendanceRecord->leave_application_id) { $this->deleteLeaveApplicationFromAttendance($attendanceRecord->leave_application_id); } } $attendanceRecord->delete(); return redirect()->back()->with('success', __('Attendance record deleted successfully')); } catch (\Exception $e) { return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete attendance record')); } } else { return redirect()->back()->with('error', __('Attendance record Not Found.')); } } public function clockIn(Request $request) { if (Auth::user()->can('clock-in-out')) { try { $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', ]); $settings = settings(); if (! empty($settings['ipRestrictionEnabled']) && $settings['ipRestrictionEnabled'] == 1) { $loginUserIp = request()->ip(); $ip = IpRestriction::whereIn('created_by', getCompanyAndUsersId())->where('ip_address', $loginUserIp)->first(); if (empty($ip) || is_null($ip)) { return redirect()->back()->with('error', __('This IP Address Is Not Allowed For Clock In & Clock Out.')); } } $today = Carbon::today(); $now = Carbon::now(); // Get working days from settings $globalSettings = settings(); $workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[]', true); if (empty($workingDaysIndices)) { return redirect()->back()->with('error', __('Please configure working days first.')); } $dateIndex = Carbon::parse($today)->dayOfWeek; if (! in_array($dateIndex, $workingDaysIndices)) { return redirect()->back()->with('error', __('Cannot create attendance record for non-working day.')); } // Check if employee has approved leave for this date $hasApprovedLeave = LeaveApplication::where('employee_id', $validated['employee_id']) ->where('status', 'approved') ->whereDate('start_date', '<=', $today) ->whereDate('end_date', '>=', $today) ->exists(); if ($hasApprovedLeave) { return redirect()->back()->with('error', __('Employee has approved leave for this date. Cannot create attendance record.')); } // Check if already clocked in today $existingRecord = AttendanceRecord::where('employee_id', $validated['employee_id']) ->where('date', $today) ->first(); if ($existingRecord && $existingRecord->clock_in) { return redirect()->back()->with('error', __('Already clocked in today.')); } // Get employee with shift and policy $employee = \App\Models\Employee::where('user_id', $validated['employee_id'])->first(); if (! $employee) { return redirect()->back()->with('error', __('Employee profile not found.')); } // Use employee's assigned shift and policy, or get defaults $shift = $employee->shift_id ? Shift::find($employee->shift_id) : Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); $policy = $employee->attendance_policy_id ? AttendancePolicy::find($employee->attendance_policy_id) : AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); if (! $shift || ! $policy) { return redirect()->back()->with('error', __('No active shift or attendance policy found. Please contact HR.')); } if ($existingRecord) { $existingRecord->update([ 'clock_in' => $now->format('H:i:s'), 'shift_id' => $shift->id, 'attendance_policy_id' => $policy->id, 'status' => 'present', ]); $record = $existingRecord; } else { $record = AttendanceRecord::create([ 'employee_id' => $validated['employee_id'], 'date' => $today, 'clock_in' => $now->format('H:i:s'), 'shift_id' => $shift->id, 'attendance_policy_id' => $policy->id, 'is_weekend' => $today->isWeekend(), 'status' => 'present', 'created_by' => creatorId(), ]); } // Check for late arrival if methods exist if (method_exists($record, 'checkLateArrival')) { $record->checkLateArrival(); $record->save(); } return redirect()->back()->with('success', __('Clocked in successfully.')); } catch (\Exception $e) { \Log::error('Clock in failed: '.$e->getMessage()); return redirect()->back()->with('error', __('Failed to clock in. Please try again.')); } } else { return redirect()->back()->with('error', __('Permission Denied.')); } } public function clockOut(Request $request) { if (Auth::user()->can('clock-in-out')) { try { $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', ]); $today = Carbon::today(); $now = Carbon::now(); $record = AttendanceRecord::where('employee_id', $validated['employee_id']) ->where('date', $today) ->first(); if (! $record || ! $record->clock_in) { return redirect()->back()->with('error', __('Must clock in first.')); } if ($record->clock_out) { return redirect()->back()->with('error', __('Already clocked out today.')); } $record->update([ 'clock_out' => $now->format('H:i:s'), ]); // Process complete attendance calculation if method exists if (method_exists($record, 'processAttendance')) { $record->processAttendance(); } return redirect()->back()->with('success', __('Clocked out successfully.')); } catch (\Exception $e) { \Log::error('Clock out failed: '.$e->getMessage()); return redirect()->back()->with('error', __('Failed to clock out. Please try again.')); } } else { return redirect()->back()->with('error', __('Permission Denied.')); } } public function getTodayAttendance(Request $request) { $validated = $request->validate([ 'employee_id' => 'required|exists:users,id', ]); $today = Carbon::today(); $attendance = AttendanceRecord::where('employee_id', $validated['employee_id']) ->where('date', $today) ->first(); return Inertia::render('employee-dashboard', [ 'attendance' => $attendance, ]); } public function export() { if (Auth::user()->can('export-attendance-record')) { try { $attendanceRecords = AttendanceRecord::with(['employee', 'shift', 'attendancePolicy']) ->where(function ($q) { if (Auth::user()->can('manage-any-attendance-records')) { $q->whereIn('created_by', getCompanyAndUsersId()); } elseif (Auth::user()->can('manage-own-attendance-records')) { $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); } else { $q->whereRaw('1 = 0'); } })->orderBy('date', 'desc')->get(); $fileName = 'attendance_records_'.date('Y-m-d_His').'.csv'; $headers = [ 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', ]; $callback = function () use ($attendanceRecords) { $file = fopen('php://output', 'w'); fputcsv($file, [ 'Employee', 'Date', 'Shift', 'Attedance Policy', 'Clock In', 'Clock Out', 'Break Hours', 'Total Hours', 'Overtime Hours', 'Status', 'Is Late', 'Is Early Departure', 'Notes' ]); foreach ($attendanceRecords as $record) { fputcsv($file, [ $record->employee->name ?? '', $record->date ? date('Y-m-d', strtotime($record->date)) : '', $record->shift->name ?? '', $record->attendancePolicy->name ?? '', $record->clock_in ?? '', $record->clock_out ?? '', $record->break_hours ?? '', $record->total_hours ?? '', $record->overtime_hours ?? '', $record->status ?? '', $record->is_late ? 'Yes' : 'No', $record->is_early_departure ? 'Yes' : 'No', $record->notes ?? '' ]); } fclose($file); }; return response()->stream($callback, 200, $headers); } catch (\Exception $e) { return response()->json(['message' => __('Failed to export attendance records: :message', ['message' => $e->getMessage()])], 500); } } else { return response()->json(['message' => __('Permission Denied.')], 403); } } public function downloadTemplate() { $filePath = storage_path('uploads/sample/sample-attendance-record.xlsx'); if (! file_exists($filePath)) { return response()->json(['error' => __('Template file not available')], 404); } return response()->download($filePath, 'sample-attendance-record.xlsx'); } public function parseFile(Request $request) { if (Auth::user()->can('import-attendance-record')) { $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; $validator = Validator::make($request->all(), $rules); if ($validator->fails()) { return response()->json(['message' => $validator->getMessageBag()->first()]); } try { $file = $request->file('file'); $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); $worksheet = $spreadsheet->getActiveSheet(); $highestColumn = $worksheet->getHighestColumn(); $highestRow = $worksheet->getHighestRow(); $headers = []; for ($col = 'A'; $col <= $highestColumn; $col++) { $value = $worksheet->getCell($col.'1')->getValue(); if ($value) { $headers[] = (string) $value; } } $previewData = []; for ($row = 2; $row <= $highestRow; $row++) { $rowData = []; $colIndex = 0; for ($col = 'A'; $col <= $highestColumn; $col++) { if ($colIndex < count($headers)) { $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col.$row)->getValue(); } $colIndex++; } $previewData[] = $rowData; } return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); } catch (\Exception $e) { return response()->json(['message' => __('Failed to parse file: :error', ['error' => $e->getMessage()])]); } } else { return response()->json(['message' => __('Permission denied.')], 403); } } public function fileImport(Request $request) { if (Auth::user()->can('import-attendance-record')) { $rules = ['data' => 'required|array']; $validator = Validator::make($request->all(), $rules); if ($validator->fails()) { return redirect()->back()->with('error', $validator->getMessageBag()->first()); } try { $data = $request->data; $imported = 0; $skipped = 0; foreach ($data as $row) { try { if (empty($row['employee']) || empty($row['date'])) { $skipped++; continue; } $employee = User::where('name', $row['employee']) ->whereIn('created_by', getCompanyAndUsersId()) ->where('type', 'employee') ->first(); if (! $employee) { $skipped++; continue; } // Check if attendance record already exists for this employee and date $exists = AttendanceRecord::where('employee_id', $employee->id) ->whereDate('date', $row['date']) ->exists(); if ($exists) { $skipped++; continue; } // Get employee with shift and policy $employeeModel = Employee::where('user_id', $employee->id)->first(); $shift = $employeeModel && $employeeModel->shift_id ? Shift::find($employeeModel->shift_id) : Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); $policy = $employeeModel && $employeeModel->attendance_policy_id ? AttendancePolicy::find($employeeModel->attendance_policy_id) : AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); if (! $shift || ! $policy) { $skipped++; continue; } $record = AttendanceRecord::create([ 'employee_id' => $employee->id, 'date' => $row['date'], 'shift_id' => $shift->id, 'attendance_policy_id' => $policy->id, 'clock_in' => $row['clock_in'] ?? null, 'clock_out' => $row['clock_out'] ?? null, 'created_by' => creatorId(), ]); // Process attendance calculation if (method_exists($record, 'processAttendance')) { $record->processAttendance(); } $imported++; } catch (\Exception $e) { $skipped++; } } return redirect()->back()->with('success', __('Import completed: :added attendance records added, :skipped attendance records skipped', ['added' => $imported, 'skipped' => $skipped])); } catch (\Exception $e) { return redirect()->back()->with('error', __('Failed to import: :error', ['error' => $e->getMessage()])); } } else { return redirect()->back()->with('error', __('Permission denied.')); } } public function calendar(Request $request) { if (Auth::user()->can('manage-attendance-records')) { $month = $request->input('month', date('n')); $year = $request->input('year', date('Y')); $department_id = $request->input('department_id'); $search = $request->input('search'); $startDate = \Carbon\Carbon::createFromDate($year, $month, 1)->startOfMonth(); $endDate = \Carbon\Carbon::createFromDate($year, $month, 1)->endOfMonth(); // Setup dates header correctly (1 -> 31) $dates = []; for ($date = $startDate->copy(); $date->lte($endDate); $date->addDay()) { $dates[] = [ 'date' => $date->format('Y-m-d'), 'day' => $date->format('d'), 'day_name' => $date->format('D'), ]; } $query = \App\Models\User::with(['employee.department', 'employee.shift']) ->whereHas('employee', function($q) { $q->where('employee_status', 'active'); }); if ($department_id) { $query->whereHas('employee', function($q) use ($department_id) { $q->where('department_id', $department_id); }); } if ($search) { $query->where('name', 'like', "%{$search}%"); } $usersRaw = $query->get(); // Fetch daily records $dailyRecords = \Illuminate\Support\Facades\DB::table('attendance_records') ->leftJoin('shifts', 'attendance_records.shift_id', '=', 'shifts.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', 'shifts.is_night_shift' ) ->get() ->groupBy('user_id'); // Fetch approved/pending leaves separately to overlay $approvedLeaves = LeaveApplication::whereIn('status', ['approved', 'pending']) ->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')]) ->orWhere(function($sub) use ($startDate, $endDate) { $sub->where('start_date', '<=', $startDate->format('Y-m-d')) ->where('end_date', '>=', $endDate->format('Y-m-d')); }); }) ->get() ->groupBy('employee_id'); $users = $usersRaw->map(function($user) use ($dates, $dailyRecords, $approvedLeaves) { $grid = []; $userRecords = $dailyRecords->has($user->id) ? $dailyRecords[$user->id]->keyBy('date') : collect(); foreach ($dates as $d) { $dateKey = $d['date']; // Default matrix values based on time mechanics $isPast = \Carbon\Carbon::parse($dateKey)->isBefore(\Carbon\Carbon::today()); $isWeekend = ($d['day_name'] === 'Sat' || $d['day_name'] === 'Sun'); $status = $isPast ? '--' : ($isWeekend ? 'W' : '--'); $label = '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->status, ]; if ($record->is_rest_day) { $status = 'RD'; $label = 'Rest Day'; } else if ($record->is_holiday || $record->status === 'holiday') { $status = 'HD'; $label = 'Holiday'; } else if ($record->status === 'on_leave') { $status = 'LV'; $label = 'Leave'; } else if ($record->status === 'absent') { $status = 'A'; $label = 'Absent'; } else if ($record->overtime_hours > 0) { $status = 'OT'; $label = 'Overtime'; } else if ($record->is_late) { $status = 'L'; $label = 'Late'; } else if ($record->is_early_departure) { $status = 'E'; $label = 'Early Checkout'; } else if ($record->status === 'half_day') { $status = 'H'; $label = 'Half Day'; } else if ($record->status === 'present') { // Only show P if they actually clocked in/out, otherwise show A for data integrity if (!empty($record->clock_in) && !empty($record->clock_out)) { $status = 'P'; $label = 'Present'; } else if (!empty($record->clock_in)) { $status = 'P'; $label = 'Present (Clocked In Only)'; } else { $status = 'A'; $label = 'Marked Present (No times recorded)'; } } } // Dynamic Overlay: If no record but approved leave exists, force show LV if (!$recordDetail && $approvedLeaves->has($user->id)) { foreach ($approvedLeaves[$user->id] as $leave) { $curr = \Carbon\Carbon::parse($dateKey); if ($curr->gte($leave->start_date) && $curr->lte($leave->end_date)) { $statusLabel = $leave->status === 'approved' ? __('Approved') : __('Pending'); $status = 'LV'; $label = __('On Leave') . ' (' . ($leave->leaveType->name ?? '') . ') [' . $statusLabel . ']'; $recordDetail = [ 'status' => 'on_leave', 'leave_application_id' => $leave->id, 'is_approved_leave' => true, 'leave_status' => $leave->status ]; break; } } } $grid[$dateKey] = [ 'status' => $status, 'label' => $label, 'record' => $recordDetail ]; } return [ 'id' => $user->id, 'name' => $user->name, 'employee_code' => $user->employee->employee_code ?? '', 'designation' => substr($user->employee->designation->name ?? 'Staff', 0, 15), 'department' => $user->employee->department->name ?? 'General', 'shift' => $user->employee->shift ? [ 'start_time' => $user->employee->shift->start_time, 'end_time' => $user->employee->shift->end_time, ] : null, 'records' => $grid ]; }); // Get departments for filter $departments = \App\Models\Department::select('id', 'name')->get(); return Inertia::render('hr/attendance-records/calendar', [ 'users' => $users, 'dates' => $dates, 'departments' => $departments, 'leaveTypes' => \App\Models\LeaveType::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->get(['id', 'name']), 'filters' => request()->all(['month', 'year', 'department_id', 'search']), ]); } else { return redirect()->back()->with('error', __('Permission Denied.')); } } /** * Check if employee has balance and deduct 1 day. */ private function checkAndDeductLeaveBalance($employeeId, $leaveTypeId, $date) { $year = Carbon::parse($date)->year; // Find or create leave balance for this employee, leave type, and year // We'll try to find a policy first to get the default allocation $leavePolicy = \App\Models\LeavePolicy::where('leave_type_id', $leaveTypeId) ->where('status', 'active') ->first(); $balance = \App\Models\LeaveBalance::where('employee_id', $employeeId) ->where('leave_type_id', $leaveTypeId) ->where('year', $year) ->first(); if (!$balance) { // Create initial balance if doesn't exist, using policy defaults or fallback to 10 $balance = \App\Models\LeaveBalance::create([ 'employee_id' => $employeeId, 'leave_type_id' => $leaveTypeId, 'leave_policy_id' => $leavePolicy ? $leavePolicy->id : null, 'year' => $year, 'allocated_days' => $leavePolicy ? ($leavePolicy->max_days_per_year ?? 10) : 10, 'used_days' => 0, 'remaining_days' => $leavePolicy ? ($leavePolicy->max_days_per_year ?? 10) : 10, 'created_by' => Auth::id(), ]); } $leaveTypeObj = \App\Models\LeaveType::find($leaveTypeId); if ($balance->remaining_days < 1 && $leaveTypeObj && $leaveTypeObj->is_paid) { return ['success' => false, 'message' => __('Insufficient leave balance. Remaining: ' . $balance->remaining_days)]; } // Deduct balance $balance->used_days += 1; $balance->calculateRemainingDays(); $balance->save(); return ['success' => true, 'type' => $balance->leaveType->name ?? 'Leave']; } /** * Restore 1 day to leave balance. */ private function checkAndRestoreLeaveBalance($employeeId, $leaveTypeId, $date) { $year = Carbon::parse($date)->year; $balance = \App\Models\LeaveBalance::where('employee_id', $employeeId) ->where('leave_type_id', $leaveTypeId) ->where('year', $year) ->first(); if ($balance) { $balance->used_days = max(0, $balance->used_days - 1); $balance->calculateRemainingDays(); $balance->save(); return true; } return false; } /** * Create a 1-day approved leave application from attendance calendar entry. */ private function createLeaveApplicationFromAttendance($employeeId, $leaveTypeId, $date, $notes = null) { // Avoid duplicate applications for the same date/user/type $existing = LeaveApplication::where('employee_id', $employeeId) ->where('leave_type_id', $leaveTypeId) ->whereDate('start_date', $date) ->whereDate('end_date', $date) ->first(); if ($existing) return $existing->id; $leavePolicy = LeavePolicy::where('leave_type_id', $leaveTypeId) ->where('status', 'active') ->first(); $application = LeaveApplication::create([ 'employee_id' => $employeeId, 'leave_type_id' => $leaveTypeId, 'leave_policy_id' => $leavePolicy ? $leavePolicy->id : null, 'start_date' => $date, 'end_date' => $date, // Attendance grid is per day 'total_days' => 1, 'reason' => $notes ?? __('Manually encoded via Attendance Calendar'), 'status' => 'approved', 'created_by' => Auth::id(), 'applied_on' => now(), ]); return $application->id; } /** * Delete a leave application created from attendance. */ private function deleteLeaveApplicationFromAttendance($applicationId) { if (!$applicationId) return; $application = LeaveApplication::find($applicationId); if ($application) { $application->delete(); } } public function getLeaveBalance(Request $request) { $employeeId = $request->employee_id; $leaveTypeId = $request->leave_type_id; $year = $request->year ?? now()->year; $balance = \App\Models\LeaveBalance::where('employee_id', $employeeId) ->where('leave_type_id', $leaveTypeId) ->where('year', $year) ->first(); return response()->json([ 'balance' => $balance ? $balance->remaining_days : 0, 'allocated' => $balance ? $balance->allocated_days : 0 ]); } public function cancelLeave(Request $request) { $applicationId = $request->leave_application_id; $application = \App\Models\LeaveApplication::where('id', $applicationId) ->whereIn('created_by', getCompanyAndUsersId()) ->first(); if ($application) { try { // Restore balance $this->checkAndRestoreLeaveBalance($application->employee_id, $application->leave_type_id, $application->start_date); // Delete application $application->delete(); return redirect()->back()->with('success', __('Leave application cancelled and balance restored.')); } catch (\Exception $e) { return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to cancel leave.')); } } return redirect()->back()->with('error', __('Leave application not found.')); } }