fix fack location complated
This commit is contained in:
parent
859d74056a
commit
e8ddd43209
4 changed files with 287 additions and 115 deletions
|
|
@ -16,7 +16,32 @@ const mapElement = ref<HTMLElement | null>(null)
|
||||||
const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected'])
|
const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected'])
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
|
|
||||||
const { validateLocation, showMockWarning } = useLocationValidation()
|
const { validateLocation } = useLocationValidation()
|
||||||
|
const MOCK_CHECK_DELAY_MS = 800
|
||||||
|
|
||||||
|
function wait(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPositionAsync(options?: PositionOptions) {
|
||||||
|
return new Promise<GeolocationPosition>((resolve, reject) => {
|
||||||
|
navigator.geolocation.getCurrentPosition(resolve, reject, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPositionForValidation(fallback: GeolocationPosition) {
|
||||||
|
await wait(MOCK_CHECK_DELAY_MS)
|
||||||
|
try {
|
||||||
|
return await getCurrentPositionAsync({
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000,
|
||||||
|
maximumAge: 0,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Keep the original reading if the second sampling fails.
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateLocation(latitude: number, longitude: number, namePOI: string) {
|
function updateLocation(latitude: number, longitude: number, namePOI: string) {
|
||||||
// ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props
|
// ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props
|
||||||
|
|
@ -24,6 +49,7 @@ function updateLocation(latitude: number, longitude: number, namePOI: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const poiPlaceName = ref<string>('') // ชื่อพื้นที่ใกล้เคียง
|
const poiPlaceName = ref<string>('') // ชื่อพื้นที่ใกล้เคียง
|
||||||
|
const isMapReady = ref<boolean>(false)
|
||||||
|
|
||||||
// Replace ArcGIS api key
|
// Replace ArcGIS api key
|
||||||
const apiKey = ref<string>(
|
const apiKey = ref<string>(
|
||||||
|
|
@ -68,6 +94,7 @@ async function initializeMap(position: GeolocationPosition) {
|
||||||
components: [], // Empty array to remove all default UI components
|
components: [], // Empty array to remove all default UI components
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
isMapReady.value = true
|
||||||
|
|
||||||
// ตำแหน่งของผู้ใช้
|
// ตำแหน่งของผู้ใช้
|
||||||
const userPoint = new Point({ longitude, latitude })
|
const userPoint = new Point({ longitude, latitude })
|
||||||
|
|
@ -180,7 +207,9 @@ async function initializeMap(position: GeolocationPosition) {
|
||||||
updateLocation(latitude, longitude, poiPlaceName.value)
|
updateLocation(latitude, longitude, poiPlaceName.value)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// console.error('Error fetching points of interest:', error)
|
// Keep map visible even when POI lookup fails.
|
||||||
|
poiPlaceName.value = 'ไม่พบข้อมูล'
|
||||||
|
updateLocation(latitude, longitude, poiPlaceName.value)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -213,13 +242,16 @@ const requestLocationPermission = () => {
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
async (position) => {
|
async (position) => {
|
||||||
// Validate location first
|
const sampledPosition = await getPositionForValidation(position)
|
||||||
const validationResult = validateLocation(position)
|
|
||||||
|
|
||||||
// Always emit mockDetected event (regardless of result)
|
// Validate location first
|
||||||
|
const validationResult = validateLocation(sampledPosition)
|
||||||
|
|
||||||
|
// Do not block map preview on initial mock detection.
|
||||||
|
// The hard-stop warning is enforced at submit time in HomeView.
|
||||||
if (validationResult.isMockDetected) {
|
if (validationResult.isMockDetected) {
|
||||||
showMockWarning(validationResult)
|
locationGranted.value = true
|
||||||
emit('mockDetected', validationResult)
|
emit('locationStatus', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for critical errors (invalid coordinates) that prevent showing location
|
// Check for critical errors (invalid coordinates) that prevent showing location
|
||||||
|
|
@ -234,11 +266,11 @@ const requestLocationPermission = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission granted based on mock detection
|
// Permission granted for map preview state.
|
||||||
locationGranted.value = !validationResult.isMockDetected
|
locationGranted.value = true
|
||||||
emit('locationStatus', !validationResult.isMockDetected)
|
emit('locationStatus', true)
|
||||||
|
|
||||||
const { latitude, longitude } = position.coords
|
const { latitude, longitude } = sampledPosition.coords
|
||||||
// console.log('Current position:', latitude, longitude)
|
// console.log('Current position:', latitude, longitude)
|
||||||
|
|
||||||
if (!latitude || !longitude) {
|
if (!latitude || !longitude) {
|
||||||
|
|
@ -248,7 +280,7 @@ const requestLocationPermission = () => {
|
||||||
|
|
||||||
// Center map on user's location if map is initialized
|
// Center map on user's location if map is initialized
|
||||||
if (privacyStore.isAccepted) {
|
if (privacyStore.isAccepted) {
|
||||||
await initializeMap(position)
|
await initializeMap(sampledPosition)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|
@ -279,7 +311,7 @@ const requestLocationPermission = () => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ enableHighAccuracy: true }
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +323,7 @@ defineExpose({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Loading skeleton -->
|
<!-- Loading skeleton -->
|
||||||
<div v-if="!poiPlaceName" class="col-12">
|
<div v-if="!isMapReady" class="col-12">
|
||||||
<q-skeleton
|
<q-skeleton
|
||||||
:height="$q.screen.gt.xs ? '35vh' : '45px'"
|
:height="$q.screen.gt.xs ? '35vh' : '45px'"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|
@ -301,7 +333,7 @@ defineExpose({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
v-show="poiPlaceName"
|
v-show="isMapReady"
|
||||||
bordered
|
bordered
|
||||||
flat
|
flat
|
||||||
class="col-12 bg-grey-2 shadow-0"
|
class="col-12 bg-grey-2 shadow-0"
|
||||||
|
|
@ -323,7 +355,7 @@ defineExpose({
|
||||||
>
|
>
|
||||||
พื้นที่ใกล้เคียง
|
พื้นที่ใกล้เคียง
|
||||||
<span class="q-px-sm">:</span>
|
<span class="q-px-sm">:</span>
|
||||||
{{ poiPlaceName }}
|
{{ poiPlaceName || 'ไม่พบข้อมูล' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -340,7 +372,7 @@ defineExpose({
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ poiPlaceName }}
|
{{ poiPlaceName || 'ไม่พบข้อมูล' }}
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ export const VALIDATION_CONFIG = {
|
||||||
POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection
|
POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection
|
||||||
MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock)
|
MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock)
|
||||||
SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number)
|
SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number)
|
||||||
|
// Geographic service area (Thailand) to reduce spoofing risk from remote fake coordinates.
|
||||||
|
SERVICE_AREA: {
|
||||||
|
MIN_LAT: 5.0,
|
||||||
|
MAX_LAT: 21.0,
|
||||||
|
MIN_LON: 97.0,
|
||||||
|
MAX_LON: 106.0,
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function useLocationValidation() {
|
export function useLocationValidation() {
|
||||||
|
|
@ -41,6 +48,8 @@ export function useLocationValidation() {
|
||||||
'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง',
|
'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง',
|
||||||
SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง',
|
SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง',
|
||||||
DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง',
|
DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง',
|
||||||
|
OUT_OF_SERVICE_AREA:
|
||||||
|
'ตรวจพบตำแหน่งนอกพื้นที่ใช้งานของระบบ กรุณาปิดแอปจำลองตำแหน่งและลองใหม่',
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousPositions = ref<PositionSnapshot[]>([])
|
const previousPositions = ref<PositionSnapshot[]>([])
|
||||||
|
|
@ -132,6 +141,16 @@ export function useLocationValidation() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isWithinServiceArea = (lat: number, lon: number): boolean => {
|
||||||
|
const area = VALIDATION_CONFIG.SERVICE_AREA
|
||||||
|
return (
|
||||||
|
lat >= area.MIN_LAT &&
|
||||||
|
lat <= area.MAX_LAT &&
|
||||||
|
lon >= area.MIN_LON &&
|
||||||
|
lon <= area.MAX_LON
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Main validation function
|
// Main validation function
|
||||||
const validateLocation = (
|
const validateLocation = (
|
||||||
position: GeolocationPosition
|
position: GeolocationPosition
|
||||||
|
|
@ -149,6 +168,12 @@ export function useLocationValidation() {
|
||||||
mockIndicators += 3
|
mockIndicators += 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.1 Service-area validation (critical for remote spoofed coordinates)
|
||||||
|
if (!isWithinServiceArea(latitude, longitude)) {
|
||||||
|
errors.push(errorMessages.OUT_OF_SERVICE_AREA)
|
||||||
|
mockIndicators += 3
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Timestamp validation
|
// 2. Timestamp validation
|
||||||
if (!validateTimestamp(timestamp)) {
|
if (!validateTimestamp(timestamp)) {
|
||||||
errors.push(errorMessages.STALE_TIMESTAMP)
|
errors.push(errorMessages.STALE_TIMESTAMP)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,56 @@ import { logout } from '@/plugins/auth'
|
||||||
import { format, utcToZonedTime } from 'date-fns-tz'
|
import { format, utcToZonedTime } from 'date-fns-tz'
|
||||||
|
|
||||||
export const useCounterMixin = defineStore('mixin', () => {
|
export const useCounterMixin = defineStore('mixin', () => {
|
||||||
|
let activeErrorDialog: any = null
|
||||||
|
|
||||||
|
const clearActiveErrorDialog = () => {
|
||||||
|
if (!activeErrorDialog) return
|
||||||
|
try {
|
||||||
|
if (typeof activeErrorDialog.hide === 'function') {
|
||||||
|
activeErrorDialog.hide()
|
||||||
|
} else if (typeof activeErrorDialog.destroy === 'function') {
|
||||||
|
activeErrorDialog.destroy()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activeErrorDialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSingleErrorDialog = (
|
||||||
|
q: any,
|
||||||
|
message: string,
|
||||||
|
onCancel?: () => void | Promise<void>
|
||||||
|
) => {
|
||||||
|
clearActiveErrorDialog()
|
||||||
|
|
||||||
|
const dialog = q.dialog({
|
||||||
|
component: CustomComponent,
|
||||||
|
componentProps: {
|
||||||
|
title: `พบข้อผิดพลาด`,
|
||||||
|
message,
|
||||||
|
icon: 'warning',
|
||||||
|
color: 'red',
|
||||||
|
onlycancel: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
activeErrorDialog = dialog
|
||||||
|
|
||||||
|
dialog.onDismiss(() => {
|
||||||
|
if (activeErrorDialog === dialog) {
|
||||||
|
activeErrorDialog = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (onCancel) {
|
||||||
|
dialog.onCancel(() => {
|
||||||
|
void onCancel()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) {
|
function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) {
|
||||||
if (srcDate == null) {
|
if (srcDate == null) {
|
||||||
return null
|
return null
|
||||||
|
|
@ -133,53 +183,30 @@ export const useCounterMixin = defineStore('mixin', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageError = (q: any, e: any = '', msg: string = '') => {
|
const messageError = (q: any, e: any = '', msg: string = '') => {
|
||||||
// q.dialog.hide();
|
// Keep only one active warning popup to prevent dialog overlap.
|
||||||
if (e.response !== undefined) {
|
if (e.response !== undefined) {
|
||||||
if (e.response.data.status !== undefined) {
|
if (e.response.data.status !== undefined) {
|
||||||
if (e.response.data.status == 401) {
|
if (e.response.data.status == 401) {
|
||||||
//invalid_token
|
//invalid_token
|
||||||
q.dialog({
|
openSingleErrorDialog(
|
||||||
component: CustomComponent,
|
q,
|
||||||
componentProps: {
|
'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง',
|
||||||
title: `พบข้อผิดพลาด`,
|
async () => {
|
||||||
message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`,
|
showLoader()
|
||||||
icon: 'warning',
|
await logout()
|
||||||
color: 'red',
|
setTimeout(() => {
|
||||||
onlycancel: true,
|
hideLoader()
|
||||||
},
|
}, 1000)
|
||||||
}).onCancel(async () => {
|
}
|
||||||
showLoader()
|
)
|
||||||
await logout()
|
|
||||||
setTimeout(() => {
|
|
||||||
hideLoader()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
const message = e.response.data.result ?? e.response.data.message
|
const message = e.response.data.result ?? e.response.data.message
|
||||||
q.dialog({
|
openSingleErrorDialog(q, `${message}`)
|
||||||
component: CustomComponent,
|
|
||||||
componentProps: {
|
|
||||||
title: `พบข้อผิดพลาด`,
|
|
||||||
message: `${message}`,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (e.response.status == 401) {
|
if (e.response.status == 401) {
|
||||||
if (msg !== '') {
|
if (msg !== '') {
|
||||||
q.dialog({
|
openSingleErrorDialog(q, msg, async () => {
|
||||||
component: CustomComponent,
|
|
||||||
componentProps: {
|
|
||||||
title: `พบข้อผิดพลาด`,
|
|
||||||
message: msg,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
}).onCancel(async () => {
|
|
||||||
showLoader()
|
showLoader()
|
||||||
await logout()
|
await logout()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -188,70 +215,35 @@ export const useCounterMixin = defineStore('mixin', () => {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
//invalid_token
|
//invalid_token
|
||||||
q.dialog({
|
openSingleErrorDialog(
|
||||||
component: CustomComponent,
|
q,
|
||||||
componentProps: {
|
'ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง',
|
||||||
title: `พบข้อผิดพลาด`,
|
async () => {
|
||||||
message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`,
|
showLoader()
|
||||||
icon: 'warning',
|
await logout()
|
||||||
color: 'red',
|
setTimeout(() => {
|
||||||
onlycancel: true,
|
hideLoader()
|
||||||
},
|
}, 1000)
|
||||||
}).onCancel(async () => {
|
}
|
||||||
showLoader()
|
)
|
||||||
await logout()
|
|
||||||
setTimeout(() => {
|
|
||||||
hideLoader()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} else if (e.response.data.successful === false) {
|
} else if (e.response.data.successful === false) {
|
||||||
q.dialog({
|
openSingleErrorDialog(q, e.response.data.message)
|
||||||
component: CustomComponent,
|
|
||||||
componentProps: {
|
|
||||||
title: `พบข้อผิดพลาด`,
|
|
||||||
message: e.response.data.message,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
q.dialog({
|
openSingleErrorDialog(
|
||||||
component: CustomComponent,
|
q,
|
||||||
componentProps: {
|
'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์'
|
||||||
title: `พบข้อผิดพลาด`,
|
)
|
||||||
message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (msg !== '') {
|
if (msg !== '') {
|
||||||
return q.dialog({
|
return openSingleErrorDialog(q, msg)
|
||||||
component: CustomComponent,
|
|
||||||
componentProps: {
|
|
||||||
title: `พบข้อผิดพลาด`,
|
|
||||||
message: msg,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
q.dialog({
|
openSingleErrorDialog(
|
||||||
component: CustomComponent,
|
q,
|
||||||
componentProps: {
|
'ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์'
|
||||||
title: `พบข้อผิดพลาด`,
|
)
|
||||||
message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`,
|
|
||||||
icon: 'warning',
|
|
||||||
color: 'red',
|
|
||||||
onlycancel: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import config from '@/app.config'
|
||||||
import http from '@/plugins/http'
|
import http from '@/plugins/http'
|
||||||
import { useCounterMixin } from '@/stores/mixin'
|
import { useCounterMixin } from '@/stores/mixin'
|
||||||
import { usePermissions } from '@/composables/usePermissions'
|
import { usePermissions } from '@/composables/usePermissions'
|
||||||
|
import { useLocationValidation } from '@/composables/useLocationValidation'
|
||||||
import { usePrivacyStore } from '@/stores/privacy'
|
import { usePrivacyStore } from '@/stores/privacy'
|
||||||
|
|
||||||
import type { FormRef, OptionReason } from '@/interface/response/checkin'
|
import type { FormRef, OptionReason } from '@/interface/response/checkin'
|
||||||
|
|
@ -18,7 +19,10 @@ const mixin = useCounterMixin()
|
||||||
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const { checkPrivacyAccepted } = usePermissions()
|
const { checkPrivacyAccepted } = usePermissions()
|
||||||
|
const { validateLocation, showMockWarning, resetValidation } =
|
||||||
|
useLocationValidation()
|
||||||
const privacyStore = usePrivacyStore()
|
const privacyStore = usePrivacyStore()
|
||||||
|
const MOCK_CHECK_DELAY_MS = 800
|
||||||
|
|
||||||
const modalTime = ref<boolean>(false) // Dailog ลงเวลาเข้างานของคุณ
|
const modalTime = ref<boolean>(false) // Dailog ลงเวลาเข้างานของคุณ
|
||||||
const checkStatus = ref<string>('')
|
const checkStatus = ref<string>('')
|
||||||
|
|
@ -120,14 +124,110 @@ async function updateLocation(
|
||||||
*/
|
*/
|
||||||
function onLocationStatus(status: boolean) {
|
function onLocationStatus(status: boolean) {
|
||||||
locationGranted.value = status
|
locationGranted.value = status
|
||||||
|
if (status) {
|
||||||
|
isMockLocationDetected.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* รับค่า mock location detection จาก AscGISMap
|
* รับค่า mock location detection จาก AscGISMap
|
||||||
*/
|
*/
|
||||||
function onMockDetected(result: any) {
|
function onMockDetected(result: any) {
|
||||||
isMockLocationDetected.value = true
|
isMockLocationDetected.value = !!result?.isMockDetected
|
||||||
disabledBtn.value = true
|
disabledBtn.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLocationForRetry() {
|
||||||
|
locationGranted.value = false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revalidateLocationBeforeSubmit() {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
messageError(
|
||||||
|
$q,
|
||||||
|
'',
|
||||||
|
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เบราว์เซอร์ของคุณไม่รองรับ Geolocation'
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If previous attempt was mock, clear history so fresh GPS is not compared
|
||||||
|
// against spoofed coordinates and incorrectly flagged as impossible speed.
|
||||||
|
if (isMockLocationDetected.value) {
|
||||||
|
resetValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = await getDelayedFreshPosition()
|
||||||
|
|
||||||
|
const validationResult = validateLocation(position)
|
||||||
|
|
||||||
|
if (validationResult.isMockDetected) {
|
||||||
|
isMockLocationDetected.value = true
|
||||||
|
disabledBtn.value = false
|
||||||
|
resetValidation()
|
||||||
|
resetLocationForRetry()
|
||||||
|
showMockWarning(validationResult)
|
||||||
|
mapRef.value?.requestLocationPermission()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationResult.errors.length > 0) {
|
||||||
|
disabledBtn.value = false
|
||||||
|
resetValidation()
|
||||||
|
resetLocationForRetry()
|
||||||
|
messageError($q, '', validationResult.errors[0])
|
||||||
|
mapRef.value?.requestLocationPermission()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
locationGranted.value = true
|
||||||
|
isMockLocationDetected.value = false
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
disabledBtn.value = false
|
||||||
|
resetLocationForRetry()
|
||||||
|
messageError(
|
||||||
|
$q,
|
||||||
|
'',
|
||||||
|
'ไม่สามารถตรวจสอบตำแหน่งล่าสุดก่อนลงเวลาได้ กรุณาลองใหม่อีกครั้ง'
|
||||||
|
)
|
||||||
|
mapRef.value?.requestLocationPermission()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const location = ref<string>('') // พื้นที่ใกล้เคียง
|
const location = ref<string>('') // พื้นที่ใกล้เคียง
|
||||||
|
|
@ -348,7 +448,7 @@ const objectRef: FormRef = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** function ตรวจสอบค่าว่างของ input*/
|
/** function ตรวจสอบค่าว่างของ input*/
|
||||||
function validateForm() {
|
async function validateForm() {
|
||||||
const hasError = []
|
const hasError = []
|
||||||
for (const key in objectRef) {
|
for (const key in objectRef) {
|
||||||
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
||||||
|
|
@ -360,6 +460,11 @@ function validateForm() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasError.every((result) => result === true)) {
|
if (hasError.every((result) => result === true)) {
|
||||||
|
const isLocationValid = await revalidateLocationBeforeSubmit()
|
||||||
|
if (!isLocationValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (statusCheckin.value == false) {
|
if (statusCheckin.value == false) {
|
||||||
getCheck()
|
getCheck()
|
||||||
} else if (statusCheckin.value) {
|
} else if (statusCheckin.value) {
|
||||||
|
|
@ -397,6 +502,12 @@ async function confirm() {
|
||||||
mapRef.value?.requestLocationPermission()
|
mapRef.value?.requestLocationPermission()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLocationValid = await revalidateLocationBeforeSubmit()
|
||||||
|
if (!isLocationValid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
disabledBtn.value = true
|
disabledBtn.value = true
|
||||||
showLoader()
|
showLoader()
|
||||||
const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง
|
const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง
|
||||||
|
|
@ -1016,7 +1127,13 @@ watch(
|
||||||
push
|
push
|
||||||
size="18px"
|
size="18px"
|
||||||
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
|
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
|
||||||
:disable="disabledBtn || !locationGranted || isMockLocationDetected ? true : camera && img ? false : true"
|
:disable="
|
||||||
|
disabledBtn || !locationGranted || isMockLocationDetected
|
||||||
|
? true
|
||||||
|
: camera && img
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
"
|
||||||
@click="validateForm"
|
@click="validateForm"
|
||||||
:loading="inQueue"
|
:loading="inQueue"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1131,7 +1248,13 @@ watch(
|
||||||
push
|
push
|
||||||
size="18px"
|
size="18px"
|
||||||
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
|
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
|
||||||
:disable="disabledBtn || !locationGranted || isMockLocationDetected ? true : camera && img ? false : true"
|
:disable="
|
||||||
|
disabledBtn || !locationGranted || isMockLocationDetected
|
||||||
|
? true
|
||||||
|
: camera && img
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
"
|
||||||
@click="validateForm"
|
@click="validateForm"
|
||||||
:loading="inQueue"
|
:loading="inQueue"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue