hrms-checkin/src/views/HomeView.vue
waruneeauy 7edfaa4e4f
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m12s
fix remove code check fake location
2026-03-30 10:47:36 +07:00

1286 lines
45 KiB
Vue

<script setup lang="ts">
import { ref, reactive, onMounted, watch, onBeforeUnmount } from 'vue'
import { useQuasar } from 'quasar'
import moment from 'moment'
import Camera from 'simple-vue-camera'
import config from '@/app.config'
import http from '@/plugins/http'
import { useCounterMixin } from '@/stores/mixin'
import { usePermissions } from '@/composables/usePermissions'
import { usePrivacyStore } from '@/stores/privacy'
import type { FormRef, OptionReason } from '@/interface/response/checkin'
import MapCheck from '@/components/AscGISMap.vue'
const mixin = useCounterMixin()
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
const $q = useQuasar()
const { checkPrivacyAccepted } = usePermissions()
const privacyStore = usePrivacyStore()
const MOCK_CHECK_DELAY_MS = 800
const modalTime = ref<boolean>(false) // Dailog ลงเวลาเข้างานของคุณ
const checkStatus = ref<string>('')
const statusCheckin = ref<boolean>(true) // สถานะเวลา เข้า,ออก
const checkDate = ref<boolean | null>(null)
const msgCheckTime = ref<string>('') // ข้อความแจ้งเตือน
const isDisabledCheckTime = ref<boolean>(true) // ข้อความแจ้งเตือน
const isErr = ref<boolean | null>(null) // ข้อความแจ้งเตือน
const endTimeAfternoon = ref<string>('12:00:00') //เวลาเช็คเอาท์ตามรอบ
const isLoadingCheckTime = ref<boolean>(false) // ตัวแปรสำหรับการโหลด
const disabledBtn = ref<boolean>(false)
/**
* fetch เช็คเวลาต้องลงเวลาเข้าหรือออกงาน
*/
async function fetchCheckTime(load: any = true) {
if (load) isLoadingCheckTime.value = true
await http
.get(config.API.checkTime())
.then(async (res) => {
const data = await res.data.result
statusCheckin.value = data.checkInId ? false : true
checkInId.value = data.checkInId ? data.checkInId : ''
endTimeAfternoon.value = data.endTimeAfternoon
isDisabledCheckTime.value = isDisabledCheckTime.value ? true : false
})
.catch((err) => {
if (err.response.status === 500) {
isErr.value = true
isDisabledCheckTime.value = true
msgCheckTime.value = err.response.data.message
} else messageError($q, err)
})
.finally(() => {
if (load) isLoadingCheckTime.value = false
})
}
/** ref อัพเดทเวลา*/
const dateNow = ref<Date>(new Date())
const Thai = ref<Date>(dateNow.value)
const formattedS = ref()
const formattedM = ref()
const formattedH = ref()
const formattedHH = ref()
const formattedA = ref()
/** function อัพเดทเวลา*/
function updateClock() {
const date = Date.now()
const hh = moment(date).format('HH')
const mm = moment(date).format('mm')
const ss = moment(date).format('ss')
const HH = moment(date).format('hh')
const A = moment(date).format('a')
formattedS.value = ss
formattedM.value = mm
formattedH.value = hh
formattedHH.value = HH
formattedA.value = A
}
setInterval(updateClock, 1000)
/** form-data */
const formLocation = reactive({
lat: 0, //พิกัดละติจูด
lng: 0, //พิกัดลองจิจูด
POI: '', //ชื่อสถานที่
})
const workplace = ref<string>('in-place') //ณ สถานที่ตั้ง, นอกสถานที่ตั้ง
const useLocation = ref<string>('') //กรณีเลือกนอกสถานที่ตั้ง ต้องระบุข้อมูลชื่อสถานะที่
const fileImg = ref<any>() //รูปถ่ายสถานที่
const remark = ref<string>('') //ข้อความหมายเหตุที่ต้องการระบุเพิ่ม
const checkInId = ref<string>('') //Id ลงเวลา check-in ล่าสุดที่ยังไม่ลงเวลาออก
/**
* funciton เรียกพิกัดละติจูด พิกัดลองติจูด
* @param location พิกัดละติจูด พิกัดลองติจูด
* @param namePOI ชื่อสถานที่ ได้มาจากระบบ ArcGis ของกองสารสนเทศภูมิศาสตร์
*/
async function updateLocation(
latitude: number,
longitude: number,
namePOI: string
) {
formLocation.lat = latitude
formLocation.lng = longitude
formLocation.POI = namePOI
}
function resetLocationForRetry() {
formLocation.lat = 0
formLocation.lng = 0
formLocation.POI = ''
}
function getCurrentPositionAsync(options?: PositionOptions) {
return new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options)
})
}
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function getDelayedFreshPosition() {
const firstPosition = await getCurrentPositionAsync({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
})
await wait(MOCK_CHECK_DELAY_MS)
try {
return await getCurrentPositionAsync({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
})
} catch (error) {
return firstPosition
}
}
const location = ref<string>('') // พื้นที่ใกล้เคียง
const model = ref<string>('') // สถานที่ทำงาน
// ตัวเลือกสถานที่ทำงาน
const options = ref<OptionReason[]>([
{ value: 'ปฏิบัติงานที่บ้าน', text: 'ปฏิบัติงานที่บ้าน (WFH)' },
{ value: 'ปฏิบัติงานนอกสถานที่', text: 'ปฏิบัติงานนอกสถานที่ (WFA)' },
// { value: 'ลืมลงเวลาเริ่มปฏิบัติราชการ', text: 'ลืมลงเวลาเริ่มปฏิบัติราชการ' },
// { value: 'ลืมลงเวลาเลิกปฏิบัติราชการ', text: 'ลืมลงเวลาเลิกปฏิบัติราชการ' },
{
value: 'ไปประชุม / อบรม / สัมมนา',
text: 'ไปประชุม / อบรม / สัมมนา',
},
// { value: 'ลางานครึ่งวัน (เช้า)', text: 'ลางานครึ่งวัน (เช้า)' },
// { value: 'ลางานครึ่งวัน (บ่าย)', text: 'ลางานครึ่งวัน (บ่าย)' },
{ value: 'ขออนุญาตออกนอกสถานที่', text: 'ขออนุญาตออกนอกสถานที่' },
// {
// value: 'ประสบภัย เช่น น้ำท่วม มีพายุ ประสบอุบัติเหตุ',
// text: 'ประสบภัย เช่น น้ำท่วม มีพายุ ประสบอุบัติเหตุ',
// },
{ value: 'อื่นๆ', text: 'อื่นๆ' },
])
/** function เลือกสถานที่*/
function selectLocation() {
if (model.value === 'อื่นๆ') {
useLocation.value = ''
} else {
useLocation.value = model.value
}
}
/** Camera */
const camera = ref<InstanceType<typeof Camera>>()
const cameraIsOn = ref<boolean>(false)
const img = ref<any>(undefined)
const photoWidth = ref<number>(350)
const photoHeight = ref<number>(350)
const intervalId = ref<number | undefined>(undefined) // ต้องใช้ตัวแปรเก็บค่า interval
/**
* เริ่มจาก onMounted #1 เช็ค status คิว
*
*/
async function startChecking() {
try {
// showLoader()
await fetchCheckStatus()
await fetchCheckTime()
} catch (error) {
console.error('Error in startChecking:', error)
// hideLoader()
}
}
/**
* เริ่มจาก onMounted #2 เช็ค status คิว
*
*/
async function fetchCheckStatus() {
try {
const res = await http.get(config.API.checkStatus())
inQueue.value = res.data.result.inQueue
if (res.data.result.inQueue) {
/** inQueue เป็น true */
isDisabledCheckTime.value = true
msgCheckTime.value = 'ระบบกำลังประมวลผล'
if (intervalId.value === undefined) {
intervalId.value = setInterval(async () => {
try {
await fetchCheckStatus()
} catch (error) {
console.error('Error in interval fetchCheckStatus:', error)
// หยุด interval ถ้าเกิด error
stopChecking()
}
}, 3000)
console.log('startChecking called, intervalId:', intervalId.value)
}
// hideLoader()
} else {
/** inQueue เป็น false */
isDisabledCheckTime.value = false
msgCheckTime.value = ''
await stopChecking() // หยุดการทำงาน
}
} catch (error) {
console.log('เกิดข้อผิดพลาด', error)
messageError($q, error)
stopChecking() // หยุดการทำงาน
}
}
/** ตัวเก่าก่อนเปลี่ยน */
// async function stopChecking() {
// if (intervalId.value !== undefined) {
// clearInterval(intervalId.value) // หยุด interval
// setTimeout(() => {
// fetchCheckTime()
// }, 1000)
// intervalId.value = undefined // รีเซ็ตค่า
// }
// }
/** ตัวใหม่ที่เปลี่ยนก่อนเปลี่ยน
* เริ่มจาก onMounted #3 เช็ค status คิว
*
*/
async function stopChecking() {
if (intervalId.value !== undefined) {
await fetchCheckTime(false) // เรียก fetchCheckTime ก่อนหยุด interval
clearInterval(intervalId.value)
intervalId.value = undefined // รีเซ็ตค่า interval
async function repeatFetch() {
await fetchCheckTime(false)
if (statusCheckin.value && checkDate.value === true) {
setTimeout(repeatFetch, 1000)
} else {
checkDate.value = null
isErr.value = false
}
}
async function repeatFetchOut() {
await fetchCheckTime(false)
if (
isErr.value === false &&
statusCheckin.value === false &&
checkDate.value === true
) {
setTimeout(repeatFetchOut, 1000) // เรียกตัวเองซ้ำ
} else {
checkDate.value = null
isErr.value = null
}
}
// ตรวจสอบเงื่อนไขก่อนเริ่มการ fetch ซ้ำ
if (statusCheckin.value && checkDate.value === true) {
repeatFetch()
} else if (
isErr.value === false &&
statusCheckin.value === false &&
checkDate.value === true
) {
repeatFetchOut()
}
}
}
/** function เปิดกล้อง*/
async function openCamera() {
// เช็คสิทธิ์ privacy ก่อนเปิดกล้อง
if (!checkPrivacyAccepted()) {
return
}
if (!isPermissionCameraDenied.value) {
// change camera device
if (cameraIsOn.value) {
await camera.value?.stop()
} else {
await camera.value?.start()
await changeCamera() // ต้องรอให้ start() เสร็จก่อน
}
cameraIsOn.value = !cameraIsOn.value
} else {
messageError(
$q,
'',
'ไม่สามารถเข้าถึงกล้องได้ กรุณาอนุญาตการเข้าถึงกล้องในเบราว์เซอร์ของคุณ'
)
return
}
}
/** change camera device*/
async function changeCamera() {
const devices: any = await camera.value?.devices(['videoinput'])
const device = await devices[0]
camera.value?.changeCamera(device.deviceId)
}
/** function ถ่ายรูป*/
async function capturePhoto() {
const imageBlob: any = await camera.value?.snapshot(
{ width: photoWidth.value, height: photoHeight.value },
'image/png',
0.5
)
if (!imageBlob) return
const fileName = 'photo.jpg'
//ไฟล์รูป
const file = new File([imageBlob], fileName, { type: 'image/png' })
fileImg.value = file
//แสดงรูป
await camera.value?.stop()
const url = URL.createObjectURL(imageBlob)
img.value = url
}
/** function เปลี่ยนรูปภาพ*/
function refreshPhoto() {
img.value = undefined
camera.value?.start()
}
/** ref validate*/
const useLocationRef = ref<object | null>(null)
const modelRef = ref<object | null>(null)
const objectRef: FormRef = {
model: modelRef,
useLocation: useLocationRef,
}
/** function ตรวจสอบค่าว่างของ input*/
async function validateForm() {
disabledBtn.value = true
const hasError = []
for (const key in objectRef) {
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
const property = objectRef[key]
if (property.value && typeof property.value.validate === 'function') {
const isValid = property.value.validate()
hasError.push(isValid)
}
}
}
if (hasError.every((result) => result === true)) {
if (statusCheckin.value == false) {
getCheck()
} else if (statusCheckin.value) {
// dialog ยืนยันการลงเวลาเข้างาน
dialogConfirm(
$q,
() => confirm(),
'ยืนยันการลงเวลาเข้างาน',
`แจ้งเตือนการลงเวลาเข้างาน ${
workplace.value === 'in-place'
? 'ในสถานที่'
: `นอกสถานที่ (${
model.value === 'อื่นๆ' ? useLocation.value : model.value
})`
} คุณต้องการยืนยันการลงเวลาเข้างาน?`,
() => {
disabledBtn.value = false
},
'red',
'ยืนยัน'
)
}
} else {
disabledBtn.value = false
}
}
const mapRef = ref<InstanceType<typeof MapCheck> | null>(null)
const timeChickin = ref<string>('') //เวลาเข้างาน,เวลาออกงาน
/** function ยืนยันการลงเวลาเข้า - ออก*/
async function confirm() {
// เช็คสิทธิ์ privacy ก่อนใช้งานแผนที่และกล้อง
if (!checkPrivacyAccepted()) {
disabledBtn.value = false
return
}
if (!formLocation.POI || !formLocation.lat || !formLocation.lng) {
disabledBtn.value = false
mapRef.value?.requestLocationPermission()
return
}
showLoader()
const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง
const locationName = workplace.value === 'in-place' ? '' : useLocation.value
const formdata = new FormData()
formdata.append('lat', formLocation.lat.toString())
formdata.append('lon', formLocation.lng.toString())
formdata.append('POI', formLocation.POI)
formdata.append('isLocation', isLocation.toString())
formdata.append('locationName', locationName)
formdata.append('img', fileImg.value)
formdata.append('remark', remark.value)
formdata.append('checkInId', checkInId.value)
await http
.post(config.API.checkin(), formdata)
.then(async (res) => {
const data = await res.data.result
const dateObject = new Date(data.date)
checkDate.value = data.date ? true : false
const options: Intl.DateTimeFormatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
}
const timeString = new Intl.DateTimeFormat('en-US', options).format(
dateObject
)
timeChickin.value = timeString
modalTime.value = true
remark.value = ''
disabledBtn.value = false
})
.catch((err) => {
messageError($q, err)
disabledBtn.value = false
})
.finally(() => {
hideLoader()
})
}
async function getCheck() {
if (!formLocation.POI || !formLocation.lat || !formLocation.lng) {
disabledBtn.value = false
mapRef.value?.requestLocationPermission()
return
}
showLoader()
const isSeminar = model.value === 'ไปประชุม / อบรม / สัมมนา' ? 'Y' : 'N'
await http
.get(config.API.checkoutCheck + `/${isSeminar}`)
.then(async (res) => {
checkStatus.value = await res.data.result.status.toLocaleUpperCase()
if (checkStatus.value == 'ABSENT') {
const options: Intl.DateTimeFormatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
}
const timeVal =
new Intl.DateTimeFormat('en-US', options).format(
new Date(res.data.result.serverTime)
) ?? ''
const endTimeAfternoonVal =
new Intl.DateTimeFormat('en-US', options).format(
new Date(res.data.result.endTime)
) ?? ''
// dialog ยืนยันการลงเวลาออกงาน
dialogConfirm(
$q,
() => confirm(),
'ยืนยันการลงเวลาออกงาน',
`เวลาออกจากงานของคุณคือ ${endTimeAfternoonVal} แต่ขณะนี้เป็นเวลา ${timeVal} น. หากคุณออกจากงานในเวลานี้สถานะการลงเวลาจะเป็น "${res.data.result.statusText}" คุณแน่ใจว่าจะลงเวลาออกงานในตอนนี้ใช่หรือไม่?`,
() => {
disabledBtn.value = false
},
'red',
'ยืนยัน'
)
} else if (checkStatus.value == 'NORMAL') {
confirm()
}
})
.catch((e) => {
disabledBtn.value = false
messageError($q, e)
})
.finally(() => {
hideLoader()
})
}
/** ปิด popup แสดงการลงเวลา*/
async function onClickConfirm() {
try {
showLoader()
cameraIsOn.value = false
img.value = undefined
modalTime.value = false
if (!statusCheckin.value) {
statusCheckin.value = true
}
await startChecking()
} finally {
hideLoader()
}
}
/** เลือกสถานที่ทำงาน*/
function updateWorkplace() {
useLocation.value = ''
model.value = ''
}
/**
* รี้เทินร์ class สีพื้นหลัง
* @param val ค่า statusCheckin
*/
const getClass = (val: boolean) => {
return {
'bg-primary text-white col-12 row items-center q-px-md q-py-sm ': val,
'bg-red-8 text-white col-12 row items-center q-px-md q-py-sm ': !val,
}
}
const getClassXS = (val: boolean) => {
return {
'text-white q-pa-lg col-12 row bg-primary': val,
'text-white q-pa-lg col-12 row bg-red-8': !val,
}
}
const inQueue = ref<boolean>(false)
// ฟังก์ชันสำหรับรีเซ็ตรูปและหยุดกล้อง
function resetCameraAndImage() {
if (img.value) {
img.value = undefined
}
if (cameraIsOn.value && camera.value) {
camera.value.stop()
cameraIsOn.value = false
}
}
// เพิ่มฟังก์ชันสำหรับจัดการการปิดแอพในมือถือ
function handleAppClose() {
resetCameraAndImage()
// หยุด interval ถ้ามี
if (intervalId.value !== undefined) {
clearInterval(intervalId.value)
intervalId.value = undefined
}
}
// จัดการ visibility change สำหรับมือถือ
function handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
handleAppClose()
} else if (document.visibilityState === 'visible') {
// เมื่อกลับมาที่แอพ ให้เรียกแผนที่ใหม่
if (privacyStore.isAccepted) {
mapRef.value?.requestLocationPermission()
}
}
}
const isPermissionCameraDenied = ref<boolean>(false) // ตัวแปรสำหรับตรวจสอบการปฏิเสธสิทธิ์กล้อง
async function requestCamera() {
try {
await navigator.mediaDevices.getUserMedia({ video: true })
} catch (err) {
isPermissionCameraDenied.value = true
}
}
/** Hook*/
onMounted(async () => {
isLoadingCheckTime.value = true
updateClock()
startChecking() //เช็ค status จาก คิว #1
// เรียกแผนที่เฉพาะเมื่อยอมรับ privacy แล้ว
if (privacyStore.isAccepted) {
mapRef.value?.requestLocationPermission()
requestCamera()
}
// เพิ่ม event listeners สำหรับมือถือ
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('pagehide', handleAppClose)
})
onBeforeUnmount(() => {
resetCameraAndImage()
// หยุด interval ถ้ามี
if (intervalId.value !== undefined) {
clearInterval(intervalId.value)
intervalId.value = undefined
}
// ลบ event listeners
document.removeEventListener('visibilitychange', handleVisibilityChange)
window.removeEventListener('pagehide', handleAppClose)
})
watch(
() => privacyStore.isAccepted,
(newVal) => {
if (newVal) {
mapRef.value?.requestLocationPermission()
requestCamera()
}
}
)
</script>
<template>
<q-page :style="$q.screen.xs ? 'padding-top: 90px' : ''">
<div class="col-12 row justify-center">
<div class="col-xs-12 col-sm-12 col-md-12">
<q-card
flat
:class="
$q.screen.gt.xs ? 'row col-12 cardNone' : 'row col-12 bg-grey-2'
"
>
<!-- <q-header elevated class="bg-purple"> -->
<div
class="col-12 q-pa-md items-center gt-xs"
v-if="isLoadingCheckTime"
>
<q-skeleton width="100%" height="50px" />
</div>
<div v-else :class="getClass(statusCheckin)" class="gt-xs">
<div class="col">
<div class="row col-12 justify-center q-py-sm text-subtitle1">
<strong v-if="!statusCheckin && inQueue">
ลงเวลาออกงาน (ระบบกำลังประมวลผล)
</strong>
<strong v-else-if="statusCheckin && inQueue">
ลงเวลาเขางาน (ระบบกำลงประมวลผล)
</strong>
<strong v-else-if="statusCheckin && !inQueue">
ลงเวลาเขางาน
</strong>
<strong v-else> ลงเวลาออกงาน </strong>
</div>
</div>
</div>
<div class="col-12 text-grey-9">
<div class="col-12 row justify-center">
<div class="col-12 row q-pt-md justify-center gt-xs">
<div
class="col-xs-12 col-sm-10 text-h6 text-center text-weight-bold"
>
{{ date2Thai(Thai) }}
</div>
<div class="row col-12 justify-center q-py-sm">
<div class="colunm">
<div class="text-h3 text-weight-bold">
{{ formattedH }}<span class="q-ma-md">:</span>
</div>
</div>
<div class="colunm">
<div class="text-h3 text-weight-bold">
{{ formattedM }}<span class="q-ma-md">:</span>
</div>
</div>
<div class="colunm">
<div class="text-h3 text-weight-bold">{{ formattedS }}</div>
</div>
</div>
</div>
<div
class="col-xs-12 col-md-11 q-pa-md q-col-gutter-md row q-pt-lg"
>
<div class="col-xs-12 col-sm-8 gt-xs">
<div class="col-12">
<MapCheck
v-if="$q.screen.gt.xs"
ref="mapRef"
@update:location="updateLocation"
/>
</div>
</div>
<div class="col-xs-12 col-sm-4">
<q-card
:class="
$q.screen.xs ? 'card-container-xs' : 'card-container'
"
>
<div
v-if="!cameraIsOn && img == null"
class="preview-placeholder"
@click="() => !isDisabledCheckTime && openCamera()"
>
<div class="text-center">
<q-icon
name="mdi-camera"
color="blue-grey-3"
size="100px"
class="center-icon"
/>
</div>
</div>
<div class="col-12 row items-center">
<!-- แสดงกลองตอนกดถายภาพ -->
<Camera
:resolution="{ width: photoWidth, height: photoHeight }"
ref="camera"
:autoplay="false"
:style="!img ? 'display: block' : 'display: none'"
/>
<!-- แสดงรปเมอกด capture -->
<div v-if="img" class="image-container">
<q-img :src="img" class="image-element"></q-img>
</div>
<div v-if="cameraIsOn">
<div
v-if="$q.screen.gt.xs"
class="absolute-bottom-right q-ma-md"
>
<q-btn
v-if="img == null"
round
push
icon="photo_camera"
size="md"
color="positive"
@click="capturePhoto"
/>
<q-btn
v-else
round
push
icon="refresh"
size="md"
color="negative"
@click="refreshPhoto"
/>
</div>
<div v-else>
<div
class="absolute-bottom text-subtitle2 text-center q-py-sm"
style="background: #00000021"
>
<q-btn
round
v-if="img == null"
icon="photo_camera"
size="18px"
style="background: #263238; color: white"
@click="capturePhoto"
unelevated
/>
<q-btn
v-else
round
icon="refresh"
size="18px"
style="background: #263238; color: white"
@click="refreshPhoto"
/>
</div>
</div>
</div>
</div>
</q-card>
</div>
<!-- กรอกขอม หนามอถ -->
<div class="col-12 row q-col-gutter-y-md" v-if="$q.screen.xs">
<div class="col-12" v-if="!isDisabledCheckTime">
<q-card
flat
bordered
class="row col-12"
style="border-radius: 10px"
>
<q-input
v-model="remark"
filled
label="กรอกหมายเหตุ"
input-style="border-radius: 10px 10px 0 0;"
class="col-12"
bg-color="white"
/>
<div class="col-12"><q-separator /></div>
<q-btn-toggle
v-model="workplace"
@update:model-value="updateWorkplace"
spread
no-caps
toggle-color="blue-grey-10"
color="white"
text-color="black"
class="col-12"
style="min-height: 3em"
size="15px"
:options="[
{ label: 'ในสถานที่', value: 'in-place' },
{ label: 'นอกสถานที่', value: 'off-site' },
]"
/>
</q-card>
</div>
<div class="col-12" v-if="workplace == 'off-site'">
<q-card class="col-12">
<q-select
ref="modelRef"
filled
v-model="model"
:options="options"
label="เลือกสถานที่"
bg-color="white"
:rules="[(val:string) => !!val || 'กรุณาระบุสถานที่']"
lazy-rules
@update:model-value="selectLocation()"
hide-bottom-space
option-value="value"
option-label="text"
emit-value
map-options
/>
</q-card>
</div>
<div
class="col-12"
v-if="model == 'อื่นๆ' && workplace === 'off-site'"
>
<q-card class="col-12">
<q-input
ref="useLocationRef"
filled
v-model="useLocation"
label="ระบุสถานที่"
bg-color="white"
:rules="[(val:string) => !!val || 'กรุณาระบุสถานที่']"
hide-bottom-space
lazy-rules
/>
</q-card>
</div>
<div class="col-12" v-if="$q.screen.xs">
<MapCheck ref="mapRef" @update:location="updateLocation" />
</div>
</div>
<!-- กรอกขอม หนามอถ -->
<div
v-if="!isDisabledCheckTime"
class="col-xs-12 col-sm-12 items-center gt-xs"
>
<q-card
bordered
flat
:class="
$q.screen.gt.xs
? 'q-px-md q-py-sm row items-center shadow-0'
: 'q-pa-md row items-center shadow-0'
"
>
<div class="text-weight-bold">สถานททำงาน</div>
<div
:class="
$q.screen.gt.xs
? 'row q-gutter-md q-pl-md col-sm-6 col-md-3'
: 'column col-12'
"
>
<q-radio
v-model="workplace"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="in-place"
label="ในสถานที่"
@update:model-value="updateWorkplace"
/>
<q-radio
v-model="workplace"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="off-site"
label="นอกสถานที่"
@update:model-value="updateWorkplace"
/>
</div>
<div class="col-sm-12 col-md-8">
<div class="row items-top">
<div
class="col-xs-12 col-sm-6 col-md-6"
v-if="workplace == 'off-site'"
>
<q-select
ref="modelRef"
dense
class="q-ml-md"
outlined
v-model="model"
:options="options"
prefix="ระบุสถานที่ :"
:rules="[(val:string) => !!val || 'กรุณาระบุสถานที่']"
lazy-rules
@update:model-value="selectLocation()"
hide-bottom-space
option-value="value"
option-label="text"
emit-value
map-options
/>
</div>
<div
class="col-xs-12 col-sm-6 col-md-6"
v-if="model == 'อื่นๆ' && workplace === 'off-site'"
>
<q-input
ref="useLocationRef"
dense
class="q-ml-md"
outlined
v-model="useLocation"
label="ระบุสถานที่"
hide-bottom-space
:rules="[(val:string) => !!val || 'กรุณาระบุสถานที่']"
lazy-rules
/>
</div>
</div>
</div>
</q-card>
</div>
<div
v-if="!isDisabledCheckTime"
class="col-xs-12 col-sm-12 gt-xs"
>
<q-card
bordered
flat
:class="
$q.screen.gt.xs
? 'q-px-md q-py-sm row items-center shadow-0'
: 'q-pa-md row items-center shadow-0'
"
>
<div class="text-weight-bold">หมายเหต</div>
<div class="col-12 q-pt-sm">
<q-input outlined v-model="remark" lazy-rules dense />
</div>
</q-card>
</div>
</div>
</div>
<div
v-if="!isDisabledCheckTime && $q.screen.gt.xs"
class="col-12 text-right"
>
<q-separator />
<div class="col-12 q-pa-md">
<p
:class="
$q.screen.gt.xs
? 'text-red text-caption '
: 'text-red text-caption text-center'
"
>
*หมายเหต คลกลงเวลาเขางานแลวระบบจะลงเวลาทนท
</p>
<q-btn
:label="
!statusCheckin && inQueue
? 'ลงเวลาเข้างาน (ระบบกำลังประมวลผล)'
: statusCheckin && inQueue
? 'ลงเวลาออกงาน (ระบบกำลังประมวลผล)'
: statusCheckin && !inQueue
? 'ลงเวลาเข้างาน'
: 'ลงเวลาออกงาน'
"
:color="
img == null
? 'grey-6'
: !statusCheckin && inQueue
? 'primary'
: statusCheckin && inQueue
? 'red-8'
: statusCheckin && !inQueue
? 'primary'
: 'red-8'
"
push
size="18px"
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
:disable="disabledBtn ? true : camera && img ? false : true"
@click="validateForm"
:loading="inQueue"
/>
</div>
</div>
<div v-if="isDisabledCheckTime && $q.screen.gt.xs" class="col-12">
<q-separator />
<div class="text-red q-pa-md">*{{ msgCheckTime }}</div>
</div>
</div>
</q-card>
</div>
</div>
</q-page>
<!-- top page sticky หนามอถ-->
<q-page-sticky expand position="top" v-if="$q.screen.xs">
<div
class="row q-col-gutter-sm col bg-white col-12 q-pa-md"
style="height: 100px"
v-if="isLoadingCheckTime"
>
<div class="col-7">
<div class="row col-12">
<q-skeleton type="text" width="90%" />
<q-skeleton type="text" width="50%" class="text-subtitle1" />
</div>
</div>
<div class="col-5">
<div class="row col-12">
<q-skeleton type="QBtn" width="100%" />
</div>
</div>
</div>
<div v-else :class="getClassXS(statusCheckin)">
<div class="col">
<div class="row col-12 justify-right items-center text-subtitle1">
<strong v-if="!statusCheckin && inQueue">
ลงเวลาออกงาน (ระบบกำลังประมวลผล)
</strong>
<strong v-else-if="statusCheckin && inQueue">
ลงเวลาเขางาน (ระบบกำลงประมวลผล)
</strong>
<strong v-else-if="statusCheckin && !inQueue"> ลงเวลาเขางาน </strong>
<strong v-else> ลงเวลาออกงาน </strong>
</div>
<div
class="text-white text-subtitle1 row col-12"
style="line-height: 1.5rem"
>
{{ date2Thai(Thai) }}
</div>
</div>
<div class="col-6 row items-center">
<div class="row col-12 justify-end">
<div class="colunm">
<div class="text-h4 text-weight-bold">
{{ formattedH }}<span class="q-ma-xs">:</span>
</div>
</div>
<div class="colunm">
<div class="text-h4 text-weight-bold">
{{ formattedM }}<span class="q-ma-xs">:</span>
</div>
</div>
<div class="colunm">
<div class="text-h4 text-weight-bold">{{ formattedS }}</div>
</div>
</div>
</div>
</div>
</q-page-sticky>
<!-- footer หนามอถ-->
<q-footer reveal v-if="$q.screen.xs" class="bg-grey-2">
<q-separator />
<div v-if="!isDisabledCheckTime" class="col-12 q-pa-md">
<p
:class="
$q.screen.gt.xs
? 'text-red text-caption '
: 'text-red text-caption text-center'
"
>
*หมายเหต คลกลงเวลาเขางานแลวระบบจะลงเวลาทนท
</p>
<q-btn
:label="
!statusCheckin && inQueue
? 'ลงเวลาเข้างาน (ระบบกำลังประมวลผล)'
: statusCheckin && inQueue
? 'ลงเวลาออกงาน (ระบบกำลังประมวลผล)'
: statusCheckin && !inQueue
? 'ลงเวลาเข้างาน'
: 'ลงเวลาออกงาน'
"
:color="
img == null
? 'grey-6'
: !statusCheckin && inQueue
? 'primary'
: statusCheckin && inQueue
? 'red-8'
: statusCheckin && !inQueue
? 'primary'
: 'red-8'
"
push
size="18px"
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
:disable="disabledBtn ? true : camera && img ? false : true"
@click="validateForm"
:loading="inQueue"
/>
</div>
<div v-if="isDisabledCheckTime" class="col-12">
<q-separator />
<div class="text-red q-pa-md text-left">*{{ msgCheckTime }}</div>
</div>
</q-footer>
<!-- แสดงการลงเวลา -->
<q-dialog v-model="modalTime" persistent>
<q-card class="full-width cardNone">
<div class="col-12 row items-center q-px-md q-py-sm bg-grey-2">
<div class="text-body1 text-center col-12 text-weight-bold">
<span v-if="statusCheckin">ลงเวลาเข้างานของคุณ</span>
<span v-else>ลงเวลาออกงานของคุณ</span>
</div>
</div>
<q-card-section class="row col-12 justify-center">
<div
class="rounded-borders q-pa-md col-11"
:class="
!statusCheckin && inQueue
? 'bg-primary'
: statusCheckin && inQueue
? 'bg-red-8'
: statusCheckin && !inQueue
? 'bg-primary'
: 'bg-red-8'
"
>
<div
class="col-12 text-subtitle1 text-center text-white text-weight-medium"
>
{{ date2Thai(Thai) }}
</div>
<div class="row col-12 justify-center q-pt-sm">
<!-- <div class="text-h3 text-white text-weight-bold"></div> -->
<div class="text-h3 text-white text-weight-bold">
{{ timeChickin }}
</div>
</div>
</div>
<div class="col-12 text-center row q-pt-md">
<div class="col-12 text-subtitle1 text-weight-medium text-secondary">
นทใกลเคยง {{ formLocation.POI ?? formLocation.POI }}
</div>
<div class="col-12 text-subtitle1 text-weight-medium text-secondary">
{{ location }}
</div>
<div class="col-12 text-grey-7">
{{ formLocation.lat }} , {{ formLocation.lng }}
</div>
</div>
</q-card-section>
<q-card-actions align="center" class="q-mb-md row">
<q-btn
class="col-xs-11 col-sm-6"
push
label="ตกลง"
color="secondary"
v-close-popup
@click="onClickConfirm"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style scoped>
.q-card.cardImg:hover {
border: 1px solid #02a998 !important;
}
.center-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.card-container {
position: relative;
overflow: hidden;
height: autu; /* Adjust as needed */
background: #f5f5f5;
height: 35vh !important;
box-shadow: none !important;
border: 1px solid #ededed;
}
.card-container-xs {
position: relative;
overflow: hidden;
height: autu; /* Adjust as needed */
background: #ffffff;
height: 35vh !important;
border-radius: 10px;
}
.image-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.image-element {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px; /* Adjust as needed */
}
.preview-placeholder {
width: 100%;
height: 100%;
}
.bg-topOut {
background: rgb(39, 50, 56);
background: linear-gradient(
175deg,
rgba(39, 50, 56, 1) 76%,
rgba(198, 40, 40, 1) 100%
);
}
.bg-topIn {
background: rgb(39, 50, 56);
background: linear-gradient(
175deg,
rgba(39, 50, 56, 1) 76%,
rgba(2, 169, 152, 1) 100%
);
}
</style>