Compare commits

...

10 commits

Author SHA1 Message Date
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
6666dcd914 fix: msgCheckTime
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m30s
2026-05-01 15:15:56 +07:00
DESKTOP-1R2VSQH\Lenovo ThinkPad E490
7ce5eab6f4 fix(HomeView): fetch Data 2026-05-01 14:31:09 +07:00
6c819a6097 เพิ่มเช็คสังกัด ถ้าไม่มีสังกัดจะแสดงหน้า no-position 2026-05-01 12:50:50 +07:00
c4e246ed74 fixed remove text and service 2026-05-01 11:01:29 +07:00
ac18c0f932 fixed use icon pin in assets 2026-05-01 10:50:13 +07:00
4d821b0ab6 fixed code 2026-04-30 16:15:46 +07:00
0a6fc4a974 fix code 2026-04-30 16:07:21 +07:00
365d55d310 clean log 2026-04-30 15:58:07 +07:00
e9d28197df fix switch Landscape -> Portrait 2026-04-30 15:55:31 +07:00
ec22714f01 fix code error 2026-04-30 11:50:20 +07:00
13 changed files with 1219 additions and 308 deletions

View file

@ -0,0 +1,9 @@
# HRMS Check-in Expert Memory
## Features
- [No Position Assignment Page](feature_no_position_page.md) - Implementation for users without organization position assignment
## Project Issues & Fixes
- [Position Orientation Change Fix](issue_position_orientation_change_fix.md) - Fix for position map not displaying on screen orientation changes

View file

@ -0,0 +1,68 @@
---
name: No Position Assignment Page
description: Feature implementation for users without organization position assignment
type: reference
---
## No Position Assignment Page Feature
When a user doesn't have a position assignment (สังกัด), they are redirected to a dedicated page explaining they are not part of the organization structure.
### Files Created/Modified
1. **Created:** `/Users/waruneeta/Desktop/ChamomindWorking/HRMSProject/hrms-checkin/src/views/NoPositionView.vue`
- Displays message in Thai: "ไม่พบข้อมูลสังกัด"
- Shows icon and explanation text
- "ตกลง" (OK) button that shows confirmation dialog
- After confirmation, performs logout and clears positionKeycloak store
2. **Modified:** `/Users/waruneeta/Desktop/ChamomindWorking/HRMSProject/hrms-checkin/src/router/index.ts`
- Added new route `/no-position` with `Auth: false`
- Enhanced router guard to check for position data
- Redirects to `/no-position` if user has no organization assignment
3. **Modified:** `/Users/waruneeta/Desktop/ChamomindWorking/HRMSProject/hrms-checkin/src/views/MainView.vue`
- Updated `fetchKeycloakPosition()` function
- Checks if `organization` object exists and has any data
- Redirects to `/no-position` if no organization data found
### Logic Flow
1. User logs in successfully
2. `MainView.vue` calls `fetchKeycloakPosition()` in `onMounted()`
3. API returns position data from `/org/profile/keycloak/position`
4. System checks if `organization` object has any non-null values (root, child1-4)
5. If no organization data exists:
- User is redirected to `/no-position`
- User sees message explaining they need to contact staff
- User clicks "ตกลง" to logout
- Staff adds them to organization structure
- User can login again
### Position Data Structure
```typescript
interface KeycloakPosition {
privacyCheckin: boolean
avatarName?: string
profileId: string
organization?: Organization
}
interface Organization {
root?: string
child1?: string
child2?: string
child3?: string
child4?: string
}
```
A user is considered to have "no position" when all organization fields (root, child1, child2, child3, child4) are null or undefined.
### Thai UI Messages
- Page title: "ไม่พบข้อมูลสังกัด"
- Description: "ท่านยังไม่มีสังกัดในโครงสร้างองค์กร กรุณาติดต่อเจ้าหน้าที่เพื่อดำเนินการเพิ่มข้อมูล"
- Confirmation dialog: "ยืนยันการออกจากระบบ" - "ท่านจะถูกนำออกจากระบบเพื่อให้เจ้าหน้าที่ดำเนินการเพิ่มข้อมูลสังกัดในโครงสร้างองค์กร"
- Button: "ตกลง"

View file

