'date', 'break_hours' => 'decimal:2', 'overtime_hours' => 'decimal:2', 'overtime_amount' => 'decimal:2', 'is_late' => 'boolean', 'late_hours' => 'decimal:2', 'is_early_departure' => 'boolean', 'early_hours' => 'decimal:2', 'is_absent' => 'boolean', 'is_holiday' => 'boolean', 'is_weekend' => 'boolean', 'is_rest_day' => 'boolean', 'night_diff_hours' => 'decimal:2', ]; /** * Get the employee. */ public function employee() { return $this->belongsTo(User::class, 'employee_id'); } /** * Get the shift. */ public function shift() { return $this->belongsTo(Shift::class); } /** * Get the attendance policy. */ public function attendancePolicy() { return $this->belongsTo(AttendancePolicy::class); } /** * Get the leave type associated with the record. */ public function leaveType() { return $this->belongsTo(LeaveType::class); } /** * Get the leave application associated with the record. */ public function leaveApplication() { return $this->belongsTo(LeaveApplication::class); } /** * Get the user who created the record. */ public function creator() { return $this->belongsTo(User::class, 'created_by'); } /** * Calculate total working hours. */ public function calculateTotalHours() { if ($this->clock_in && $this->clock_out) { $clockIn = Carbon::parse($this->clock_in); $clockOut = Carbon::parse($this->clock_out); // Handle next day clock out (night shifts) if ($clockOut->lt($clockIn)) { $clockOut->addDay(); } $totalMinutes = abs($clockOut->diffInMinutes($clockIn)); // Use shift's break times for accurate calculation $breakMinutes = 0; if ($this->shift && $this->shift->break_start_time && $this->shift->break_end_time) { $breakStart = Carbon::parse($this->shift->break_start_time); $breakEnd = Carbon::parse($this->shift->break_end_time); // Handle next day break times for night shifts if ($breakEnd->lt($breakStart)) { $breakEnd->addDay(); } // Only deduct break if employee worked through the break period if ($clockIn->lte($breakStart) && $clockOut->gte($breakEnd)) { // Worked through entire break - deduct full break $breakMinutes = $this->shift->break_duration; } elseif ($clockIn->lte($breakStart) && $clockOut->gt($breakStart) && $clockOut->lte($breakEnd)) { // Left during break - deduct time spent on break $breakMinutes = abs($clockOut->diffInMinutes($breakStart)); } elseif ($clockIn->gt($breakStart) && $clockIn->lt($breakEnd) && $clockOut->gte($breakEnd)) { // Came during break - deduct partial break (missed part of break) $breakMinutes = abs($breakEnd->diffInMinutes($clockIn)); } elseif ($clockIn->gt($breakStart) && $clockOut->lt($breakEnd)) { // Came and left during break - no break deduction $breakMinutes = 0; } } $workingMinutes = max(0, $totalMinutes - $breakMinutes); $calculatedHours = round($workingMinutes / 60, 2); $this->attributes['total_hours'] = $calculatedHours; $this->attributes['break_hours'] = round($breakMinutes / 60, 2); } else { $this->attributes['total_hours'] = 0; $this->attributes['break_hours'] = 0; } return $this->attributes['total_hours'] ?? 0; } /** * Check if employee is late. */ public function checkLateArrival() { if ($this->shift && $this->clock_in && $this->attendancePolicy) { $expectedTime = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->shift->start_time); $actualTime = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->clock_in); $graceMinutes = $this->attendancePolicy->late_arrival_grace ?? 15; if ($actualTime->gt($expectedTime->copy()->addMinutes($graceMinutes))) { $this->is_late = true; $this->late_hours = round($actualTime->diffInMinutes($expectedTime) / 60, 2); } else { $this->is_late = false; $this->late_hours = 0; } } else { $this->is_late = false; $this->late_hours = 0; } return $this->is_late; } /** * Check if employee left early. */ public function checkEarlyDeparture() { if ($this->shift && $this->clock_out && $this->clock_in && $this->attendancePolicy) { $expectedTime = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->shift->end_time); $actualTime = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->clock_out); // Handle cross-midnight shift end if (\Carbon\Carbon::parse($this->shift->end_time)->lt(\Carbon\Carbon::parse($this->shift->start_time))) { $expectedTime->addDay(); } if ($actualTime->lt(\Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->clock_in))) { $actualTime->addDay(); } $graceMinutes = $this->attendancePolicy->early_departure_grace ?? 0; if ($actualTime->lt($expectedTime->copy()->subMinutes($graceMinutes))) { $this->is_early_departure = true; $this->early_hours = round($expectedTime->diffInMinutes($actualTime) / 60, 2); } else { $this->is_early_departure = false; $this->early_hours = 0; } } else { $this->is_early_departure = false; $this->early_hours = 0; } return $this->is_early_departure; } /** * Calculate Night Differential (10 PM to 6 AM). */ public function calculateNightDifferential() { if (!$this->clock_in || !$this->clock_out) { $this->night_diff_hours = 0; return 0; } $clockIn = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->clock_in); $clockOut = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->clock_out); if ($clockOut->lt($clockIn)) { $clockOut->addDay(); } $ndHours = 0; // ND interval 1: Day 1, 22:00 to Day 2, 06:00 $ndStart1 = $clockIn->copy()->setTime(22, 0, 0); $ndEnd1 = $ndStart1->copy()->addHours(8); // Day 2, 06:00 // ND interval 2: Day 0, 22:00 to Day 1, 06:00 $ndStart0 = $clockIn->copy()->subDay()->setTime(22, 0, 0); $ndEnd0 = $ndStart0->copy()->addHours(8); // Day 1, 06:00 $calculateOverlap = function($start1, $end1, $start2, $end2) { $latestStart = $start1->max($start2); $earliestEnd = $end1->min($end2); if ($latestStart->lt($earliestEnd)) { return $latestStart->diffInMinutes($earliestEnd) / 60; } return 0; }; $ndHours += $calculateOverlap($clockIn, $clockOut, $ndStart1, $ndEnd1); $ndHours += $calculateOverlap($clockIn, $clockOut, $ndStart0, $ndEnd0); // ND interval 3: Day 2, 22:00 to Day 3, 06:00 $ndStart2 = $clockIn->copy()->addDay()->setTime(22, 0, 0); $ndEnd2 = $ndStart2->copy()->addHours(8); $ndHours += $calculateOverlap($clockIn, $clockOut, $ndStart2, $ndEnd2); // Deduct break time if break overlaps with ND period if ($this->shift && $this->shift->break_start_time && $this->shift->break_end_time && $ndHours > 0) { $breakStart = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->shift->break_start_time); $breakEnd = \Carbon\Carbon::parse($this->date->format('Y-m-d') . ' ' . $this->shift->break_end_time); if ($breakEnd->lt($breakStart)) { $breakEnd->addDay(); } if ($breakStart->lt($clockIn->copy()->subHours(12))) { $breakStart->addDay(); $breakEnd->addDay(); } $breakND1 = $calculateOverlap($breakStart, $breakEnd, $ndStart1, $ndEnd1); $breakND0 = $calculateOverlap($breakStart, $breakEnd, $ndStart0, $ndEnd0); $ndHours -= ($breakND1 + $breakND0); $ndHours = max(0, $ndHours); } $this->night_diff_hours = round($ndHours, 2); return $this->night_diff_hours; } /** * Process complete attendance - calculate everything automatically. */ public function processAttendance($autoCalculateStatus = true) { // Step 1: Calculate total working hours first $this->calculateTotalHours(); // Step 2: Calculate overtime using shift working hours dynamically if ($this->shift && $this->shift->working_hours > 0) { $standardHours = $this->shift->working_hours; // Use actual shift hours } else { $standardHours = 8; // Fallback to 8 hours if no shift or invalid hours } $this->overtime_hours = max(0, round($this->total_hours - $standardHours, 2)); // Step 3: Calculate overtime amount using policy if ($this->overtime_hours > 0 && $this->attendancePolicy) { $this->overtime_amount = round($this->overtime_hours * $this->attendancePolicy->overtime_rate_per_hour, 2); } else { $this->overtime_amount = 0; } // Step 4: Check late arrival, early departure, and Night Differential if ($this->clock_in && $this->clock_out) { $this->checkLateArrival(); $this->checkEarlyDeparture(); $this->calculateNightDifferential(); } // Step 5: Set status based on holiday or total hours (only if not manually set) if ($autoCalculateStatus) { if ($this->is_holiday) { $this->status = 'holiday'; } elseif ($this->exists || $this->isDirty('clock_in') || $this->isDirty('clock_out')) { $fullDayThreshold = $standardHours; // e.g. 8 hours $halfDayThreshold = $standardHours / 2; // e.g. 4 hours if ($this->total_hours >= $fullDayThreshold) { $this->status = 'present'; } elseif ($this->total_hours >= $halfDayThreshold) { $this->status = 'half_day'; } elseif ($this->total_hours > 0) { $this->status = 'absent'; // or mark as short_leave if needed } else { $this->status = 'absent'; } } } // If record exists and times haven't changed, keep manual status if ($this->isDirty()) { $this->save(); } } /** * Format clock in time for frontend (H:i format). */ public function getClockInAttribute($value) { return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; } /** * Format clock out time for frontend (H:i format). */ public function getClockOutAttribute($value) { return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; } }