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 @@