@ -0,0 +1,48 @@
---
name: position_orientation_change_fix
description: Fix for position map not displaying on screen orientation changes
type: project
---
## Issue: Position/Geolocation Not Displaying on Screen Orientation Changes
**Problem:**
The ArcGIS map component (`AscGISMap.vue`) failed to display or update position when the screen orientation changed between portrait and landscape modes.
**Root Cause:**
1. The component uses `v-if="$q.screen.gt.xs"` for conditional rendering of different layouts
2. When orientation changes, the ArcGIS MapView doesn't automatically resize to fit the new container dimensions
3. ArcGIS MapView requires manual `resize()` call when its container's size changes
**Solution Implemented:**
Added screen resize handling to `src/components/AscGISMap.vue`:
1. **Added state tracking:**
- `isMapInitialized` ref to track when map is ready
- `isInitializing` ref to prevent concurrent initializations
2. **Added `handleMapResize()` function:**
- Checks if mapView exists and isn't destroyed
- Uses `nextTick()` to ensure DOM updates complete before resizing
- Calls `mapView.value.resize()` to update map dimensions
3. **Added watcher on `$q.screen.gt.xs`:**
- Triggers when screen size crosses the xs breakpoint
- Waits for DOM update via `nextTick()`
- Calls `handleMapResize()` to adjust map
4. **Added window resize event listener:**
- Falls back for orientation changes that might not trigger Quasar's screen watcher
- Debounced with 300ms timeout to avoid excessive calls
- Properly cleaned up on component unmount
**Files Modified:**
- `src/components/AscGISMap.vue` - Added resize handling logic
**Best Practices Applied:**
- Proper cleanup of event listeners in `onBeforeUnmount`
- Debouncing resize events for performance
- Using `nextTick()` to ensure DOM synchronization
- Checking for destroyed state before calling map methods
**Note:** The Google Maps component (`MapCheckin.vue`) was not affected as `vue3-google-map` handles resize automatically.

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M12 2c-3.866 0-7 3.134-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.866-3.134-7-7-7Z" fill="#1E88E5"/>
<circle cx="12" cy="9" r="3" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M12 2c-3.866 0-7 3.134-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.866-3.134-7-7-7Z" fill="#E53935"/>
<circle cx="12" cy="9" r="3" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

View file

