store/app/Services/Hr/AttendanceService.php

200 lines
6.1 KiB
PHP

<?php
namespace App\Services\Hr;
use App\Models\Attendance;
use App\Models\Employee;
use App\Services\Media\MediaService;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
class AttendanceService
{
public function __construct(
private readonly MediaService $mediaService,
) {}
public function listForCalendar(
CarbonInterface $start,
CarbonInterface $end,
?int $scopedEmployeeId = null,
bool $hasScopedAccess = true,
): Collection {
return Attendance::query()
->with(['employee.user.profile', 'media'])
->when(! $hasScopedAccess, function (Builder $query): void {
$query->whereRaw('1 = 0');
})
->when($hasScopedAccess && $scopedEmployeeId !== null, function (Builder $query) use ($scopedEmployeeId): void {
$query->where('employee_id', $scopedEmployeeId);
})
->whereDate('attendance_date', '>=', $start->toDateString())
->whereDate('attendance_date', '<', $end->toDateString())
->orderBy('attendance_date')
->orderBy('check_in_at')
->get();
}
/**
* @return array<string, mixed>|null
*/
public function todayAttendanceForEmployee(Employee $employee): ?array
{
$attendance = Attendance::query()
->with('media')
->where('employee_id', $employee->id)
->whereDate('attendance_date', today())
->first();
return $attendance?->toArray();
}
/**
* @param array{photo: string, latitude: float, longitude: float, location_tag: string} $validated
*/
public function checkIn(array $validated): void
{
$employee = $this->resolveAuthEmployee();
$existing = Attendance::query()
->where('employee_id', $employee->id)
->whereDate('attendance_date', today())
->exists();
if ($existing) {
throw ValidationException::withMessages([
'attendance' => 'Anda sudah melakukan presensi masuk hari ini.',
]);
}
$locationTag = $this->resolveLocationTag(
$validated['location_tag'],
(float) $validated['latitude'],
(float) $validated['longitude'],
);
$attendance = Attendance::create([
'employee_id' => $employee->id,
'attendance_date' => today(),
'check_in_at' => now(),
'check_in_latitude' => $validated['latitude'],
'check_in_longitude' => $validated['longitude'],
'check_in_location_tag' => $locationTag,
]);
$this->mediaService->addBase64Image(
$attendance,
$validated['photo'],
'checkin',
'checkin',
);
}
/**
* @param array{photo: string, latitude: float, longitude: float, location_tag: string} $validated
*/
public function checkOut(array $validated): void
{
$employee = $this->resolveAuthEmployee();
$attendance = Attendance::query()
->where('employee_id', $employee->id)
->whereDate('attendance_date', today())
->first();
if ($attendance === null) {
throw ValidationException::withMessages([
'attendance' => 'Anda belum melakukan presensi masuk hari ini.',
]);
}
if ($attendance->check_out_at !== null) {
throw ValidationException::withMessages([
'attendance' => 'Anda sudah melakukan presensi pulang hari ini.',
]);
}
$checkOutAt = now();
$workDurationMinutes = (int) $attendance->check_in_at->diffInMinutes($checkOutAt);
$locationTag = $this->resolveLocationTag(
$validated['location_tag'],
(float) $validated['latitude'],
(float) $validated['longitude'],
);
$attendance->check_out_at = $checkOutAt;
$attendance->check_out_latitude = $validated['latitude'];
$attendance->check_out_longitude = $validated['longitude'];
$attendance->check_out_location_tag = $locationTag;
$attendance->work_duration_minutes = $workDurationMinutes;
$attendance->save();
$this->mediaService->addBase64Image(
$attendance,
$validated['photo'],
'checkout',
'checkout',
);
}
public function delete(Attendance $attendance): void
{
$attendance->clearMediaCollection('checkin');
$attendance->clearMediaCollection('checkout');
$attendance->delete();
}
private function resolveAuthEmployee(): Employee
{
$employee = auth()->user()?->employee;
if ($employee === null) {
throw ValidationException::withMessages([
'employee' => 'Akun Anda tidak terhubung ke data pegawai.',
]);
}
return $employee;
}
private function resolveLocationTag(string $clientTag, float $latitude, float $longitude): string
{
if ($clientTag !== '') {
return mb_substr($clientTag, 0, 255);
}
$geocoded = $this->reverseGeocode($latitude, $longitude);
if ($geocoded !== null) {
return mb_substr($geocoded, 0, 255);
}
return mb_substr(sprintf('%s, %s', $latitude, $longitude), 0, 255);
}
private function reverseGeocode(float $latitude, float $longitude): ?string
{
try {
$response = Http::timeout(5)
->withHeaders(['User-Agent' => config('app.name', 'DST Collection')])
->get('https://nominatim.openstreetmap.org/reverse', [
'lat' => $latitude,
'lon' => $longitude,
'format' => 'json',
]);
if (! $response->successful()) {
return null;
}
return $response->json('display_name');
} catch (\Throwable) {
return null;
}
}
}