169 lines
6.2 KiB
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,
|
|
],
|
|
];
|
|
});
|
|
}
|
|
}
|