From 786d0fdb3929b62a5098c78a9e4602240dce527a Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 4 Jul 2026 12:00:52 +0700 Subject: [PATCH] feat: implement transaction management in AttendanceService for check-in and check-out processes; refactor attendance date formatting in Attendance model; enhance AttendanceDetailDialog and Index components for improved UI and code clarity --- app/Models/Attendance.php | 2 +- app/Services/Hr/AttendanceService.php | 124 ++++++++++-------- .../js/pages/admin/hr/attendances/Index.vue | 11 +- .../table/AttendanceDetailDialog.vue | 39 ++---- tests/Feature/Admin/Hr/AttendanceTest.php | 2 +- 5 files changed, 88 insertions(+), 90 deletions(-) diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index 96e2c2a..d33b022 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -49,7 +49,7 @@ protected function casts(): array public function attendanceDateFormatted(): Attribute { return Attribute::make( - get: fn () => Carbon::parse($this->attendance_date)->translatedFormat('l, d F Y'), + get: fn () => $this->attendance_date?->translatedFormat('l, d F Y'), ); } diff --git a/app/Services/Hr/AttendanceService.php b/app/Services/Hr/AttendanceService.php index 5c51da6..80f8ef9 100644 --- a/app/Services/Hr/AttendanceService.php +++ b/app/Services/Hr/AttendanceService.php @@ -9,6 +9,7 @@ use App\Models\LeaveRequest; use App\Models\User; use App\Services\Concerns\ResolvesAuthenticatedEmployee; +use App\Services\Concerns\RunsInTransaction; use App\Services\Media\MediaService; use App\Services\System\PushNotificationService; use Carbon\CarbonInterface; @@ -18,7 +19,7 @@ class AttendanceService { - use ResolvesAuthenticatedEmployee; + use ResolvesAuthenticatedEmployee, RunsInTransaction; public function __construct( private readonly MediaService $mediaService, @@ -71,30 +72,35 @@ public function checkIn(array $validated, User $user): void { $employee = $this->resolveAuthenticatedEmployee($user); - $existing = Attendance::query() - ->where('employee_id', $employee->id) - ->whereDate('attendance_date', today()) - ->exists(); + $this->runInTransaction( + function () use ($employee, $validated): void { + $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.', - ]); - } + if ($existing) { + throw ValidationException::withMessages([ + 'attendance' => 'Anda sudah melakukan presensi masuk hari ini.', + ]); + } - $attendance = Attendance::create([ - 'employee_id' => $employee->id, - 'attendance_date' => today(), - 'check_in_at' => now(), - 'check_in_latitude' => $validated['latitude'], - 'check_in_longitude' => $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'], + ]); - $this->mediaService->addBase64Image( - $attendance, - $validated['photo'], - 'checkin', - 'checkin', + $this->mediaService->addBase64Image( + $attendance, + $validated['photo'], + 'checkin', + 'checkin', + ); + }, + 'Gagal mencatat presensi masuk', ); $this->pushNotificationService->sendToRoles( @@ -109,38 +115,45 @@ public function checkOut(array $validated, User $user): void { $employee = $this->resolveAuthenticatedEmployee($user); - $attendance = Attendance::query() - ->where('employee_id', $employee->id) - ->whereDate('attendance_date', today()) - ->first(); + $workDurationMinutes = $this->runInTransaction( + function () use ($employee, $validated): int { + $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 === 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.', - ]); - } + 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); + $checkOutAt = now(); + $workDurationMinutes = (int) $attendance->check_in_at->diffInMinutes($checkOutAt); - $attendance->update([ - 'check_out_at' => $checkOutAt, - 'check_out_latitude' => $validated['latitude'], - 'check_out_longitude' => $validated['longitude'], - 'work_duration_minutes' => $workDurationMinutes, - ]); + $attendance->update([ + 'check_out_at' => $checkOutAt, + 'check_out_latitude' => $validated['latitude'], + 'check_out_longitude' => $validated['longitude'], + 'work_duration_minutes' => $workDurationMinutes, + ]); - $this->mediaService->addBase64Image( - $attendance, - $validated['photo'], - 'checkout', - 'checkout', + $this->mediaService->addBase64Image( + $attendance, + $validated['photo'], + 'checkout', + 'checkout', + ); + + return $workDurationMinutes; + }, + 'Gagal mencatat presensi pulang', ); $this->pushNotificationService->sendToRoles( @@ -157,13 +170,18 @@ public function delete(Attendance $attendance): void $employeeName = $attendance->employee?->user?->profile?->full_name; $date = $attendance->attendance_date; - $attendance->clearMediaCollection('checkin'); - $attendance->clearMediaCollection('checkout'); - $attendance->delete(); + $this->runInTransaction( + function () use ($attendance): void { + $attendance->clearMediaCollection('checkin'); + $attendance->clearMediaCollection('checkout'); + $attendance->delete(); + }, + 'Gagal menghapus presensi', + ); $this->pushNotificationService->sendToRoles( '🗑️ Presensi Dihapus', - "Data presensi {$employeeName} tanggal {$date} telah dihapus.", + "Data presensi {$employeeName} tanggal {$date?->toDateString()} telah dihapus.", ['owner', 'developer', 'direktur'], route('admin.hr.attendances.index'), ); diff --git a/resources/js/pages/admin/hr/attendances/Index.vue b/resources/js/pages/admin/hr/attendances/Index.vue index 240a9cc..4b99d57 100644 --- a/resources/js/pages/admin/hr/attendances/Index.vue +++ b/resources/js/pages/admin/hr/attendances/Index.vue @@ -8,7 +8,7 @@ import type { AttendanceListItem, CalendarRange, TodayAttendance } from '@/types import AttendanceWebcamModal from './form/AttendanceWebcamModal.vue'; import AttendanceCalendar from './table/AttendanceCalendar.vue'; -const props = defineProps<{ +defineProps<{ attendances: AttendanceListItem[]; todayAttendance: TodayAttendance; isOnLeave: boolean; @@ -44,13 +44,8 @@ function openCheckOutModal() { - + diff --git a/resources/js/pages/admin/hr/attendances/table/AttendanceDetailDialog.vue b/resources/js/pages/admin/hr/attendances/table/AttendanceDetailDialog.vue index 39530b5..b8610d1 100644 --- a/resources/js/pages/admin/hr/attendances/table/AttendanceDetailDialog.vue +++ b/resources/js/pages/admin/hr/attendances/table/AttendanceDetailDialog.vue @@ -1,7 +1,5 @@