@ -1,22 +1,44 @@
<script setup lang="ts">
import { ref, shallowRef, onBeforeUnmount } from 'vue'
import {
computed,
ref,
shallowRef,
onBeforeUnmount,
onMounted,
watch,
nextTick,
} from 'vue'
import { loadModules } from 'esri-loader'
import axios from 'axios'
import { useCounterMixin } from '@/stores/mixin'
import { useQuasar } from 'quasar'
import { usePrivacyStore } from '@/stores/privacy'
import markerRedUrl from '@/assets/markers/marker-red.svg?url'
import markerBlueUrl from '@/assets/markers/marker-blue.svg?url'
const mixin = useCounterMixin()
const { messageError } = mixin
const privacyStore = usePrivacyStore()
const emit = defineEmits(['update:location'])
// Accept initial POI from parent to prevent skeleton on re-mount
const props = defineProps<{
initialPOI?: string
}>()
const $q = useQuasar()
// Throttle mechanism to prevent excessive location updates
const LOCATION_UPDATE_THROTTLE_MS = 500
let lastLocationUpdate = 0
// Track if map is initialized to prevent re-initialization
const isMapInitialized = ref<boolean>(false)
const isInitializing = ref<boolean>(false)
const currentScreenSize = ref<boolean>($q.screen.gt.xs) // Track screen size to detect changes
const mobileMapExpanded = ref<boolean>(true)
function updateLocation(latitude: number, longitude: number, namePOI: string) {
const now = Date.now()
// Skip update if called too frequently (within throttle period)
@ -28,8 +50,18 @@ function updateLocation(latitude: number, longitude: number, namePOI: string) {
emit('update:location', latitude, longitude, namePOI)
}
const poiPlaceName = ref<string>('') //
const poiPlaceName = ref<string>(props.initialPOI || '') // - use initialPOI if provided
const mapView = shallowRef<any>(null) // Store mapView reference for cleanup (use shallowRef to avoid reactivity issues)
const desktopMapContainerRef = ref<HTMLElement | null>(null)
const mobileMapContainerRef = ref<HTMLElement | null>(null)
const isMapVisible = computed(
() => !!mapView.value && !mapView.value.destroyed && isMapInitialized.value
)
// Unique container ID for each component instance to avoid conflicts
const mapContainerId = `mapViewDisplay_${Math.random()
.toString(36)
.substring(2, 9)}`
// ArcGIS API key from environment variable
const apiKey = ref<string>(
@ -39,174 +71,345 @@ const zoomMap = ref<number>(18)
const textTooltip = ref<string>(
'พื้นที่ใกล้เคียงคือ สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)'
)
let attachRetryTimer: number | undefined
let mapInitVersion = 0
let isRecoveryReinitializing = false
function getActiveMapContainer() {
return $q.screen.gt.xs
? desktopMapContainerRef.value
: mobileMapContainerRef.value
}
async function waitForActiveMapContainer(maxAttempts = 25, delayMs = 120) {
for (let i = 0; i < maxAttempts; i += 1) {
await nextTick()
const container = getActiveMapContainer()
if (container && container.clientWidth > 0 && container.clientHeight > 0) {
return container
}
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
return null
}
function clearAttachRetryTimer() {
if (attachRetryTimer) {
clearTimeout(attachRetryTimer)
attachRetryTimer = undefined
}
}
async function recoverMapByReinitialize() {
if (isRecoveryReinitializing) {
return
}
isRecoveryReinitializing = true
try {
mapInitVersion += 1
isInitializing.value = false
clearAttachRetryTimer()
if (mapView.value && !mapView.value.destroyed) {
mapView.value.destroy()
mapView.value = null
}
isMapInitialized.value = false
await nextTick()
await initializeMap()
} finally {
isRecoveryReinitializing = false
}
}
function reattachAndResizeMap(retry = 0) {
if (!mapView.value || mapView.value.destroyed) {
return
}
const activeContainer = getActiveMapContainer()
if (
!activeContainer ||
activeContainer.clientWidth === 0 ||
activeContainer.clientHeight === 0
) {
if (retry < 15) {
clearAttachRetryTimer()
attachRetryTimer = window.setTimeout(() => {
reattachAndResizeMap(retry + 1)
}, 140)
} else if (!isRecoveryReinitializing) {
void recoverMapByReinitialize()
}
return
}
clearAttachRetryTimer()
nextTick(() => {
if (!mapView.value || mapView.value.destroyed) {
return
}
if (mapView.value.container !== activeContainer) {
mapView.value.container = null
mapView.value.container = activeContainer
}
mapView.value.resize()
mapView.value.requestRender?.()
mapView.value
.goTo(
{
center: mapView.value.center,
zoom: mapView.value.zoom,
},
{ animate: false }
)
.catch(() => {
// Ignore interrupted goTo during rapid orientation changes.
})
})
}
async function initializeMap() {
if (isInitializing.value || (mapView.value && !mapView.value.destroyed)) {
return
}
const initVersion = ++mapInitVersion
isInitializing.value = true
try {
// Load modules of ArcGIS
loadModules([
'esri/config',
'esri/Map',
'esri/views/MapView',
'esri/geometry/Point',
'esri/Graphic',
'esri/layers/TileLayer',
]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => {
// Set apiKey
// esriConfig.apiKey =
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
const [esriConfig, Map, MapView, Point, Graphic, TileLayer] =
await loadModules([
'esri/config',
'esri/Map',
'esri/views/MapView',
'esri/geometry/Point',
'esri/Graphic',
// 'esri/layers/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`,
// })
// 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 map = new Map({
basemap: 'streets',
// basemap: 'arcgis-topographic',
// layers: [hillshadeLayer],
})
navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords
if (initVersion !== mapInitVersion) {
return
}
mapView.value = 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)
},
ui: {
components: [], // Empty array to remove all default UI components
},
const position = await new Promise<GeolocationPosition>(
(resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
})
}
)
//
const userPoint = new Point({ longitude, latitude })
const userSymbol = {
if (initVersion !== mapInitVersion) {
return
}
const { latitude, longitude } = position.coords
const mapContainer = await waitForActiveMapContainer()
if (!mapContainer || initVersion !== mapInitVersion) {
isMapInitialized.value = false
return
}
mapView.value = new MapView({
container: mapContainer,
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)
},
ui: {
components: [], // Empty array to remove all default UI components
},
})
await mapView.value.when()
if (initVersion !== mapInitVersion) {
if (mapView.value && !mapView.value.destroyed) {
mapView.value.destroy()
}
mapView.value = null
return
}
isMapInitialized.value = true
reattachAndResizeMap()
//
const userPoint = new Point({ longitude, latitude })
const userSymbol = {
type: 'picture-marker',
url: markerRedUrl,
width: '32px',
height: '32px',
}
const userGraphic = new Graphic({
geometry: userPoint,
symbol: userSymbol,
})
mapView.value.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: markerBlueUrl,
// width: '32px',
// height: '32px',
// }
// const poiGraphic = new Graphic({
// geometry: poiPoint,
// symbol: poiSymbol,
// })
// mapView.value.graphics.add(poiGraphic)
// // POI
// mapView.value.goTo({
// target: [userPoint, poiPoint],
// zoom: zoomMap.value,
// })
// // Mark map as initialized
// isMapInitialized.value = true
// 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) => {
if (initVersion !== mapInitVersion || !mapView.value) {
return
}
// 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/red.png',
url: markerBlueUrl,
width: '32px',
height: '32px',
}
const userGraphic = new Graphic({
geometry: userPoint,
symbol: userSymbol,
const poiGraphic = new Graphic({
geometry: poiPoint,
symbol: poiSymbol,
})
mapView.value.graphics.add(poiGraphic)
// POI
mapView.value.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
mapView.value.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.value.graphics.add(poiGraphic)
// POI
mapView.value.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.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.value.graphics.add(poiGraphic)
// POI
mapView.value.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch((error) => {
// console.error('Error fetching points of interest:', error)
})
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
})
.catch(() => {
if (initVersion !== mapInitVersion) {
return
}
poiPlaceName.value = poiPlaceName.value || 'ไม่พบข้อมูล'
updateLocation(latitude, longitude, poiPlaceName.value)
})
if (initVersion === mapInitVersion) {
reattachAndResizeMap()
}
} catch (error) {
if (initVersion === mapInitVersion) {
isMapInitialized.value = false
}
console.error('Error loading the map', error)
} finally {
if (initVersion === mapInitVersion) {
isInitializing.value = false
}
}
}
@ -281,12 +484,116 @@ const requestLocationPermission = () => {
)
}
// Also add a resize event listener as a fallback for orientation changes
// that might not trigger the Quasar screen watcher
let resizeTimeout: number | undefined
const handleWindowResize = () => {
// Debounce resize events to avoid excessive calls
if (resizeTimeout) {
clearTimeout(resizeTimeout)
}
resizeTimeout = window.setTimeout(() => {
handleMapResize()
}, 300)
}
/**
* Handle map resize when screen orientation changes
* ArcGIS MapView needs to be manually resized when its container changes dimensions
* Only call resize() if the map is fully initialized
*/
function handleMapResize() {
if (mapView.value && !mapView.value.destroyed && isMapInitialized.value) {
// Use nextTick to ensure the DOM has finished updating before resizing
reattachAndResizeMap()
}
}
/**
* Watch for screen size/orientation changes
* When the screen size changes (e.g., portrait to landscape), we need to re-initialize the map
* because the container div changes between desktop and mobile versions
*/
watch(
() => $q.screen.gt.xs,
async (newValue) => {
// Skip if no actual change (same layout)
if (newValue === currentScreenSize.value) {
return
}
currentScreenSize.value = newValue
if (!newValue) {
mobileMapExpanded.value = true
}
mapInitVersion += 1
isInitializing.value = false
clearAttachRetryTimer()
// Always destroy and re-initialize when screen size changes
// This ensures the map is attached to the correct container
if (mapView.value && !mapView.value.destroyed) {
mapView.value.destroy()
mapView.value = null
}
// Reset initialization state
isMapInitialized.value = false
// Wait for DOM to update with new layout
await nextTick()
await waitForActiveMapContainer(30, 100)
// Re-initialize the map
if (privacyStore.isAccepted) {
await initializeMap()
if (!mapView.value || mapView.value.destroyed) {
await recoverMapByReinitialize()
}
reattachAndResizeMap()
}
}
)
// Watch for initialPOI prop changes to update poiPlaceName
watch(
() => props.initialPOI,
(newValue) => {
if (newValue && newValue !== poiPlaceName.value) {
poiPlaceName.value = newValue
}
}
)
// Set up resize event listener on mount
onMounted(async () => {
window.addEventListener('resize', handleWindowResize)
// Component can be remounted after orientation/layout switches.
// Recreate map automatically when consent is already granted.
if (privacyStore.isAccepted && (!mapView.value || mapView.value.destroyed)) {
await waitForActiveMapContainer(30, 100)
await initializeMap()
reattachAndResizeMap()
}
})
// Cleanup map resources when component unmounts
onBeforeUnmount(() => {
clearAttachRetryTimer()
if (mapView.value) {
mapView.value.destroy()
mapView.value = null
}
// Clean up resize event listener
window.removeEventListener('resize', handleWindowResize)
// Clear timeout if it exists
if (resizeTimeout) {
clearTimeout(resizeTimeout)
}
})
defineExpose({
@ -295,8 +602,8 @@ defineExpose({
</script>
<template>
<!-- Loading skeleton -->
<div v-if="!poiPlaceName" class="col-12">
<!-- Loading skeleton - show until map is visible -->
<div v-if="!isMapVisible" class="col-12">
<q-skeleton
:height="$q.screen.gt.xs ? '35vh' : '45px'"
width="100%"
@ -306,16 +613,16 @@ defineExpose({
</div>
<q-card
v-show="poiPlaceName"
bordered
flat
class="col-12 bg-grey-2 shadow-0"
:class="{ 'map-hidden': !isMapVisible }"
:style="$q.screen.gt.xs ? ';' : 'border-radius: 20px'"
>
<div v-if="$q.screen.gt.xs">
<div
id="mapViewDisplay"
ref="mapElement"
:id="mapContainerId"
ref="desktopMapContainerRef"
style="height: 35vh; pointer-events: none"
></div>
@ -339,6 +646,7 @@ defineExpose({
<q-card v-else style="border-radius: 20px">
<q-expansion-item
v-model="mobileMapExpanded"
class="shadow-1 overflow-hidden bg-grey-4 text-left q-pa-xs"
style="border-radius: 20px"
dense
@ -355,16 +663,20 @@ defineExpose({
name="mdi-information-outline"
size="xs"
class="q-mr-xs cursor-pointer"
@click.stop="$q.dialog({ message: textTooltip, ok: 'ปิด' })"
@click.stop="$q.dialog({ message: textTooltip, ok: 'ตกลง' })"
/>
</q-item-label>
<q-item-label class="text-weight-medium text-grey-9">
{{ poiPlaceName }}
{{ poiPlaceName || '-' }}
</q-item-label>
</q-item-section>
</template>
<div id="mapViewDisplay" style="height: 20vh"></div>
<div
:id="mapContainerId"
ref="mobileMapContainerRef"
style="height: 20vh"
></div>
</q-expansion-item>
</q-card>
</q-card>
@ -374,4 +686,8 @@ defineExpose({
.expanAS.q-item__section--avatar {
min-width: 40px !important;
}
.map-hidden {
visibility: hidden;
}
</style>

View file

@ -1,9 +1,8 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, shallowRef, ref } from 'vue'
import { loadModules } from 'esri-loader'
import axios from 'axios'
import type { LocationObject } from '@/interface/index/Main'
import markerRedUrl from '@/assets/markers/marker-red.svg?url'
const emit = defineEmits(['update:location'])
@ -20,8 +19,67 @@ const apiKey = ref<string>(
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
)
const zoomMap = ref<number>(18)
const mapViewRef = shallowRef<any>(null)
const mapContainerRef = ref<HTMLElement | null>(null)
const mapRenderKey = ref<number>(0)
let resizeTimer: ReturnType<typeof setTimeout> | null = null
let resizeRetryCount = 0
const MAX_RESIZE_RETRY = 8
const isRecreatingMap = ref<boolean>(false)
function scheduleMapResize() {
if (resizeTimer) {
clearTimeout(resizeTimer)
}
// Delay a little to wait for viewport + CSS layout to settle after rotation.
resizeTimer = setTimeout(async () => {
await nextTick()
if (!mapViewRef.value || !mapContainerRef.value) {
return
}
const container = mapContainerRef.value
const mapView = mapViewRef.value
if (container.clientWidth === 0 || container.clientHeight === 0) {
if (resizeRetryCount < MAX_RESIZE_RETRY) {
resizeRetryCount += 1
scheduleMapResize()
}
return
}
resizeRetryCount = 0
// Force canvas to bind to the current element after repeated rotations.
if (mapView.container !== container) {
mapView.container = null
await nextTick()
mapView.container = container
}
mapView.resize()
mapView
.goTo(
{
center: mapView.center,
zoom: mapView.zoom,
},
{ animate: false }
)
.catch(() => {
// Ignore interruption errors from rapid orientation changes.
})
}, 300)
}
async function initializeMap() {
if (mapViewRef.value || !mapContainerRef.value) {
return
}
try {
// Load modules of ArcGIS
loadModules([
@ -67,7 +125,7 @@ async function initializeMap() {
// MapView
const mapView = new MapView({
container: 'mapViewDisplay', // div
container: mapContainerRef.value, // div
map: map,
center: {
latitude: latitude,
@ -79,10 +137,11 @@ async function initializeMap() {
components: [], // Empty array to remove all default UI components
},
})
mapViewRef.value = mapView
//
const markerSymbol = new PictureMarkerSymbol({
url: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png', //
url: markerRedUrl,
width: '32px',
height: '32px',
})
@ -98,9 +157,42 @@ async function initializeMap() {
position: 'top-right', // Search widget
})
// token .
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -111,52 +203,20 @@ async function initializeMap() {
x: longitude,
y: latitude,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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
: 'ไม่พบข้อมูล'
updateLocation(longitude, latitude, poiPlaceName.value)
})
updateLocation(longitude, latitude, poiPlaceName.value)
})
// })
let searchMarker: any = null
@ -186,9 +246,41 @@ async function initializeMap() {
symbol: markerSymbol,
})
if (searchMarker) {
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -199,52 +291,20 @@ async function initializeMap() {
x: longitude,
y: latitude,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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
: 'ไม่พบข้อมูล'
updateLocation(longitude, latitude, poiPlaceName.value)
})
updateLocation(longitude, latitude, poiPlaceName.value)
})
// })
}
mapView.graphics.add(searchMarker)
@ -275,9 +335,41 @@ async function initializeMap() {
} else {
searchMarker.geometry = point //
if (searchMarker) {
// 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: lon,
// y: lat,
// },
// 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,
// })
// updateLocation(latitude, longitude, poiPlaceName.value)
// })
// .catch(async () => {
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
@ -288,52 +380,20 @@ async function initializeMap() {
x: lon,
y: lat,
},
token: apiKey.value,
},
}
)
.then((response) => {
console.log('poi', response.data.location)
// 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,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch(async () => {
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: lon,
y: lat,
},
},
}
)
.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
: 'ไม่พบข้อมูล'
updateLocation(lat, lon, poiPlaceName.value)
})
updateLocation(lat, lon, poiPlaceName.value)
})
// })
}
}
@ -350,14 +410,64 @@ async function initializeMap() {
}
}
async function recreateMapOnOrientationChange() {
if (isRecreatingMap.value) {
return
}
isRecreatingMap.value = true
try {
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
if (mapViewRef.value) {
mapViewRef.value.destroy()
mapViewRef.value = null
}
mapRenderKey.value += 1
await nextTick()
await initializeMap()
scheduleMapResize()
} finally {
isRecreatingMap.value = false
}
}
onMounted(async () => {
await initializeMap()
window.addEventListener('resize', scheduleMapResize)
window.addEventListener('orientationchange', recreateMapOnOrientationChange)
window.addEventListener('pageshow', scheduleMapResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', scheduleMapResize)
window.removeEventListener(
'orientationchange',
recreateMapOnOrientationChange
)
window.removeEventListener('pageshow', scheduleMapResize)
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
if (mapViewRef.value) {
mapViewRef.value.destroy()
mapViewRef.value = null
}
})
</script>
<template>
<q-card bordered flat class="col-12 bg-grey-2 shadow-0">
<div id="mapViewDisplay" style="height: 35vh"></div>
<div :key="mapRenderKey" ref="mapContainerRef" style="height: 35vh"></div>
<div
:class="

View file

@ -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'
@ -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()
})

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, reactive, onMounted, watch, onBeforeUnmount } from 'vue'
import { ref, reactive, onMounted, watch, onBeforeUnmount, computed } from 'vue'
import { useQuasar } from 'quasar'
import { format } from 'date-fns'
import Camera from 'simple-vue-camera'
@ -9,6 +9,7 @@ import http from '@/plugins/http'
import { useCounterMixin } from '@/stores/mixin'
import { usePermissions } from '@/composables/usePermissions'
import { usePrivacyStore } from '@/stores/privacy'
import { usePositionKeycloakStore } from '@/stores/positionKeycloak'
import type { FormRef, OptionReason } from '@/interface/response/checkin'
@ -19,8 +20,27 @@ const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
const $q = useQuasar()
const { checkPrivacyAccepted } = usePermissions()
const privacyStore = usePrivacyStore()
const positionKeycloakStore = usePositionKeycloakStore()
const MOCK_CHECK_DELAY_MS = 800
//
const hasOrganization = computed(() => {
const data = positionKeycloakStore.dataPositionKeycloak
if (!data || !data.organization) return false
const org = data.organization
// null, undefined string
const hasValue = (val: string | null | undefined) => val && val.trim() !== ''
return !!(
hasValue(org.root) ||
hasValue(org.child1) ||
hasValue(org.child2) ||
hasValue(org.child3) ||
hasValue(org.child4)
)
})
const modalTime = ref<boolean>(false) // Dailog
const checkStatus = ref<string>('')
const statusCheckin = ref<boolean>(true) // ,
@ -69,7 +89,7 @@ const formattedM = ref()
const formattedH = ref()
const formattedHH = ref()
const formattedA = ref()
const clockInterval = ref<number | NodeJS.Timeout | undefined>()
const clockInterval = ref<number | undefined>()
/** function อัพเดทเวลา*/
function updateClock() {
@ -194,7 +214,7 @@ const availableCameras = ref<any[]>([])
const currentCameraIndex = ref<number>(0)
const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown')
const intervalId = ref<number | NodeJS.Timeout | undefined>(undefined) // interval
const intervalId = ref<number | undefined>(undefined) // interval
/**
* เรมจาก onMounted #1 เช status
@ -298,9 +318,17 @@ async function stopChecking() {
function identifyCameraType(label: string): 'front' | 'back' | 'unknown' {
const lowerLabel = label.toLowerCase()
if (lowerLabel.includes('front') || lowerLabel.includes('user') || lowerLabel.includes('face')) {
if (
lowerLabel.includes('front') ||
lowerLabel.includes('user') ||
lowerLabel.includes('face')
) {
return 'front'
} else if (lowerLabel.includes('back') || lowerLabel.includes('environment') || lowerLabel.includes('rear')) {
} else if (
lowerLabel.includes('back') ||
lowerLabel.includes('environment') ||
lowerLabel.includes('rear')
) {
return 'back'
}
return 'unknown'
@ -316,7 +344,7 @@ async function openCamera() {
if (!isPermissionCameraDenied.value) {
// change camera device
if (cameraIsOn.value) {
await camera.value?.stop()
camera.value?.stop()
} else {
await camera.value?.start()
const devices: any = await camera.value?.devices(['videoinput'])
@ -356,8 +384,9 @@ async function changeCamera(targetCameraType?: 'front' | 'back') {
return
}
const matchingCameras = devices.filter((device: any) =>
identifyCameraType(device.label || '') === targetCameraType
const matchingCameras = devices.filter(
(device: any) =>
identifyCameraType(device.label || '') === targetCameraType
)
if (matchingCameras.length > 0) {
@ -405,7 +434,7 @@ async function capturePhoto() {
const file = new File([imageBlob], fileName, { type: 'image/png' })
fileImg.value = file
//
await camera.value?.stop()
camera.value?.stop()
const url = URL.createObjectURL(imageBlob)
img.value = url
}
@ -466,7 +495,14 @@ async function validateForm() {
}
}
const mapRef = ref<InstanceType<typeof MapCheck> | null>(null)
const mapRefDesktop = ref<InstanceType<typeof MapCheck> | null>(null)
const mapRefMobile = ref<InstanceType<typeof MapCheck> | null>(null)
// Computed ref that returns the correct map ref based on screen size
const mapRef = computed(() =>
$q.screen.gt.xs ? mapRefDesktop.value : mapRefMobile.value
)
const timeChickin = ref<string>('') //,
/** function ยืนยันการลงเวลาเข้า - ออก*/
@ -663,22 +699,40 @@ async function requestCamera() {
/** Hook*/
onMounted(async () => {
isLoadingCheckTime.value = true
// clock
updateClock()
clockInterval.value = setInterval(updateClock, 1000)
startChecking() // status #1
// event listeners
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('pagehide', handleAppClose)
// checking
if (hasOrganization.value) {
isLoadingCheckTime.value = true
startChecking()
}
// privacy
if (privacyStore.isAccepted) {
mapRef.value?.requestLocationPermission()
requestCamera()
}
// event listeners
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('pagehide', handleAppClose)
})
// hasOrganization
// startChecking
watch(
hasOrganization,
(newValue) => {
if (newValue && !isLoadingCheckTime.value) {
isLoadingCheckTime.value = true
startChecking()
}
},
{ immediate: false }
)
onBeforeUnmount(() => {
resetCameraAndImage()
@ -727,6 +781,7 @@ watch(
>
<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">
@ -771,19 +826,34 @@ watch(
</div>
</div>
</div>
<div v-if="isDisabledCheckTime && msgCheckTime">
<div class="col-12 column items-center justify-center q-pa-xl">
<q-icon
name="mdi-map-marker-radius"
size="8em"
class="text-primary q-mb-md"
/>
<div class="text-h6 text-center text-red q-mt-sm">
*{{ msgCheckTime }}
</div>
</div>
</div>
<div
class="col-xs-12 col-md-11 q-pa-md q-col-gutter-md row q-pt-lg"
v-if="!isDisabledCheckTime"
>
<div class="col-xs-12 col-sm-8 gt-xs">
<div class="col-12">
<MapCheck
v-if="$q.screen.gt.xs"
ref="mapRef"
ref="mapRefDesktop"
:initialPOI="formLocation.POI"
@update:location="updateLocation"
/>
</div>
</div>
<div class="col-xs-12 col-sm-4">
<div class="col-xs-12 col-sm-4 gt-xs">
<q-card
:class="
$q.screen.xs ? 'card-container-xs' : 'card-container'
@ -893,6 +963,84 @@ watch(
<!-- กรอกขอม หนามอถ -->
<div class="col-12 row q-col-gutter-y-md" v-if="$q.screen.xs">
<!-- กลอง หนามอถ -->
<div class="col-12">
<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
class="absolute-bottom text-subtitle2 text-center q-py-sm"
style="background: #00000021"
>
<q-btn
v-if="availableCameras.length > 1 && img == null"
round
icon="flip_camera_ios"
size="16px"
style="background: #424242; color: white"
@click="switchCamera"
unelevated
class="q-mr-xs"
/>
<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"
unelevated
/>
</div>
</div>
</div>
</q-card>
</div>
<div class="col-12" v-if="!isDisabledCheckTime">
<q-card
flat
@ -965,8 +1113,13 @@ watch(
</q-card>
</div>
<div class="col-12" v-if="$q.screen.xs">
<MapCheck ref="mapRef" @update:location="updateLocation" />
<!-- Map หนามอถ -->
<div class="col-12">
<MapCheck
ref="mapRefMobile"
:initialPOI="formLocation.POI"
@update:location="updateLocation"
/>
</div>
</div>
<!-- กรอกขอม หนามอถ -->
@ -1082,7 +1235,7 @@ watch(
>
<q-separator />
<div class="col-12 q-pa-md">
<p
<!-- <p
:class="
$q.screen.gt.xs
? 'text-red text-caption '
@ -1090,7 +1243,7 @@ watch(
"
>
*หมายเหต คลกลงเวลาเขางานแลวระบบจะลงเวลาทนท
</p>
</p> -->
<q-btn
:label="
!statusCheckin && inQueue
@ -1122,10 +1275,10 @@ watch(
</div>
</div>
<div v-if="isDisabledCheckTime && $q.screen.gt.xs" class="col-12">
<!-- <div v-if="isDisabledCheckTime && $q.screen.gt.xs" class="col-12">
<q-separator />
<div class="text-red q-pa-md">*{{ msgCheckTime }}</div>
</div>
</div> -->
</div>
</q-card>
</div>
@ -1197,7 +1350,7 @@ watch(
<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
<!-- <p
:class="
$q.screen.gt.xs
? 'text-red text-caption '
@ -1205,7 +1358,7 @@ watch(
"
>
*หมายเหต คลกลงเวลาเขางานแลวระบบจะลงเวลาทนท
</p>
</p> -->
<q-btn
:label="
!statusCheckin && inQueue
@ -1235,10 +1388,10 @@ watch(
:loading="inQueue"
/>
</div>
<div v-if="isDisabledCheckTime" class="col-12">
<!-- <div v-if="isDisabledCheckTime" class="col-12">
<q-separator />
<div class="text-red q-pa-md text-left">*{{ msgCheckTime }}</div>
</div>
</div> -->
</q-footer>
<!-- แสดงการลงเวลา -->
@ -1367,4 +1520,18 @@ watch(
rgba(2, 169, 152, 1) 100%
);
}
</style>
.icon-bounce {
animation: bounce 2s infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
</style>

View file

@ -182,6 +182,22 @@ async function fetchKeycloakPosition() {
if (existingData) {
//
// (organization)
const hasOrganization =
existingData.organization &&
(existingData.organization.root ||
existingData.organization.child1 ||
existingData.organization.child2 ||
existingData.organization.child3 ||
existingData.organization.child4)
if (!hasOrganization) {
// redirect NoPosition
router.replace('/no-position')
return
}
privacyStore.modalPrivacy = !existingData.privacyCheckin
privacyStore.setAccepted(existingData.privacyCheckin)
@ -208,6 +224,24 @@ async function fetchKeycloakPosition() {
},
}
// (organization)
// redirect NoPosition
const hasOrganization =
keycloakData.organization &&
(keycloakData.organization.root ||
keycloakData.organization.child1 ||
keycloakData.organization.child2 ||
keycloakData.organization.child3 ||
keycloakData.organization.child4)
if (!hasOrganization) {
// ()
positionKeycloakStore.setPositionKeycloak(keycloakData)
// Redirect NoPosition
router.replace('/no-position')
return
}
positionKeycloakStore.setPositionKeycloak(keycloakData)
privacyStore.modalPrivacy = !keycloakData.privacyCheckin
privacyStore.setAccepted(keycloakData.privacyCheckin)

View file

@ -460,7 +460,7 @@ onMounted(async () => {
<div class="col-12 text-right">
<q-separator />
<div class="col-12 q-pa-md">
<p
<!-- <p
:class="
$q.screen.gt.xs
? 'text-red text-caption '
@ -468,7 +468,7 @@ onMounted(async () => {
"
>
*หมายเหต คลกลงเวลาเขางานแลวระบบจะลงเวลาทนท
</p>
</p> -->
<q-btn
:label="
stetusCheckin == true ? 'ลงเวลาเข้างาน' : 'ลงเวลาออกงาน'

View file

@ -0,0 +1,106 @@
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { useRouter } from 'vue-router'
import { useCounterMixin } from '@/stores/mixin'
import { usePositionKeycloakStore } from '@/stores/positionKeycloak'
import { logout } from '@/plugins/auth'
import CustomComponent from '@/components/CustomDialog.vue'
const $q = useQuasar()
const router = useRouter()
const mixin = useCounterMixin()
const positionKeycloakStore = usePositionKeycloakStore()
const { showLoader, hideLoader } = mixin
/**
* งกนจดการเมอผใชกดปมตกลง
* ดำเนนการ logout โดยตรง
*/
function handleOkClick() {
performLogout()
}
/**
* งกนดำเนนการออกจากระบบ
*/
async function performLogout() {
showLoader()
try {
// positionKeycloak logout
positionKeycloakStore.clearPositionKeycloak()
await logout()
} catch (error) {
console.error('Logout error:', error)
} finally {
setTimeout(() => {
hideLoader()
}, 1000)
}
}
</script>
<template>
<div
class="fullscreen bg-secondary text-white text-center q-pa-sm flex flex-center"
>
<div class="no-position-container">
<!-- Icon แสดงความผดปกต -->
<div class="icon-container">
<q-icon name="mdi-account-off-outline" size="100px" color="white" />
</div>
<!-- วขอหล -->
<div class="text-h4 q-mt-lg q-mb-md">ไมพบขอมลสงก</div>
<!-- รายละเอยด -->
<div class="text-h6 q-mb-lg text-weight-regular">
านยงไมงกดในโครงสรางองคกร<br />
กรณาตดตอเจาหนาทเบอร 1171
<br />เพอดำเนนการเพมขอม
</div>
<div class="text-weight-regular">
เมอเจาหนาทไดเพมทานในโครงสรางองคกรเรยบรอยแล
กรณาเขาสระบบใหมกคร
</div>
<!-- มตกลง -->
<q-btn
class="q-mt-xl"
color="white"
text-color="secondary"
unelevated
label="ตกลง"
no-caps
size="lg"
padding="md xl"
@click="handleOkClick"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.no-position-container {
max-width: 600px;
padding: 2rem;
}
.icon-container {
opacity: 0.9;
}
.text-h4 {
font-weight: 700;
}
.text-h6 {
line-height: 1.8;
opacity: 0.95;
}
.q-btn {
border-radius: 8px;
font-weight: 600;
min-width: 150px;
}
</style>

View file

@ -23,7 +23,7 @@ export default defineConfig({
cleanupOutdatedCaches: true,
globPatterns: ['**/*.*'],
},
includeAssets: ['icons/safari-pinned-tab.svg'],
includeAssets: ['icons/safari-pinned-tab.svg', 'src/assets/markers/*.svg'],
manifest: {
name: 'HRMS-Checkin',
short_name: 'HRMS Checkin',