Files
HRM-System/app/Services/AttendanceService.php

169 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\AttendanceRecord;
use App\Models\Branch;
use App\Models\Employee;
use App\Models\LeaveApplication;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Exception;
class AttendanceService
{
private function calculateHaversineDistance($lat1, $lon1, $lat2, $lon2)
{
if ($lat1 === null || $lon1 === null || $lat2 === null || $lon2 === null) {
return 0;
}
$earthRadius = 6371000; // in meters
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
public function getTodayRecord(Employee $employee): ?AttendanceRecord
{
return AttendanceRecord::where('employee_id', $employee->user_id)
->whereDate('date', date('Y-m-d'))
->first();
}
public function processClock(User $user, Employee $employee, array $data): array
{
$action = $data['action'];
$today = date('Y-m-d');
$nowTime = date('H:i:s');
$noteContent = $data['notes'] ?? $data['activity_note'] ?? null;
$latitude = isset($data['latitude']) ? (float)$data['latitude'] : null;
$longitude = isset($data['longitude']) ? (float)$data['longitude'] : null;
// 1. Check duplicate clock-in
$record = $this->getTodayRecord($employee);
if ($action === 'clock_in' && $record && $record->clock_in) {
return [
'success' => false,
'message' => 'Already clocked in for today',
'status_code' => 422,
'data' => null,
];
}
// 2. Check approved leave for today
$onLeave = LeaveApplication::where('employee_id', $user->id)
->where('status', 'approved')
->whereDate('start_date', '<=', $today)
->whereDate('end_date', '>=', $today)
->exists();
if ($onLeave && $action === 'clock_in') {
return [
'success' => false,
'message' => 'Cannot clock in while on approved leave',
'status_code' => 422,
'data' => null,
];
}
// 3. Clock out check without clock in
if ($action === 'clock_out' && (!$record || !$record->clock_in)) {
return [
'success' => false,
'message' => 'No active clock-in record found for today. Please clock in first.',
'status_code' => 422,
'data' => null,
];
}
// 4. Strict Branch & Geofence Validation
$branchId = $employee->branch_id ?? $user->branch_id ?? null;
if (!$branchId && $action === 'clock_in') {
return [
'success' => false,
'message' => 'No assigned branch found for employee. Please assign a branch prior to clocking in.',
'status_code' => 422,
'data' => null,
];
}
$branch = $branchId ? Branch::find($branchId) : null;
if ($branch && isset($branch->enable_clock_in_out) && $branch->enable_clock_in_out == 1) {
if ($latitude !== null && $longitude !== null && isset($branch->latitude, $branch->longitude)) {
$distance = $this->calculateHaversineDistance($latitude, $longitude, (float)$branch->latitude, (float)$branch->longitude);
$allowedRadius = (float)($branch->radius ?? 500); // 500 meters default
if ($distance > $allowedRadius) {
return [
'success' => false,
'message' => "Geofence validation failed. You are outside the allowed branch radius (" . round($distance) . "m away).",
'status_code' => 422,
'data' => null,
];
}
}
}
// 5. Perform database transaction writing user->id
return DB::transaction(function () use ($action, $record, $employee, $user, $today, $nowTime, $noteContent, $latitude, $longitude, $branchId) {
if ($action === 'clock_in') {
if (!$record) {
$record = AttendanceRecord::create([
'employee_id' => $user->id,
'branch_id' => $branchId,
'date' => $today,
'clock_in' => $nowTime,
'status' => 'present',
'notes' => $noteContent,
'clock_in_latitude' => $latitude,
'clock_in_longitude' => $longitude,
'created_by' => $user->id,
]);
} else {
$record->update([
'clock_in' => $record->clock_in ?? $nowTime,
'notes' => $noteContent ?? $record->notes,
]);
}
} else { // clock_out
$clockInTime = Carbon::parse($record->clock_in);
$clockOutTime = Carbon::now();
$totalHours = round($clockOutTime->diffInMinutes($clockInTime) / 60, 2);
$record->update([
'clock_out' => $nowTime,
'total_hours' => $totalHours,
'clock_out_latitude' => $latitude,
'clock_out_longitude' => $longitude,
'notes' => $noteContent ?? $record->notes,
]);
}
return [
'success' => true,
'message' => 'Attendance successfully updated',
'status_code' => 200,
'data' => [
'id' => $record->id,
'date' => $record->date,
'clock_in' => $record->clock_in,
'clock_out' => $record->clock_out,
'total_hours' => $record->total_hours,
'status' => $record->status ?? 'present',
'notes' => $record->notes,
],
];
});
}
}