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
|
public function attendanceDateFormatted(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
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\LeaveRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
||||||
|
use App\Services\Concerns\RunsInTransaction;
|
||||||
use App\Services\Media\MediaService;
|
use App\Services\Media\MediaService;
|
||||||
use App\Services\System\PushNotificationService;
|
use App\Services\System\PushNotificationService;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
class AttendanceService
|
class AttendanceService
|
||||||
{
|
{
|
||||||
use ResolvesAuthenticatedEmployee;
|
use ResolvesAuthenticatedEmployee, RunsInTransaction;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly MediaService $mediaService,
|
private readonly MediaService $mediaService,
|
||||||
@ -71,6 +72,8 @@ public function checkIn(array $validated, User $user): void
|
|||||||
{
|
{
|
||||||
$employee = $this->resolveAuthenticatedEmployee($user);
|
$employee = $this->resolveAuthenticatedEmployee($user);
|
||||||
|
|
||||||
|
$this->runInTransaction(
|
||||||
|
function () use ($employee, $validated): void {
|
||||||
$existing = Attendance::query()
|
$existing = Attendance::query()
|
||||||
->where('employee_id', $employee->id)
|
->where('employee_id', $employee->id)
|
||||||
->whereDate('attendance_date', today())
|
->whereDate('attendance_date', today())
|
||||||
@ -96,6 +99,9 @@ public function checkIn(array $validated, User $user): void
|
|||||||
'checkin',
|
'checkin',
|
||||||
'checkin',
|
'checkin',
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
'Gagal mencatat presensi masuk',
|
||||||
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'⏰ Presensi Masuk',
|
'⏰ Presensi Masuk',
|
||||||
@ -109,6 +115,8 @@ public function checkOut(array $validated, User $user): void
|
|||||||
{
|
{
|
||||||
$employee = $this->resolveAuthenticatedEmployee($user);
|
$employee = $this->resolveAuthenticatedEmployee($user);
|
||||||
|
|
||||||
|
$workDurationMinutes = $this->runInTransaction(
|
||||||
|
function () use ($employee, $validated): int {
|
||||||
$attendance = Attendance::query()
|
$attendance = Attendance::query()
|
||||||
->where('employee_id', $employee->id)
|
->where('employee_id', $employee->id)
|
||||||
->whereDate('attendance_date', today())
|
->whereDate('attendance_date', today())
|
||||||
@ -143,6 +151,11 @@ public function checkOut(array $validated, User $user): void
|
|||||||
'checkout',
|
'checkout',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return $workDurationMinutes;
|
||||||
|
},
|
||||||
|
'Gagal mencatat presensi pulang',
|
||||||
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'⏰ Presensi Pulang',
|
'⏰ Presensi Pulang',
|
||||||
"Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).',
|
"Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).',
|
||||||
@ -157,13 +170,18 @@ public function delete(Attendance $attendance): void
|
|||||||
$employeeName = $attendance->employee?->user?->profile?->full_name;
|
$employeeName = $attendance->employee?->user?->profile?->full_name;
|
||||||
$date = $attendance->attendance_date;
|
$date = $attendance->attendance_date;
|
||||||
|
|
||||||
|
$this->runInTransaction(
|
||||||
|
function () use ($attendance): void {
|
||||||
$attendance->clearMediaCollection('checkin');
|
$attendance->clearMediaCollection('checkin');
|
||||||
$attendance->clearMediaCollection('checkout');
|
$attendance->clearMediaCollection('checkout');
|
||||||
$attendance->delete();
|
$attendance->delete();
|
||||||
|
},
|
||||||
|
'Gagal menghapus presensi',
|
||||||
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Presensi Dihapus',
|
'🗑️ Presensi Dihapus',
|
||||||
"Data presensi {$employeeName} tanggal {$date} telah dihapus.",
|
"Data presensi {$employeeName} tanggal {$date?->toDateString()} telah dihapus.",
|
||||||
['owner', 'developer', 'direktur'],
|
['owner', 'developer', 'direktur'],
|
||||||
route('admin.hr.attendances.index'),
|
route('admin.hr.attendances.index'),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type { AttendanceListItem, CalendarRange, TodayAttendance } from '@/types
|
|||||||
import AttendanceWebcamModal from './form/AttendanceWebcamModal.vue';
|
import AttendanceWebcamModal from './form/AttendanceWebcamModal.vue';
|
||||||
import AttendanceCalendar from './table/AttendanceCalendar.vue';
|
import AttendanceCalendar from './table/AttendanceCalendar.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
attendances: AttendanceListItem[];
|
attendances: AttendanceListItem[];
|
||||||
todayAttendance: TodayAttendance;
|
todayAttendance: TodayAttendance;
|
||||||
isOnLeave: boolean;
|
isOnLeave: boolean;
|
||||||
@ -44,13 +44,8 @@ function openCheckOutModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AttendanceCard
|
<AttendanceCard :today-attendance="todayAttendance" :is-on-leave="isOnLeave" :can-check-in="canCheckIn"
|
||||||
:today-attendance="todayAttendance"
|
@check-in="openCheckInModal" @check-out="openCheckOutModal" />
|
||||||
:is-on-leave="isOnLeave"
|
|
||||||
:can-check-in="canCheckIn"
|
|
||||||
@check-in="openCheckInModal"
|
|
||||||
@check-out="openCheckOutModal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card class="min-w-0 overflow-hidden">
|
<Card class="min-w-0 overflow-hidden">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Trash2 } from '@lucide/vue';
|
import { Trash2 } from '@lucide/vue';
|
||||||
import AttendanceMap from './AttendanceMap.vue';
|
|
||||||
import AttendancePhotoCell from './attendance-photo-cell.vue';
|
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -13,8 +11,10 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { useDestroy } from '@/composables/useDestroy';
|
import { useDestroy } from '@/composables/useDestroy';
|
||||||
import type { AttendanceListItem } from '@/types/attendance';
|
|
||||||
import { destroy } from '@/routes/admin/hr/attendances';
|
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 });
|
const open = defineModel<boolean>('open', { required: true });
|
||||||
|
|
||||||
@ -76,29 +76,20 @@ const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyA
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<AttendanceMap
|
<AttendanceMap :check-in-lat="attendance.check_in_latitude"
|
||||||
:check-in-lat="attendance.check_in_latitude"
|
:check-in-lng="attendance.check_in_longitude" :check-out-lat="attendance.check_out_latitude"
|
||||||
:check-in-lng="attendance.check_in_longitude"
|
:check-out-lng="attendance.check_out_longitude" />
|
||||||
:check-out-lat="attendance.check_out_latitude"
|
|
||||||
:check-out-lng="attendance.check_out_longitude"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-muted-foreground mb-2 text-sm">
|
<p class="text-muted-foreground mb-2 text-sm">
|
||||||
Foto
|
Foto
|
||||||
</p>
|
</p>
|
||||||
<AttendancePhotoCell
|
<AttendancePhotoCell :check-in-photo-url="attendance.check_in_photo_url"
|
||||||
:check-in-photo-url="attendance.check_in_photo_url"
|
:check-out-photo-url="attendance.check_out_photo_url" />
|
||||||
:check-out-photo-url="attendance.check_out_photo_url"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="can('attendances.delete')" class="flex justify-end border-t pt-4">
|
<div v-if="can('attendances.delete')" class="flex justify-end border-t pt-4">
|
||||||
<Button
|
<Button variant="destructive" size="sm" @click="deleteConfirmOpen = true">
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
@click="deleteConfirmOpen = true"
|
|
||||||
>
|
|
||||||
<Trash2 class="size-4" />
|
<Trash2 class="size-4" />
|
||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
@ -107,15 +98,9 @@ const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyA
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-if="can('attendances.delete') && attendance" v-model:open="deleteConfirmOpen"
|
||||||
v-if="can('attendances.delete') && attendance"
|
|
||||||
v-model:open="deleteConfirmOpen"
|
|
||||||
title="Hapus data presensi?"
|
title="Hapus data presensi?"
|
||||||
:description="`Presensi tanggal ${attendance.attendance_date_formatted} akan dihapus secara permanen.`"
|
:description="`Presensi tanggal ${attendance.attendance_date_formatted} akan dihapus secara permanen.`"
|
||||||
confirm-label="Hapus"
|
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
|
||||||
cancel-label="Batal"
|
@confirm="destroyAttendance" />
|
||||||
destructive
|
|
||||||
:loading="deleteProcessing"
|
|
||||||
@confirm="destroyAttendance"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -46,7 +46,7 @@ function createAttendanceEmployeeUser(PermissionEnum ...$permissions): User
|
|||||||
'full_name' => fake()->name(),
|
'full_name' => fake()->name(),
|
||||||
]);
|
]);
|
||||||
Employee::factory()->create(['user_id' => $user->id]);
|
Employee::factory()->create(['user_id' => $user->id]);
|
||||||
$user->assignRole('marketing');
|
$user->assignRole('marketing-offline');
|
||||||
|
|
||||||
$user->givePermissionTo(
|
$user->givePermissionTo(
|
||||||
array_merge(
|
array_merge(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user