feat: add AttendanceMap component and integrate it into AttendanceDetailDialog for displaying attendance locations on a map
This commit is contained in:
parent
070ac5b953
commit
bb8dd68f15
@ -3,6 +3,7 @@ import { router } from '@inertiajs/vue3';
|
||||
import { Trash2 } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import AttendanceMap from '@/components/admin/hr/attendances/AttendanceMap.vue';
|
||||
import AttendancePhotoCell from '@/components/admin/hr/attendances/attendance-photo-cell.vue';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -52,7 +53,7 @@ function destroyAttendance() {
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogContent class="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Presensi</DialogTitle>
|
||||
<DialogDescription v-if="attendance">
|
||||
@ -93,6 +94,19 @@ function destroyAttendance() {
|
||||
</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"
|
||||
:check-in-location-tag="attendance.check_in_location_tag"
|
||||
:check-out-location-tag="attendance.check_out_location_tag"
|
||||
:check-in-at-formatted="attendance.check_in_at_formatted"
|
||||
:check-out-at-formatted="attendance.check_out_at_formatted"
|
||||
:attendance-date-formatted="attendance.attendance_date_formatted"
|
||||
:employee-name="attendance.employee_name"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
Foto
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
checkInLat: number | null;
|
||||
checkInLng: number | null;
|
||||
checkOutLat: number | null;
|
||||
checkOutLng: number | null;
|
||||
checkInLocationTag: string | null;
|
||||
checkOutLocationTag: string | null;
|
||||
checkInAtFormatted: string;
|
||||
checkOutAtFormatted: string | null;
|
||||
attendanceDateFormatted: string;
|
||||
employeeName: string | null;
|
||||
}>();
|
||||
|
||||
const hasCoordinates = computed(() => props.checkInLat != null && props.checkInLng != null);
|
||||
|
||||
const checkInSrc = computed(() => {
|
||||
if (props.checkInLat == null || props.checkInLng == null) return '';
|
||||
return `https://www.google.com/maps?q=${props.checkInLat},${props.checkInLng}&z=18&t=k&output=embed`;
|
||||
});
|
||||
|
||||
const checkOutSrc = computed(() => {
|
||||
if (props.checkOutLat == null || props.checkOutLng == null) return '';
|
||||
return `https://www.google.com/maps?q=${props.checkOutLat},${props.checkOutLng}&z=18&t=k&output=embed`;
|
||||
});
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function buildLabel(
|
||||
title: string,
|
||||
color: string,
|
||||
time: string,
|
||||
locationTag: string | null,
|
||||
lat: number | null,
|
||||
lng: number | null,
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`<strong style="color:${color}">${title}</strong>`);
|
||||
if (props.employeeName) lines.push(`<span>Karyawan: <b>${escapeHtml(props.employeeName)}</b></span>`);
|
||||
lines.push(`<span>Tanggal: ${escapeHtml(props.attendanceDateFormatted)}</span>`);
|
||||
lines.push(`<span>Jam: <b>${escapeHtml(time)}</b></span>`);
|
||||
lines.push(`<span>Lokasi: ${escapeHtml(locationTag ?? '-')}</span>`);
|
||||
lines.push(`<span style="font-family:monospace;font-size:11px">Koordinat: ${lat}, ${lng}</span>`);
|
||||
return lines.join('<br>');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasCoordinates" class="space-y-3">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Lokasi Presensi
|
||||
</p>
|
||||
|
||||
<!-- Check-in map -->
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
class="text-xs font-medium"
|
||||
v-html="buildLabel('Masuk', '#16a34a', checkInAtFormatted, checkInLocationTag, checkInLat, checkInLng)"
|
||||
/>
|
||||
<iframe
|
||||
:src="checkInSrc"
|
||||
class="h-56 w-full overflow-hidden rounded-lg"
|
||||
style="border: 0"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Check-out map -->
|
||||
<div v-if="checkOutLat != null && checkOutLng != null" class="space-y-1.5">
|
||||
<div
|
||||
class="text-xs font-medium"
|
||||
v-html="buildLabel('Pulang', '#dc2626', checkOutAtFormatted ?? '-', checkOutLocationTag, checkOutLat, checkOutLng)"
|
||||
/>
|
||||
<iframe
|
||||
:src="checkOutSrc"
|
||||
class="h-56 w-full overflow-hidden rounded-lg"
|
||||
style="border: 0"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -12,6 +12,10 @@ export type AttendanceListItem = {
|
||||
check_out_photo_url: string | null;
|
||||
check_in_location_tag: string | null;
|
||||
check_out_location_tag: string | null;
|
||||
check_in_latitude: number | null;
|
||||
check_in_longitude: number | null;
|
||||
check_out_latitude: number | null;
|
||||
check_out_longitude: number | null;
|
||||
work_duration_minutes: number | null;
|
||||
work_duration_formatted: string | null;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user