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
This commit is contained in:
parent
e153497114
commit
786d0fdb39
@ -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'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AttendanceCard
|
||||
:today-attendance="todayAttendance"
|
||||
:is-on-leave="isOnLeave"
|
||||
:can-check-in="canCheckIn"
|
||||
@check-in="openCheckInModal"
|
||||
@check-out="openCheckOutModal"
|
||||
/>
|
||||
<AttendanceCard :today-attendance="todayAttendance" :is-on-leave="isOnLeave" :can-check-in="canCheckIn"
|
||||
@check-in="openCheckInModal" @check-out="openCheckOutModal" />
|
||||
|
||||
<Card class="min-w-0 overflow-hidden">
|
||||
<CardContent class="min-w-0">
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Trash2 } from '@lucide/vue';
|
||||
import AttendanceMap from './AttendanceMap.vue';
|
||||
import AttendancePhotoCell from './attendance-photo-cell.vue';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -13,8 +11,10 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { useDestroy } from '@/composables/useDestroy';
|
||||
import type { AttendanceListItem } from '@/types/attendance';
|
||||
import { destroy } from '@/routes/admin/hr/attendances';
|
||||
import type { AttendanceListItem } from '@/types/attendance';
|
||||
import AttendancePhotoCell from './attendance-photo-cell.vue';
|
||||
import AttendanceMap from './AttendanceMap.vue';
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
@ -76,29 +76,20 @@ const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyA
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<AttendanceMap
|
||||
:check-in-lat="attendance.check_in_latitude"
|
||||
:check-in-lng="attendance.check_in_longitude"
|
||||
:check-out-lat="attendance.check_out_latitude"
|
||||
:check-out-lng="attendance.check_out_longitude"
|
||||
/>
|
||||
<AttendanceMap :check-in-lat="attendance.check_in_latitude"
|
||||
:check-in-lng="attendance.check_in_longitude" :check-out-lat="attendance.check_out_latitude"
|
||||
:check-out-lng="attendance.check_out_longitude" />
|
||||
|
||||
<div>
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
Foto
|
||||
</p>
|
||||
<AttendancePhotoCell
|
||||
:check-in-photo-url="attendance.check_in_photo_url"
|
||||
:check-out-photo-url="attendance.check_out_photo_url"
|
||||
/>
|
||||
<AttendancePhotoCell :check-in-photo-url="attendance.check_in_photo_url"
|
||||
:check-out-photo-url="attendance.check_out_photo_url" />
|
||||
</div>
|
||||
|
||||
<div v-if="can('attendances.delete')" class="flex justify-end border-t pt-4">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@click="deleteConfirmOpen = true"
|
||||
>
|
||||
<Button variant="destructive" size="sm" @click="deleteConfirmOpen = true">
|
||||
<Trash2 class="size-4" />
|
||||
Hapus
|
||||
</Button>
|
||||
@ -107,15 +98,9 @@ const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyA
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="can('attendances.delete') && attendance"
|
||||
v-model:open="deleteConfirmOpen"
|
||||
<ConfirmDialog v-if="can('attendances.delete') && attendance" v-model:open="deleteConfirmOpen"
|
||||
title="Hapus data presensi?"
|
||||
:description="`Presensi tanggal ${attendance.attendance_date_formatted} akan dihapus secara permanen.`"
|
||||
confirm-label="Hapus"
|
||||
cancel-label="Batal"
|
||||
destructive
|
||||
:loading="deleteProcessing"
|
||||
@confirm="destroyAttendance"
|
||||
/>
|
||||
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
|
||||
@confirm="destroyAttendance" />
|
||||
</template>
|
||||
|
||||
@ -46,7 +46,7 @@ function createAttendanceEmployeeUser(PermissionEnum ...$permissions): User
|
||||
'full_name' => fake()->name(),
|
||||
]);
|
||||
Employee::factory()->create(['user_id' => $user->id]);
|
||||
$user->assignRole('marketing');
|
||||
$user->assignRole('marketing-offline');
|
||||
|
||||
$user->givePermissionTo(
|
||||
array_merge(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user