diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 7ed48a7..7651b7c 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -5,51 +5,20 @@ import axios from 'axios' import { useCounterMixin } from '@/stores/mixin' import { useQuasar } from 'quasar' import { usePrivacyStore } from '@/stores/privacy' -import { useLocationValidation } from '@/composables/useLocationValidation' const mixin = useCounterMixin() const { messageError } = mixin const privacyStore = usePrivacyStore() -// import type { LocationObject } from '@/interface/index/Main' -const mapElement = ref(null) -const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected']) +const emit = defineEmits(['update:location']) const $q = useQuasar() -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((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) { // ส่ง event ไปยัง parent component เพื่ออัพเดทค่า props emit('update:location', latitude, longitude, namePOI) } const poiPlaceName = ref('') // ชื่อพื้นที่ใกล้เคียง -const isMapReady = ref(false) // Replace ArcGIS api key const apiKey = ref( @@ -58,7 +27,7 @@ const apiKey = ref( ) const zoomMap = ref(18) -async function initializeMap(position: GeolocationPosition) { +async function initializeMap() { try { // Load modules of ArcGIS loadModules([ @@ -69,149 +38,159 @@ async function initializeMap(position: GeolocationPosition) { 'esri/Graphic', 'esri/layers/TileLayer', ]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => { + // Set apiKey + // esriConfig.apiKey = + // 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP' + + // Create a FeatureLayer using a custom server URL + // const hillshadeLayer = new TileLayer({ + // url: `https://bmagis.bangkok.go.th/arcgis/rest/services/cache/BMA_3D_2D_Cache/MapServer`, + // }) + const map = new Map({ basemap: 'streets', + // basemap: 'arcgis-topographic', + // layers: [hillshadeLayer], }) - const { latitude, longitude } = position.coords + navigator.geolocation.getCurrentPosition(async (position) => { + const { latitude, longitude } = position.coords - const mapView = new MapView({ - container: 'mapViewDisplay', - map: map, - center: { - latitude: latitude, - longitude: longitude, - }, // Set the initial map center current position + const mapView = new MapView({ + container: 'mapViewDisplay', + map: map, + center: { + latitude: latitude, + longitude: longitude, + }, // Set the initial map center current position - zoom: zoomMap.value, - constraints: { - snapToZoom: false, // Disables snapping to the zoom level - minZoom: zoomMap.value, // Set minimum zoom level - maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) - }, + zoom: zoomMap.value, + constraints: { + snapToZoom: false, // Disables snapping to the zoom level + minZoom: zoomMap.value, // Set minimum zoom level + maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) + }, - ui: { - components: [], // Empty array to remove all default UI components - }, - }) - isMapReady.value = true - - // ตำแหน่งของผู้ใช้ - const userPoint = new Point({ longitude, latitude }) - const userSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/red.png', - width: '32px', - height: '32px', - } - const userGraphic = new Graphic({ - geometry: userPoint, - symbol: userSymbol, - }) - mapView.graphics.add(userGraphic) - // Get POI place ยิงไปขอที่ server ของกทม.ก่อน - await axios - .get( - 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, - token: apiKey.value, - }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) - - updateLocation(latitude, longitude, poiPlaceName.value) + ui: { + components: [], // Empty array to remove all default UI components + }, }) - .catch(async (error) => { - // console.error('Error fetching points of interest:', error) - // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token - await axios - .get( - 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', - { - params: { - f: 'json', // Format JSON response - distance: 2000, - category: 'POI', - location: { - spatialReference: { wkid: 4326 }, - x: longitude, - y: latitude, - }, + + // ตำแหน่งของผู้ใช้ + const userPoint = new Point({ longitude, latitude }) + const userSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/red.png', + width: '32px', + height: '32px', + } + const userGraphic = new Graphic({ + geometry: userPoint, + symbol: userSymbol, + }) + mapView.graphics.add(userGraphic) + // Get POI place ยิงไปขอที่ server ของกทม.ก่อน + await axios + .get( + 'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, }, - } - ) - .then((response) => { - // console.log('poi', response.data.location) - poiPlaceName.value = response.data.address - ? response.data.address.PlaceName === '' - ? response.data.address.ShortLabel - : response.data.address.PlaceName - : 'ไม่พบข้อมูล' - const poiPoint = new Point({ - longitude: response.data.location.x, - latitude: response.data.location.y, - }) - const poiSymbol = { - type: 'picture-marker', - url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', - width: '32px', - height: '32px', - } - const poiGraphic = new Graphic({ - geometry: poiPoint, - symbol: poiSymbol, - }) - mapView.graphics.add(poiGraphic) - // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI - mapView.goTo({ - target: [userPoint, poiPoint], - zoom: zoomMap.value, - }) + token: apiKey.value, + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, + }) + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) - updateLocation(latitude, longitude, poiPlaceName.value) - }) - .catch((error) => { - // Keep map visible even when POI lookup fails. - poiPlaceName.value = 'ไม่พบข้อมูล' - updateLocation(latitude, longitude, poiPlaceName.value) - }) - }) + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch(async (error) => { + // console.error('Error fetching points of interest:', error) + // Get POI place ยิงไปขอที่ server arcgis ไม่ต้องใช้ token + await axios + .get( + 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', + { + params: { + f: 'json', // Format JSON response + distance: 2000, + category: 'POI', + location: { + spatialReference: { wkid: 4326 }, + x: longitude, + y: latitude, + }, + }, + } + ) + .then((response) => { + // console.log('poi', response.data.location) + poiPlaceName.value = response.data.address + ? response.data.address.PlaceName === '' + ? response.data.address.ShortLabel + : response.data.address.PlaceName + : 'ไม่พบข้อมูล' + const poiPoint = new Point({ + longitude: response.data.location.x, + latitude: response.data.location.y, + }) + const poiSymbol = { + type: 'picture-marker', + url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', + width: '32px', + height: '32px', + } + const poiGraphic = new Graphic({ + geometry: poiPoint, + symbol: poiSymbol, + }) + mapView.graphics.add(poiGraphic) + // อัปเดตการแสดงผลให้แสดงทั้งตำแหน่งของผู้ใช้และ POI + mapView.goTo({ + target: [userPoint, poiPoint], + zoom: zoomMap.value, + }) + + updateLocation(latitude, longitude, poiPlaceName.value) + }) + .catch((error) => { + // console.error('Error fetching points of interest:', error) + }) + }) + }) }) } catch (error) { console.error('Error loading the map', error) @@ -242,35 +221,10 @@ const requestLocationPermission = () => { navigator.geolocation.getCurrentPosition( async (position) => { - const sampledPosition = await getPositionForValidation(position) - - // 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) { - locationGranted.value = true - emit('locationStatus', true) - } - - // Check for critical errors (invalid coordinates) that prevent showing location - const hasCriticalErrors = validationResult.errors.some((error) => - error.includes('พิกัดตำแหน่งไม่ถูกต้อง') - ) - - if (hasCriticalErrors) { - locationGranted.value = false - emit('locationStatus', false) - messageError($q, '', validationResult.errors[0]) - return - } - - // Permission granted for map preview state. + // Permission granted locationGranted.value = true - emit('locationStatus', true) - const { latitude, longitude } = sampledPosition.coords + const { latitude, longitude } = position.coords // console.log('Current position:', latitude, longitude) if (!latitude || !longitude) { @@ -280,13 +234,12 @@ const requestLocationPermission = () => { // Center map on user's location if map is initialized if (privacyStore.isAccepted) { - await initializeMap(sampledPosition) + await initializeMap() } }, (error) => { // Permission denied locationGranted.value = false - emit('locationStatus', false) switch (error.code) { case error.PERMISSION_DENIED: @@ -311,19 +264,18 @@ const requestLocationPermission = () => { break } }, - { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } + { enableHighAccuracy: true } ) } defineExpose({ requestLocationPermission, - locationGranted, }) diff --git a/src/components/DialogDebug.vue b/src/components/DialogDebug.vue index 5fa1f3f..fb151e3 100644 --- a/src/components/DialogDebug.vue +++ b/src/components/DialogDebug.vue @@ -11,6 +11,7 @@ import http from '@/plugins/http' import config from '@/app.config' import DialogHeader from '@/components/DialogHeader.vue' +import FooterContact from '@/components/FooterContact.vue' const $q = useQuasar() const store = usePositionKeycloakStore() @@ -175,7 +176,7 @@ function onClose() { + + + + diff --git a/src/components/PopupPrivacy.vue b/src/components/PopupPrivacy.vue index a72dde3..4493fab 100644 --- a/src/components/PopupPrivacy.vue +++ b/src/components/PopupPrivacy.vue @@ -108,6 +108,7 @@ const checkIfScrollable = () => { transition-show="slide-up" transition-hide="slide-down" :maximized="$q.screen.lt.sm" + @show="checkIfScrollable" > diff --git a/src/composables/useLocationValidation.ts b/src/composables/useLocationValidation.ts deleted file mode 100644 index 689af41..0000000 --- a/src/composables/useLocationValidation.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { ref } from 'vue' -import { useQuasar } from 'quasar' -import { useCounterMixin } from '@/stores/mixin' - -export interface LocationValidationResult { - isValid: boolean - isMockDetected: boolean - confidence: 'low' | 'medium' | 'high' - warnings: string[] - errors: string[] -} - -export interface PositionSnapshot { - latitude: number - longitude: number - timestamp: number -} - -// Configuration constants - exported for documentation and testing purposes -export const VALIDATION_CONFIG = { - MAX_TIMESTAMP_AGE_MS: 60_000, // 60 seconds - maximum acceptable age of location data - MAX_ACCURACY_METERS: 100, // 100 meters - maximum acceptable GPS accuracy - MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed - POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection - 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) - // 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 - -export function useLocationValidation() { - const $q = useQuasar() - const { messageError } = useCounterMixin() - - // Thai error messages - exported for i18n consistency - const errorMessages = { - MOCK_DETECTED: - 'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', - INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่', - STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่', - POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS', - IMPOSSIBLE_SPEED: - 'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง', - SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง', - DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง', - OUT_OF_SERVICE_AREA: - 'ตรวจพบตำแหน่งนอกพื้นที่ใช้งานของระบบ กรุณาปิดแอปจำลองตำแหน่งและลองใหม่', - } - - const previousPositions = ref([]) - - // คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula) - const haversineDistance = ( - lat1: number, - lon1: number, - lat2: number, - lon2: number - ): number => { - const R = 6371e3 // Earth's radius in meters - const φ1 = (lat1 * Math.PI) / 180 - const φ2 = (lat2 * Math.PI) / 180 - const Δφ = ((lat2 - lat1) * Math.PI) / 180 - const Δλ = ((lon2 - lon1) * Math.PI) / 180 - - const a = - Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + - Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2) - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) - - return R * c - } - - // ตรวจสอบพิกัดถูกต้อง - const validateCoordinates = (lat: number, lon: number): boolean => { - return ( - lat >= -90 && - lat <= 90 && - lon >= -180 && - lon <= 180 && - !isNaN(lat) && - !isNaN(lon) && - !(lat === 0 && lon === 0) // Mock มักใช้ 0,0 - ) - } - - // ตรวจสอบความแม่นยำ - const validateAccuracy = (accuracy: number | null): boolean => { - if (accuracy === null) return true - return accuracy <= VALIDATION_CONFIG.MAX_ACCURACY_METERS - } - - // ตรวจสอบ Timestamp - const validateTimestamp = (timestamp: number): boolean => { - const now = Date.now() - const age = Math.abs(now - timestamp) - return age <= VALIDATION_CONFIG.MAX_TIMESTAMP_AGE_MS - } - - // คำนวณความเร็ว - const calculateSpeed = ( - pos1: PositionSnapshot, - pos2: PositionSnapshot - ): number => { - const distance = haversineDistance( - pos1.latitude, - pos1.longitude, - pos2.latitude, - pos2.longitude - ) - const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds - return timeDiff > 0 ? distance / timeDiff : 0 - } - - // ตรวจสอบความเร็วปกติ - const validateSpeed = ( - current: PositionSnapshot, - previous: PositionSnapshot - ): boolean => { - const speed = calculateSpeed(previous, current) - return speed <= VALIDATION_CONFIG.MAX_SPEED_MS - } - - // ตรวจสอบค่าความแม่นยำที่ผิดปกติ (mock apps มักรายงานค่าที่เป็นเลขจำนวนเต็มสวยงาม เช่น 5.0, 3.0) - const hasSuspiciousPerfectAccuracy = (accuracy: number | null): boolean => { - if (accuracy === null) return false - return ( - Number.isInteger(accuracy) && - accuracy <= VALIDATION_CONFIG.SUSPICIOUS_ACCURACY_MAX - ) - } - - // ตรวจสอบพิกัดซ้ำกันทุกค่า (real GPS มีการสั่นเล็กน้อย mock app มักคืนพิกัดเดิมซ้ำ) - const hasDuplicateCoordinates = (lat: number, lon: number): boolean => { - return previousPositions.value.some( - (p) => p.latitude === lat && p.longitude === lon - ) - } - - 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 - const validateLocation = ( - position: GeolocationPosition - ): LocationValidationResult => { - const warnings: string[] = [] - const errors: string[] = [] - let mockIndicators = 0 - - const { latitude, longitude, accuracy } = position.coords - const { timestamp } = position - - // 1. Coordinate validation - if (!validateCoordinates(latitude, longitude)) { - errors.push(errorMessages.INVALID_COORDINATES) - 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 - if (!validateTimestamp(timestamp)) { - errors.push(errorMessages.STALE_TIMESTAMP) - mockIndicators += 2 - } - - // 3. Accuracy validation - if (!validateAccuracy(accuracy)) { - warnings.push(errorMessages.POOR_ACCURACY) - mockIndicators += 1 - } - - // 4. Compare with previous positions - if (previousPositions.value.length > 0) { - const previous = - previousPositions.value[previousPositions.value.length - 1] - - if (!validateSpeed({ latitude, longitude, timestamp }, previous)) { - errors.push(errorMessages.IMPOSSIBLE_SPEED) - mockIndicators += 3 - } - } - - // 5. Suspiciously perfect accuracy — real GPS never rounds to a whole-number ≤ 5m - if (hasSuspiciousPerfectAccuracy(accuracy)) { - warnings.push(errorMessages.SUSPICIOUS_ACCURACY) - mockIndicators += 2 - } - - // 6. Exact duplicate coordinates — real GPS always drifts slightly between readings - if ( - previousPositions.value.length > 0 && - hasDuplicateCoordinates(latitude, longitude) - ) { - warnings.push(errorMessages.DUPLICATE_POSITION) - mockIndicators += 2 - } - - // Store current position - previousPositions.value.push({ latitude, longitude, timestamp }) - if ( - previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE - ) { - previousPositions.value.shift() - } - - // Determine result - const isMockDetected = - mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD - const isValid = errors.length === 0 - - let confidence: 'low' | 'medium' | 'high' = 'low' - if (mockIndicators >= 5) confidence = 'high' - else if (mockIndicators >= 3) confidence = 'medium' - - return { - isValid, - isMockDetected, - confidence, - warnings, - errors, - } - } - - const showMockWarning = (result: LocationValidationResult) => { - if (!result.isMockDetected) return - messageError($q, '', errorMessages.MOCK_DETECTED) - } - - const resetValidation = () => { - previousPositions.value = [] - } - - return { - validateLocation, - showMockWarning, - resetValidation, - } -} diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 43c14c0..5df3c0c 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -8,7 +8,6 @@ import config from '@/app.config' import http from '@/plugins/http' import { useCounterMixin } from '@/stores/mixin' import { usePermissions } from '@/composables/usePermissions' -import { useLocationValidation } from '@/composables/useLocationValidation' import { usePrivacyStore } from '@/stores/privacy' import type { FormRef, OptionReason } from '@/interface/response/checkin' @@ -19,8 +18,6 @@ const mixin = useCounterMixin() const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin const $q = useQuasar() const { checkPrivacyAccepted } = usePermissions() -const { validateLocation, showMockWarning, resetValidation } = - useLocationValidation() const privacyStore = usePrivacyStore() const MOCK_CHECK_DELAY_MS = 800 @@ -36,8 +33,6 @@ const endTimeAfternoon = ref('12:00:00') //เวลาเช็คเ const isLoadingCheckTime = ref(false) // ตัวแปรสำหรับการโหลด const disabledBtn = ref(false) -const locationGranted = ref(false) -const isMockLocationDetected = ref(false) /** * fetch เช็คเวลาต้องลงเวลาเข้าหรือออกงาน @@ -119,26 +114,7 @@ async function updateLocation( formLocation.POI = namePOI } -/** - * รับค่าสถานะ location จาก AscGISMap - */ -function onLocationStatus(status: boolean) { - locationGranted.value = status - if (status) { - isMockLocationDetected.value = false - } -} - -/** - * รับค่า mock location detection จาก AscGISMap - */ -function onMockDetected(result: any) { - isMockLocationDetected.value = !!result?.isMockDetected - disabledBtn.value = false -} - function resetLocationForRetry() { - locationGranted.value = false formLocation.lat = 0 formLocation.lng = 0 formLocation.POI = '' @@ -174,62 +150,6 @@ async function getDelayedFreshPosition() { } } -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('') // พื้นที่ใกล้เคียง const model = ref('') // สถานที่ทำงาน // ตัวเลือกสถานที่ทำงาน @@ -267,6 +187,9 @@ const cameraIsOn = ref(false) const img = ref(undefined) const photoWidth = ref(350) const photoHeight = ref(350) +const availableCameras = ref([]) +const currentCameraIndex = ref(0) +const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown') const intervalId = ref(undefined) // ต้องใช้ตัวแปรเก็บค่า interval @@ -382,6 +305,16 @@ async function stopChecking() { } } +function identifyCameraType(label: string): 'front' | 'back' | 'unknown' { + const lowerLabel = label.toLowerCase() + if (lowerLabel.includes('front') || lowerLabel.includes('user') || lowerLabel.includes('face')) { + return 'front' + } else if (lowerLabel.includes('back') || lowerLabel.includes('environment') || lowerLabel.includes('rear')) { + return 'back' + } + return 'unknown' +} + /** function เปิดกล้อง*/ async function openCamera() { // เช็คสิทธิ์ privacy ก่อนเปิดกล้อง @@ -395,7 +328,11 @@ async function openCamera() { await camera.value?.stop() } else { await camera.value?.start() - await changeCamera() // ต้องรอให้ start() เสร็จก่อน + const devices: any = await camera.value?.devices(['videoinput']) + if (devices) { + availableCameras.value = devices + await changeCamera() + } } cameraIsOn.value = !cameraIsOn.value } else { @@ -409,10 +346,59 @@ async function openCamera() { } /** change camera device*/ -async function changeCamera() { - const devices: any = await camera.value?.devices(['videoinput']) - const device = await devices[0] - camera.value?.changeCamera(device.deviceId) +async function changeCamera(targetCameraType?: 'front' | 'back') { + try { + const devices: any = await camera.value?.devices(['videoinput']) + + if (!devices || devices.length === 0) { + console.warn('No cameras found') + return + } + + availableCameras.value = devices + + if (devices.length === 1 || !targetCameraType) { + const device = devices[0] + await camera.value?.changeCamera(device.deviceId) + currentCameraIndex.value = 0 + currentCameraType.value = identifyCameraType(device.label || '') + return + } + + const matchingCameras = devices.filter((device: any) => + identifyCameraType(device.label || '') === targetCameraType + ) + + if (matchingCameras.length > 0) { + const targetDevice = matchingCameras[0] + await camera.value?.changeCamera(targetDevice.deviceId) + currentCameraIndex.value = devices.indexOf(targetDevice) + currentCameraType.value = targetCameraType + } else { + const nextIndex = (currentCameraIndex.value + 1) % devices.length + const nextDevice = devices[nextIndex] + await camera.value?.changeCamera(nextDevice.deviceId) + currentCameraIndex.value = nextIndex + currentCameraType.value = identifyCameraType(nextDevice.label || '') + } + } catch (error) { + console.error('Error switching camera:', error) + } +} + +/** switch camera device*/ +async function switchCamera() { + if (availableCameras.value.length <= 1) { + return + } + + // สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก) + const targetIndex = currentCameraIndex.value === 0 ? 1 : 0 + const targetDevice = availableCameras.value[targetIndex] + + await camera.value?.changeCamera(targetDevice.deviceId) + currentCameraIndex.value = targetIndex + currentCameraType.value = identifyCameraType(targetDevice.label || '') } /** function ถ่ายรูป*/ @@ -449,6 +435,8 @@ const objectRef: FormRef = { /** function ตรวจสอบค่าว่างของ input*/ async function validateForm() { + disabledBtn.value = true + const hasError = [] for (const key in objectRef) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) { @@ -460,11 +448,6 @@ async function validateForm() { } } if (hasError.every((result) => result === true)) { - const isLocationValid = await revalidateLocationBeforeSubmit() - if (!isLocationValid) { - return - } - if (statusCheckin.value == false) { getCheck() } else if (statusCheckin.value) { @@ -480,11 +463,15 @@ async function validateForm() { model.value === 'อื่นๆ' ? useLocation.value : model.value })` } คุณต้องการยืนยันการลงเวลาเข้างาน?`, - () => {}, + () => { + disabledBtn.value = false + }, 'red', 'ยืนยัน' ) } + } else { + disabledBtn.value = false } } @@ -495,20 +482,16 @@ const timeChickin = ref('') //เวลาเข้างาน,เว async function confirm() { // เช็คสิทธิ์ privacy ก่อนใช้งานแผนที่และกล้อง if (!checkPrivacyAccepted()) { + disabledBtn.value = false return } if (!formLocation.POI || !formLocation.lat || !formLocation.lng) { + disabledBtn.value = false mapRef.value?.requestLocationPermission() return } - const isLocationValid = await revalidateLocationBeforeSubmit() - if (!isLocationValid) { - return - } - - disabledBtn.value = true showLoader() const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง const locationName = workplace.value === 'in-place' ? '' : useLocation.value @@ -551,6 +534,7 @@ async function confirm() { async function getCheck() { if (!formLocation.POI || !formLocation.lat || !formLocation.lng) { + disabledBtn.value = false mapRef.value?.requestLocationPermission() return } @@ -583,7 +567,9 @@ async function getCheck() { () => confirm(), 'ยืนยันการลงเวลาออกงาน', `เวลาออกจากงานของคุณคือ ${endTimeAfternoonVal} แต่ขณะนี้เป็นเวลา ${timeVal} น. หากคุณออกจากงานในเวลานี้สถานะการลงเวลาจะเป็น "${res.data.result.statusText}" คุณแน่ใจว่าจะลงเวลาออกงานในตอนนี้ใช่หรือไม่?`, - () => {}, + () => { + disabledBtn.value = false + }, 'red', 'ยืนยัน' ) @@ -592,6 +578,7 @@ async function getCheck() { } }) .catch((e) => { + disabledBtn.value = false messageError($q, e) }) .finally(() => { @@ -723,16 +710,6 @@ watch( } } ) - -watch( - () => locationGranted.value, - (newVal) => { - // Removed auto-reset of isMockLocationDetected to prevent - // clearing mock detection state when permission is granted. - // Mock detection state should only be reset after explicit user action - // or after a successful validation without mock indicators. - } -)