+
+
@@ -354,29 +659,49 @@ defineExpose({
"
>
พื้นที่ใกล้เคียง
+
+
+
+ {{ textTooltip }}
+
:
- {{ poiPlaceName || 'ไม่พบข้อมูล' }}
+ {{ poiPlaceName }}
-
+
-
- {{ poiPlaceName || 'ไม่พบข้อมูล' }}
+
+ พื้นที่ใกล้เคียง
+
+
+
+ {{ poiPlaceName || '-' }}
+
-
+
@@ -386,4 +711,8 @@ defineExpose({
.expanAS.q-item__section--avatar {
min-width: 40px !important;
}
+
+.map-hidden {
+ visibility: hidden;
+}
diff --git a/src/components/AscGISMapTime.vue b/src/components/AscGISMapTime.vue
index 7360980..1264791 100644
--- a/src/components/AscGISMapTime.vue
+++ b/src/components/AscGISMapTime.vue
@@ -1,9 +1,8 @@
-
+
-
+
@@ -304,6 +305,9 @@ function onClose() {
+
+ ผู้ดูแลระบบจะติดต่อกลับผ่านทางอีเมลที่ท่านระบุ กรุณาตรวจสอบอีเมลของท่านเป็นระยะ
+
@@ -329,12 +334,6 @@ function onClose() {
v-model="formData.phone"
class="inputgreen"
hide-bottom-space
- :rules="[
- () =>
- !!formData.email ||
- !!formData.phone ||
- 'กรุณากรอกอีเมลหรือเบอร์โทรติดต่อกลับ',
- ]"
/>
@@ -343,7 +342,9 @@ function onClose() {
-
+
+
+
+
+
+
+ พบปัญหาการใช้งานกรุณาติดต่อผู้ดูแลระบบ
+ 088-264-9800
+
+
+
+
+
+
+
diff --git a/src/components/FormTime.vue b/src/components/FormTime.vue
index c299e4f..79a8f68 100644
--- a/src/components/FormTime.vue
+++ b/src/components/FormTime.vue
@@ -1,6 +1,6 @@
@@ -80,7 +114,7 @@ onMounted(() => {
>
import { ref, watch } from 'vue'
-import { PropType } from 'vue'
+import type { PropType } from 'vue'
import type { DataCheckIn } from '@/interface/index/Main'
diff --git a/src/components/PopupPrivacy.vue b/src/components/PopupPrivacy.vue
index a72dde3..1bd816b 100644
--- a/src/components/PopupPrivacy.vue
+++ b/src/components/PopupPrivacy.vue
@@ -4,9 +4,11 @@ import { useQuasar } from 'quasar'
import http from '@/plugins/http'
import config from '@/app.config'
import { usePrivacyStore } from '@/stores/privacy'
+import { usePositionKeycloakStore } from '@/stores/positionKeycloak'
const $q = useQuasar()
const privacyStore = usePrivacyStore()
+const positionKeycloakStore = usePositionKeycloakStore()
const modal = defineModel('modal', {
required: true,
@@ -78,6 +80,12 @@ const handleAccept = async () => {
accept: true,
})
privacyStore.setAccepted(true)
+
+ // อัปเดต privacyCheckin ใน positionKeycloak store ด้วย
+ if (positionKeycloakStore.dataPositionKeycloak) {
+ positionKeycloakStore.dataPositionKeycloak.privacyCheckin = true
+ }
+
modal.value = false
} catch (error) {}
}
@@ -89,7 +97,7 @@ const toggleDetails = () => {
const checkIfScrollable = () => {
nextTick(() => {
- const container = scrollContainer.value?.$el || scrollContainer.value
+ const container = (scrollContainer.value as any)?.$el || scrollContainer.value
if (container) {
const { scrollHeight, clientHeight } = container
@@ -108,6 +116,7 @@ const checkIfScrollable = () => {
transition-show="slide-up"
transition-hide="slide-down"
:maximized="$q.screen.lt.sm"
+ @show="checkIfScrollable"
>
diff --git a/src/components/TableHistory.vue b/src/components/TableHistory.vue
index 5d13e66..e081a3c 100644
--- a/src/components/TableHistory.vue
+++ b/src/components/TableHistory.vue
@@ -2,42 +2,37 @@
import { ref, watch } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
-import { useChekIn } from '@/stores/chekin'
+import { useCheckIn } from '@/stores/checkin'
-import { is, type QTableProps } from 'quasar'
+import type { QTableProps } from 'quasar'
import type { Pagination, DataCheckIn } from '@/interface/index/Main'
import Popup from '@/components/PopUp.vue' // dialog เพิ่ม/ขอแก้ไขลงเวลากรณีพิเศษ
import SkeletonTable from '@/components/SkeletonTable.vue' // skeleton table
const { date2Thai } = useCounterMixin()
-const stores = useChekIn()
+const stores = useCheckIn()
+
+const page = defineModel('page', { required: true })
+const pageSize = defineModel('pageSize', { required: true })
/** props ข้อมูลจาก Components Page HistoryView */
const props = defineProps({
paging: {
type: Boolean,
- defualt: false,
- },
- pageSize: {
- type: Number,
- defualt: 10,
- },
- page: {
- type: Number,
- defualt: 1,
+ default: false,
},
maxPage: {
type: Number,
- defualt: 1,
+ default: 1,
},
total: {
type: Number,
- defualt: 0,
+ default: 0,
},
tab: {
type: String,
- defualt: '',
+ default: '',
required: true,
},
fetchData: {
@@ -62,15 +57,15 @@ const selected = ref([])
const columns = ref(
props.tab == 'history'
? [
- {
- name: 'checkInDate',
- align: 'left',
- label: 'วัน/เดือน/ปี',
- sortable: true,
- field: 'checkInDate',
- headerStyle: 'font-size: 14px',
- style: 'font-size: 14px; width:15%;',
- },
+ // {
+ // name: 'checkInDate',
+ // align: 'left',
+ // label: 'วัน/เดือน/ปี',
+ // sortable: true,
+ // field: 'checkInDate',
+ // headerStyle: 'font-size: 14px',
+ // style: 'font-size: 14px; width:15%;',
+ // },
{
name: 'checkInTime',
align: 'left',
@@ -79,6 +74,9 @@ const columns = ref(
field: 'checkInTime',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px; width:15%;',
+ format: (val: string, row: DataCheckIn) => {
+ return `${row.checkInDate} ${val} น.`
+ },
},
{
name: 'checkInLocation',
@@ -107,7 +105,9 @@ const columns = ref(
headerStyle: 'font-size: 14px',
style: 'font-size: 14px; width:15%;',
format: (val: string, row: DataCheckIn) => {
- return row.checkOutStatus != '-' && val ? val : '-'
+ return row.checkOutStatus != '-' && val
+ ? `${row.checkOutDate} ${val} น.`
+ : '-'
},
},
{
@@ -182,7 +182,6 @@ const columns = ref(
)
const visibleColumns = ref([
'checkInDateTime',
- 'checkInDate',
'checkInTime',
'checkInLocation',
'checkInStatus',
@@ -190,31 +189,20 @@ const visibleColumns = ref([
'checkOutLocation',
'checkOutStatus',
])
-
-const currentPage = ref(1) //หน้าปัจจุบัน
-// Pagination - initial pagination
-const initialPagination = ref({
- sortBy: null,
- descending: false,
- page: 1,
- rowsPerPage: props.pageSize, // set ตาม page หลักส่งมา
+const pagination = ref({
+ page: page.value,
+ rowsPerPage: pageSize.value,
})
// Pagination - update rowsPerPage
async function updatePagination(newPagination: Pagination) {
- initialPagination.value = newPagination
- currentPage.value = 1 // set current page เป็น 1 เสมอเมื่อเปลี่ยน per row
+ pageSize.value = newPagination.rowsPerPage
}
async function filterFn() {
- // ส่งอีเวนต์ 'update:change-page' เมื่อหน้าเปลี่ยนแปลง
- emit(
- 'update:change-page',
- 1,
- initialPagination.value.rowsPerPage,
- keyword.value
- )
+ emit('update:change-page', 1, pageSize.value, keyword.value)
}
+
const modalPopup = ref(false) // popup แก้ไขลงเวลา
const titlePopup = ref('') // หัวขข้อ popup แก้ไขลงเวลา
const dataRow = ref() // ข้อมูลการลงเวลา
@@ -254,7 +242,7 @@ function classStatus(status: string) {
case 'ปฏิบัติงานไม่ครบตามกำหนดเวลา':
return 'text-orange-8'
default:
- break
+ return ''
}
}
@@ -272,17 +260,15 @@ function onClickOpenPopupRejrct(data: DataCheckIn) {
}
}
-/** watch currentPage และ rowsPerPage*/
+function onUpdatePage(val: number) {
+ emit('update:change-page', val, pageSize.value, keyword.value)
+}
+
+/** watch pageSize*/
watch(
- [() => currentPage.value, () => initialPagination.value.rowsPerPage],
+ () => pageSize.value,
() => {
- // ส่งอีเวนต์ 'update:change-page' เมื่อหน้าเปลี่ยนแปลง
- emit(
- 'update:change-page',
- currentPage.value,
- initialPagination.value.rowsPerPage,
- keyword.value
- )
+ emit('update:change-page', 1, pageSize.value, keyword.value)
}
)
@@ -336,10 +322,10 @@ watch(
v-model:selected="selected"
:virtual-scroll-sticky-size-start="48"
:style="$q.screen.gt.xs ? 'max-height: 64vh' : ''"
- :rows-per-page-options="[10, 25, 50, 100]"
+ :rows-per-page-options="[1, 10, 25, 50, 100]"
:grid="$q.screen.gt.xs ? false : true"
- :pagination="initialPagination"
@update:pagination="updatePagination"
+ v-model:pagination="pagination"
>
@@ -472,10 +458,11 @@ watch(
+
ทั้งหมด {{ props.total }} รายการ
diff --git a/src/components/ToolBar.vue b/src/components/ToolBar.vue
index ab54d66..c95111d 100644
--- a/src/components/ToolBar.vue
+++ b/src/components/ToolBar.vue
@@ -2,14 +2,14 @@
import { onMounted, ref, watch } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
-import { useChekIn } from '@/stores/chekin'
+import { useCheckIn } from '@/stores/checkin'
import { calculateFiscalYear } from '@/utils/function'
import type { DataDateMonthObject } from '@/interface/index/Main'
import Popup from '@/components/PopUp.vue'
-const stores = useChekIn()
+const stores = useCheckIn()
const { monthYear2Thai } = useCounterMixin()
/**
@@ -18,11 +18,11 @@ const { monthYear2Thai } = useCounterMixin()
const props = defineProps({
fetchData: {
type: Function,
- require: true,
+ required: true,
},
tab: {
type: String,
- require: true,
+ required: true,
},
})
const emit = defineEmits(['update:year'])
@@ -43,8 +43,6 @@ function filterYearFn(type: string) {
const year = type === 'year' ? filterYear.value : dateMonth.value.year
const month = dateMonth.value.month
- console.log(year, month)
-
// ตรวจสอบค่าก่อนส่ง
if (isNaN(Number(year)) || isNaN(Number(month))) {
console.warn('Invalid year or month value:', { year, month })
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/composables/usePermissions.ts b/src/composables/usePermissions.ts
index ecf1ef3..b92ed29 100644
--- a/src/composables/usePermissions.ts
+++ b/src/composables/usePermissions.ts
@@ -1,52 +1,171 @@
-import { useQuasar } from 'quasar'
+import { onBeforeUnmount, ref } from 'vue'
import { usePrivacyStore } from '@/stores/privacy'
+type BrowserPermissionState = PermissionState | 'unsupported'
+
+type PermissionQueryName = 'camera' | 'geolocation'
+
export function usePermissions() {
- const $q = useQuasar()
const privacyStore = usePrivacyStore()
+ const cameraPermissionState = ref('prompt')
+ const locationPermissionState = ref('prompt')
- // const checkCameraPermission = (): boolean => {
- // if (!privacyStore.isAccepted) {
- // privacyStore.modalPrivacy = true
- // $q.notify({
- // type: 'warning',
- // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานกล้อง',
- // position: 'top',
- // })
- // return false
- // }
- // return true
- // }
+ let cameraPermissionStatus: PermissionStatus | null = null
+ let locationPermissionStatus: PermissionStatus | null = null
- // const checkLocationPermission = (): boolean => {
- // if (!privacyStore.isAccepted) {
- // privacyStore.modalPrivacy = true
- // $q.notify({
- // type: 'warning',
- // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานแผนที่',
- // position: 'top',
- // })
- // return false
- // }
- // return true
- // }
+ const isPermissionsApiSupported = () =>
+ typeof navigator !== 'undefined' && 'permissions' in navigator
+
+ const setPermissionState = (
+ target: typeof cameraPermissionState | typeof locationPermissionState,
+ state: BrowserPermissionState
+ ) => {
+ target.value = state
+ }
+
+ const setPermissionChangeListener = (
+ name: PermissionQueryName,
+ status: PermissionStatus
+ ) => {
+ status.onchange = () => {
+ const target =
+ name === 'camera' ? cameraPermissionState : locationPermissionState
+ setPermissionState(target, status.state)
+ }
+ }
+
+ async function queryPermissionState(name: PermissionQueryName) {
+ if (!isPermissionsApiSupported()) {
+ return null
+ }
+
+ try {
+ return await navigator.permissions.query({ name } as PermissionDescriptor)
+ } catch (error) {
+ return null
+ }
+ }
+
+ async function syncPermissionState(name: PermissionQueryName) {
+ const target =
+ name === 'camera' ? cameraPermissionState : locationPermissionState
+ const previousStatus =
+ name === 'camera' ? cameraPermissionStatus : locationPermissionStatus
+
+ if (previousStatus) {
+ previousStatus.onchange = null
+ }
+
+ const status = await queryPermissionState(name)
+ if (!status) {
+ if (target.value === 'prompt') {
+ setPermissionState(target, 'unsupported')
+ }
+ return
+ }
+
+ if (name === 'camera') {
+ cameraPermissionStatus = status
+ } else {
+ locationPermissionStatus = status
+ }
+
+ setPermissionState(target, status.state)
+ setPermissionChangeListener(name, status)
+ }
+
+ async function syncPermissionStates() {
+ await Promise.all([
+ syncPermissionState('camera'),
+ syncPermissionState('geolocation'),
+ ])
+ }
const checkPrivacyAccepted = (): boolean => {
if (!privacyStore.isAccepted) {
privacyStore.modalPrivacy = true
- // $q.notify({
- // type: 'warning',
- // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งาน',
- // position: 'center',
- // })
return false
}
return true
}
+ async function requestCameraPermission() {
+ if (!checkPrivacyAccepted()) {
+ return false
+ }
+
+ if (cameraPermissionState.value === 'granted') {
+ return true
+ }
+
+ if (!navigator.mediaDevices?.getUserMedia) {
+ setPermissionState(cameraPermissionState, 'unsupported')
+ return false
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: { facingMode: 'user' },
+ audio: false,
+ })
+
+ stream.getTracks().forEach((track) => track.stop())
+ setPermissionState(cameraPermissionState, 'granted')
+ return true
+ } catch (error) {
+ setPermissionState(cameraPermissionState, 'denied')
+ return false
+ } finally {
+ await syncPermissionState('camera')
+ }
+ }
+
+ async function requestLocationPermission() {
+ if (!checkPrivacyAccepted()) {
+ return false
+ }
+
+ if (locationPermissionState.value === 'granted') {
+ return true
+ }
+
+ if (!navigator.geolocation) {
+ setPermissionState(locationPermissionState, 'unsupported')
+ return false
+ }
+
+ return new Promise((resolve) => {
+ navigator.geolocation.getCurrentPosition(
+ async () => {
+ setPermissionState(locationPermissionState, 'granted')
+ await syncPermissionState('geolocation')
+ resolve(true)
+ },
+ async () => {
+ setPermissionState(locationPermissionState, 'denied')
+ await syncPermissionState('geolocation')
+ resolve(false)
+ }
+ )
+ })
+ }
+
+ onBeforeUnmount(() => {
+ if (cameraPermissionStatus) {
+ cameraPermissionStatus.onchange = null
+ }
+
+ if (locationPermissionStatus) {
+ locationPermissionStatus.onchange = null
+ }
+ })
+
return {
- // checkCameraPermission,
- // checkLocationPermission,
+ cameraPermissionState,
+ locationPermissionState,
checkPrivacyAccepted,
+ syncPermissionStates,
+ requestCameraPermission,
+ requestLocationPermission,
}
}
diff --git a/src/interface/index/Main.ts b/src/interface/index/Main.ts
index 14def36..72b2ba1 100644
--- a/src/interface/index/Main.ts
+++ b/src/interface/index/Main.ts
@@ -29,7 +29,7 @@ interface Pagination {
sortBy: string | null
descending: boolean
page: number
- rowsPerPage: number | undefined
+ rowsPerPage: number
}
interface DataCheckIn {
checkInDate: string
@@ -38,6 +38,7 @@ interface DataCheckIn {
checkInLocation: string
checkInStatus: string
checkInTime: string
+ checkOutDate: string
checkOutLocation: string
checkOutStatus: string
checkOutTime: string
diff --git a/src/interface/keycloak-position.ts b/src/interface/keycloak-position.ts
new file mode 100644
index 0000000..dfd2680
--- /dev/null
+++ b/src/interface/keycloak-position.ts
@@ -0,0 +1,29 @@
+/**
+ * Interface for Keycloak position data from /org/profile/keycloak/position API
+ */
+export interface KeycloakPosition {
+ /** Privacy consent status */
+ privacyCheckin: boolean
+ /** Profile image filename */
+ avatarName?: string
+ /** User profile ID */
+ profileId: string
+ /** Organization hierarchy data */
+ organization?: Organization
+}
+
+/**
+ * Organization hierarchy structure
+ */
+export interface Organization {
+ /** Top level organization */
+ root?: string
+ /** Child level 1 */
+ child1?: string
+ /** Child level 2 */
+ child2?: string
+ /** Child level 3 */
+ child3?: string
+ /** Child level 4 */
+ child4?: string
+}
diff --git a/src/main.ts b/src/main.ts
index fb28e50..04b0dfd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,6 +3,7 @@ import App from '@/App.vue'
import '@/registerServiceWorker'
import router from '@/router'
import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { Quasar, Dialog, Notify, Loading } from 'quasar'
import '@vuepic/vue-datepicker/dist/main.css'
import quasarUserOptions from '@/quasar-user-options'
@@ -11,9 +12,13 @@ import 'quasar/src/css/index.sass'
import th from 'quasar/lang/th'
import http from '@/plugins/http'
+import { forceOpenInExternalBrowser } from '@/utils/forceExternalBrowser'
+
+forceOpenInExternalBrowser()
const app = createApp(App)
const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
app.use(router)
app.use(pinia)
diff --git a/src/plugins/http.ts b/src/plugins/http.ts
index 9020cb6..378006f 100644
--- a/src/plugins/http.ts
+++ b/src/plugins/http.ts
@@ -2,7 +2,7 @@ import Axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { getToken } from './auth'
const http = Axios.create({
- timeout: 1000000000, // เพิ่มค่า timeout
+ timeout: 30000, // 30 seconds - reasonable timeout for API calls
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
@@ -30,6 +30,7 @@ http.interceptors.response.use(
// eslint-disable-next-line no-prototype-builtins
if (error.hasOwnProperty('response')) {
if (error.response.status === 401 || error.response.status === 403) {
+ // TODO: Implement proper logout logic on 401/403
// Store.commit("SET_ERROR_MESSAGE", error.response.data.message);
// Store.commit("REMOVE_ACCESS_TOKEN")
}
diff --git a/src/plugins/keycloak.ts b/src/plugins/keycloak.ts
index bb382dd..f1660ed 100644
--- a/src/plugins/keycloak.ts
+++ b/src/plugins/keycloak.ts
@@ -1,6 +1,8 @@
// // authen with keycloak client
// import Keycloak from 'keycloak-js'
+export {}
+
// const ACCESS_TOKEN = 'BMAHRIS_KEYCLOAK_IDENTITY'
// const REFRESH_TOKEN = 'BMAHRIS_KEYCLOAK_REFRESH'
// const keycloakConfig = {
diff --git a/src/router/index.ts b/src/router/index.ts
index 23e3edb..8346697 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -5,6 +5,7 @@ import MainView from '@/views/MainView.vue'
const loginView = () => import('@/views/login.vue')
const resetPasswordView = () => import('@/views/ResetPassword.vue')
+const noPositionView = () => import('@/views/NoPositionView.vue')
import { authenticated, logout } from '@/plugins/auth'
@@ -24,14 +25,14 @@ const router = createRouter({
Auth: true,
},
},
- {
- path: '/map',
- name: 'map',
- component: MapView,
- meta: {
- Auth: true,
- },
- },
+ // {
+ // path: '/map',
+ // name: 'map',
+ // component: MapView,
+ // meta: {
+ // Auth: true,
+ // },
+ // },
{
path: '/about',
name: 'about',
@@ -87,19 +88,63 @@ const router = createRouter({
Auth: false,
},
},
+ {
+ path: '/no-position',
+ name: 'no-position',
+ component: noPositionView,
+ meta: {
+ Auth: false,
+ },
+ },
],
})
// authen with keycloak client
router.beforeEach(async (to, from, next) => {
+ // ตรวจสอบเส้นทางพิเศษที่ไม่ต้องตรวจสอบสิทธิ์
+ const publicPaths = ['/login', '/auth', '/reset-password', '/no-position']
+ if (publicPaths.includes(to.path)) {
+ next()
+ return
+ }
+
+ // ตรวจสอบการ authenticate
if (to.meta.Auth) {
const checkAuthen = await authenticated()
if (!checkAuthen && to.meta.Auth) {
logout()
+ return
}
- } else {
- next()
}
+
+ // ตรวจสอบว่าผู้ใช้มีข้อมูลสังกัดหรือไม่
+ // ต้องทำการ dynamic import เนื่องจากเป็นการใช้งาน Pinia store ใน router
+ if (to.path !== '/no-position') {
+ try {
+ const { usePositionKeycloakStore } = await import('@/stores/positionKeycloak')
+ const positionKeycloakStore = usePositionKeycloakStore()
+ const dataPositionKeycloak = positionKeycloakStore.dataPositionKeycloak
+
+ // ถ้ามีข้อมูล positionKeycloak แล้ว ให้ตรวจสอบว่ามี organization หรือไม่
+ if (dataPositionKeycloak) {
+ const hasOrganization =
+ dataPositionKeycloak.organization &&
+ (dataPositionKeycloak.organization.root ||
+ dataPositionKeycloak.organization.child1 ||
+ dataPositionKeycloak.organization.child2 ||
+ dataPositionKeycloak.organization.child3 ||
+ dataPositionKeycloak.organization.child4)
+
+ if (!hasOrganization) {
+ next('/no-position')
+ return
+ }
+ }
+ } catch (error) {
+ console.error('Error checking position:', error)
+ }
+ }
+
next()
})
diff --git a/src/stores/chekin.ts b/src/stores/checkin.ts
similarity index 96%
rename from src/stores/chekin.ts
rename to src/stores/checkin.ts
index bf27004..47f0694 100644
--- a/src/stores/chekin.ts
+++ b/src/stores/checkin.ts
@@ -8,7 +8,7 @@ const mixin = useCounterMixin()
const { date2Thai } = mixin
/** store for checkin history*/
-export const useChekIn = defineStore('checkin', () => {
+export const useCheckIn = defineStore('checkin', () => {
const year = ref(calculateFiscalYear(new Date())) //ปีงบประมาณ
const rows = ref
+