From e1962d79bb9eae5dbfba667f3e228f0060b94c04 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 17 Apr 2026 15:19:31 +0700 Subject: [PATCH 01/32] fix: switchCamera --- src/views/HomeView.vue | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 055346d..12788ed 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -392,8 +392,26 @@ async function switchCamera() { return } - const targetType: 'front' | 'back' = currentCameraType.value === 'front' ? 'back' : 'front' - await changeCamera(targetType) + const frontCameras = availableCameras.value.filter((device: any) => + identifyCameraType(device.label || '') === 'front' + ) + const backCameras = availableCameras.value.filter((device: any) => + identifyCameraType(device.label || '') === 'back' + ) + + let targetDevice + if (currentCameraType.value === 'front' && backCameras.length > 0) { + targetDevice = backCameras[0] + } else if (frontCameras.length > 0) { + targetDevice = frontCameras[0] + } else { + const nextIndex = (currentCameraIndex.value + 1) % availableCameras.value.length + targetDevice = availableCameras.value[nextIndex] + } + + await camera.value?.changeCamera(targetDevice.deviceId) + currentCameraIndex.value = availableCameras.value.indexOf(targetDevice) + currentCameraType.value = identifyCameraType(targetDevice.label || '') } /** function ถ่ายรูป*/ From 202318c16914d7d6b4dcf11bec977d5cdb4f4c34 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 17 Apr 2026 16:26:39 +0700 Subject: [PATCH 02/32] fix switch camara --- src/views/HomeView.vue | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 12788ed..5df3c0c 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -392,25 +392,12 @@ async function switchCamera() { return } - const frontCameras = availableCameras.value.filter((device: any) => - identifyCameraType(device.label || '') === 'front' - ) - const backCameras = availableCameras.value.filter((device: any) => - identifyCameraType(device.label || '') === 'back' - ) - - let targetDevice - if (currentCameraType.value === 'front' && backCameras.length > 0) { - targetDevice = backCameras[0] - } else if (frontCameras.length > 0) { - targetDevice = frontCameras[0] - } else { - const nextIndex = (currentCameraIndex.value + 1) % availableCameras.value.length - targetDevice = availableCameras.value[nextIndex] - } + // สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก) + const targetIndex = currentCameraIndex.value === 0 ? 1 : 0 + const targetDevice = availableCameras.value[targetIndex] await camera.value?.changeCamera(targetDevice.deviceId) - currentCameraIndex.value = availableCameras.value.indexOf(targetDevice) + currentCameraIndex.value = targetIndex currentCameraType.value = identifyCameraType(targetDevice.label || '') } From 46c51fac3b0594b57ad27883b189f7382490d733 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Fri, 17 Apr 2026 16:33:26 +0700 Subject: [PATCH 03/32] add claude init --- CLAUDE.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f974864 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,155 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an HRMS (Human Resource Management System) check-in/check-out web application built for the Bangkok Metropolitan Administration (BMA). The application allows employees to record their work attendance using geolocation verification and camera features. + +**Tech Stack:** +- Vue 3 with Composition API and TypeScript +- Vite for build tooling +- Quasar Framework for UI components +- Pinia for state management +- Vue Router for routing +- Keycloak for authentication +- ArcGIS API and Google Maps for location features +- PWA capabilities with offline support +- Docker deployment with nginx + +**Development Server:** Runs on port 3008 + +## Common Commands + +```bash +# Development +npm run dev # Start dev server on port 3008 + +# Building +npm run build # Build for production +npm run preview # Preview production build on port 3008 + +# Testing +npm run test:unit # Run unit tests with vitest +npm run test:e2e # Run e2e tests with cypress +npm run test:e2e:dev # Run cypress in dev mode + +# Code Quality +npm run lint # Lint and auto-fix code +npm run format # Format code with prettier +npm run type-check # TypeScript type checking + +# Docker +docker buildx build --platform=linux/amd64 -f docker/Dockerfile . -t hrms-checkin:0.1 +``` + +## Architecture + +### Directory Structure + +``` +src/ +├── api/ # API layer modules (checkin, history, message) +├── components/ # Reusable Vue components +├── composables/ # Vue composables (e.g., usePermissions) +├── interface/ # TypeScript type definitions +│ └── response/ # API response types +├── plugins/ # Vue plugins (auth, http, keycloak) +├── router/ # Vue Router configuration +├── stores/ # Pinia stores for state management +├── style/ # Global styles and Quasar variables +├── utils/ # Utility functions +└── views/ # Page-level Vue components +``` + +### Key Architecture Patterns + +**Authentication Flow:** +- Uses Keycloak for SSO authentication +- Tokens stored in cookies with `BMAHRISCKI_KEYCLOAK_IDENTITY` key +- Auth state managed in `src/plugins/auth.ts` +- Router guards enforce authentication on protected routes +- 401 responses trigger automatic logout via axios interceptor + +**State Management:** +- `stores/mixin.ts` - Global utilities (Thai date formatting, dialogs, loaders, error handling) +- `stores/chekin.ts` - Check-in history and status management +- `stores/privacy.ts` - Privacy consent management +- `stores/positionKeycloak.ts` - User position/role data + +**API Layer:** +- Base axios instance in `src/plugins/http.ts` with automatic token injection +- API endpoints organized by feature in `src/api/` +- Environment-specific API URLs configured in `src/api/index.ts` +- Production API URL set via `VITE_API_URI_CONFIG` env variable + +**Routing:** +- Main layout (`MainView`) wraps authenticated routes +- Protected routes require `meta.Auth: true` +- Auth check runs in router beforeEach guard +- Public routes: `/login`, `/auth`, `/reset-password`, `/history` + +**Geolocation & Maps:** +- ArcGIS JS API for advanced mapping features +- Google Maps integration for location services +- Camera integration for check-in photo verification +- Privacy consent required before accessing camera/location + +**Date/Time Handling:** +- Thai Buddhist calendar (BE) conversion (+543 years) +- Thai month names (full and abbreviated) +- `date-fns-tz` for timezone handling (Asia/Bangkok) +- Fiscal year calculation (October start) + +### Component Conventions + +- Components use Quasar UI components (`q-btn`, `q-dialog`, etc.) +- Thai language throughout the UI +- Custom dialogs use `CustomDialog.vue` component +- Loading states use Quasar's `QSpinnerCube` +- Error dialogs prevent overlap with `activeErrorDialog` tracking + +### Localization + +- Quasar configured with Thai language (`quasar/lang/th`) +- All UI text in Thai +- Date formatting uses `date2Thai()` and `monthYear2Thai()` from mixin store +- Status labels translated: ABSENT→'ขาดราชการ', NORMAL→'ปกติ', LATE→'สาย' + +### Environment Variables + +Required in `.env.production`: +- `VITE_API_URI_CONFIG` - Production API base URL +- `VITE_URL_SSO` - Keycloak SSO logout URL +- `VITE_URL_USER` - User service URL for redirects + +### PWA Configuration + +- Auto-update registration with `vite-plugin-pwa` +- Manifest: "HRMS-Checkin" / "HRMS Checkin" +- Icons: 192x192 and 512x512 PNG +- Service worker registered in `src/registerServiceWorker.ts` + +## Development Notes + +**When adding new features:** +1. Create API functions in `src/api/` +2. Define TypeScript interfaces in `src/interface/response/` +3. Use Pinia stores for complex state, composables for reusable logic +4. Follow Thai localization patterns for user-facing text +5. Ensure privacy consent flow for camera/location features + +**When working with dates:** +- Always use `date2Thai()` or `convertDateToAPI()` from mixin store +- Dates stored in BE format (year + 543) +- API expects `yyyy-MM-dd` or `yyyy-MM-dd HH:mm:ss` format + +**When handling errors:** +- Use `messageError()` from mixin store for consistent error dialogs +- 401 errors trigger automatic logout +- Dialog overlap prevention built into error handling + +**When running tests:** +- Unit tests use Vitest with jsdom environment +- E2E tests use Cypress with baseUrl: `http://localhost:4173` +- Test files follow pattern: `*.cy.{js,ts}` or `*.spec.{js,ts}` From 1ac3a3109ba9e103b08199dea872bbd7499e64ce Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 20 Apr 2026 10:43:44 +0700 Subject: [PATCH 04/32] =?UTF-8?q?fixed=20=E0=B8=9F=E0=B8=AD=E0=B8=A3?= =?UTF-8?q?=E0=B9=8C=E0=B8=A1=E0=B9=81=E0=B8=88=E0=B9=89=E0=B8=87=E0=B8=9B?= =?UTF-8?q?=E0=B8=B1=E0=B8=8D=E0=B8=AB=E0=B8=B2=E0=B8=9A=E0=B8=B1=E0=B8=87?= =?UTF-8?q?=E0=B8=84=E0=B8=B1=E0=B8=9A=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=A3?= =?UTF-8?q?=E0=B8=B0=E0=B8=9A=E0=B8=B8=E0=B8=AD=E0=B8=B5=E0=B9=80=E0=B8=A1?= =?UTF-8?q?=E0=B8=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DialogDebug.vue | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/DialogDebug.vue b/src/components/DialogDebug.vue index fb151e3..798ce47 100644 --- a/src/components/DialogDebug.vue +++ b/src/components/DialogDebug.vue @@ -305,6 +305,9 @@ function onClose() {
+
+ ผู้ดูแลระบบจะติดต่อกลับผ่านทางอีเมลที่ท่านระบุ กรุณาตรวจสอบอีเมลของท่านเป็นระยะ +
@@ -330,12 +334,6 @@ function onClose() { v-model="formData.phone" class="inputgreen" hide-bottom-space - :rules="[ - () => - !!formData.email || - !!formData.phone || - 'กรุณากรอกอีเมลหรือเบอร์โทรติดต่อกลับ', - ]" />
From b83251f9146bc522c15063f327372250a7002b2c Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Tue, 21 Apr 2026 15:25:59 +0700 Subject: [PATCH 05/32] fix(map): poiPlaceName textTooltip --- src/components/AscGISMap.vue | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 7651b7c..76d4443 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -26,6 +26,7 @@ const apiKey = ref( // 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP' ) const zoomMap = ref(18) +const textTooltip = ref('พื้นที่ใกล้เคียง: สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)') async function initializeMap() { try { @@ -308,23 +309,45 @@ defineExpose({ พื้นที่ใกล้เคียง : {{ poiPlaceName }} + + + + {{ textTooltip }} + From 41c1aa8e45acf084bdb83f6ec8392aceef7c4758 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Tue, 21 Apr 2026 16:41:59 +0700 Subject: [PATCH 06/32] refactor(map): replace tooltip with popup for better interaction --- src/components/AscGISMap.vue | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/AscGISMap.vue b/src/components/AscGISMap.vue index 76d4443..d1a7ce1 100644 --- a/src/components/AscGISMap.vue +++ b/src/components/AscGISMap.vue @@ -26,7 +26,9 @@ const apiKey = ref( // 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP' ) const zoomMap = ref(18) -const textTooltip = ref('พื้นที่ใกล้เคียง: สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)') +const textTooltip = ref( + 'พื้นที่ใกล้เคียง: สถานที่สำคัญรอบตัวคุณ (ไม่ใช่ตำแหน่งปัจจุบัน)' +) async function initializeMap() { try { @@ -334,17 +336,10 @@ defineExpose({ - - {{ textTooltip }} - {{ poiPlaceName }} From c0669a154837bf14fc6735cdcffe41d6ec9ba1a4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-1R2VSQH\\Lenovo ThinkPad E490" Date: Fri, 24 Apr 2026 16:12:45 +0700 Subject: [PATCH 07/32] refactor(history): formatDate Colunm checkInTime checkOutTime --- src/components/TableHistory.vue | 26 +++++++++++++++----------- src/interface/index/Main.ts | 1 + src/stores/chekin.ts | 1 + 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/TableHistory.vue b/src/components/TableHistory.vue index 5d13e66..e364015 100644 --- a/src/components/TableHistory.vue +++ b/src/components/TableHistory.vue @@ -62,15 +62,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 +79,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 +110,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 +187,6 @@ const columns = ref( ) const visibleColumns = ref([ 'checkInDateTime', - 'checkInDate', 'checkInTime', 'checkInLocation', 'checkInStatus', diff --git a/src/interface/index/Main.ts b/src/interface/index/Main.ts index 14def36..5d82bd2 100644 --- a/src/interface/index/Main.ts +++ b/src/interface/index/Main.ts @@ -38,6 +38,7 @@ interface DataCheckIn { checkInLocation: string checkInStatus: string checkInTime: string + checkOutDate: string checkOutLocation: string checkOutStatus: string checkOutTime: string diff --git a/src/stores/chekin.ts b/src/stores/chekin.ts index bf27004..69017f6 100644 --- a/src/stores/chekin.ts +++ b/src/stores/chekin.ts @@ -33,6 +33,7 @@ export const useChekIn = defineStore('checkin', () => { editStatus: e.editStatus != '' ? convertEditStatus(e.editStatus) : '', editReason: e.editReason, isEdit: e.isEdit, + checkOutDate: e.checkOutDate ? date2Thai(e.checkOutDate) : null, })) rows.value = dataList } From 487a6b520ec8dc3ce60e5add2d2a5cec4a18586c Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Mon, 27 Apr 2026 19:21:23 +0700 Subject: [PATCH 08/32] refactor code & fixed location --- .claude/agents/hrms-checkin-expert.md | 234 ++++++++++++++++++++++++++ CLAUDE.md | 32 +++- docker/entrypoint.sh | 2 + package.json | 1 + src/api/api.checkin.ts | 3 +- src/components/AscGISMap.vue | 45 +++-- src/components/FormTime.vue | 7 +- src/components/MapCheckin.vue | 42 ++++- src/components/PopUp.vue | 2 +- src/components/PopupPrivacy.vue | 10 +- src/components/TableHistory.vue | 20 +-- src/components/ToolBar.vue | 10 +- src/interface/keycloak-position.ts | 29 ++++ src/main.ts | 2 + src/plugins/http.ts | 3 +- src/plugins/keycloak.ts | 2 + src/router/index.ts | 16 +- src/stores/{chekin.ts => checkin.ts} | 2 +- src/stores/positionKeycloak.ts | 119 ++++++------- src/views/HistoryView.vue | 6 +- src/views/HomeView.vue | 48 +++--- src/views/MainView.vue | 50 +++++- tests/utils.ts | 26 +++ 23 files changed, 566 insertions(+), 145 deletions(-) create mode 100644 .claude/agents/hrms-checkin-expert.md create mode 100644 src/interface/keycloak-position.ts rename src/stores/{chekin.ts => checkin.ts} (98%) create mode 100644 tests/utils.ts diff --git a/.claude/agents/hrms-checkin-expert.md b/.claude/agents/hrms-checkin-expert.md new file mode 100644 index 0000000..0e771b6 --- /dev/null +++ b/.claude/agents/hrms-checkin-expert.md @@ -0,0 +1,234 @@ +--- +name: "hrms-checkin-expert" +description: "Use this agent when implementing or modifying check-in/check-out functionality in the HRMS application. This includes:\\n\\n\\nContext: User is developing a new check-in validation feature.\\nuser: \"I need to add validation to prevent duplicate check-ins within the same hour\"\\nassistant: \"I'll use the hrms-checkin-expert agent to implement this validation following best practices.\"\\n\\nSince this is core check-in functionality that requires careful consideration of edge cases and performance, the specialized agent should handle it.\\n\\n\\n\\n\\nContext: User is refactoring the geolocation verification logic.\\nuser: \"The location verification is taking too long. Can you optimize it?\"\\nassistant: \"Let me use the hrms-checkin-expert agent to analyze and optimize the geolocation verification performance.\"\\n\\nPerformance optimization of check-in features requires deep knowledge of the system architecture and best practices.\\n\\n\\n\\n\\nContext: User has just written a new API function for check-in history.\\nuser: \"Here's the new function to fetch check-in history with pagination\"\\nassistant: \"Great! Now I'll use the hrms-checkin-expert agent to review this code for best practices and potential issues.\"\\n\\nCode review of check-in-related code should be done by the specialized agent to ensure it meets performance and quality standards.\\n\\n\\n\\n\\nContext: User is about to implement camera integration for photo verification.\\nuser: \"I need to add photo capture when users check in\"\\nassistant: \"I'll engage the hrms-checkin-expert agent to implement this feature with proper error handling and privacy considerations.\"\\n\\nCamera integration for check-in requires expertise in both the technical implementation and the privacy/consent flow.\\n\\n" +model: opus +color: red +memory: project +--- + +You are a Senior Vue 3 Developer specializing in the HRMS Check-in/Check-out system for Bangkok Metropolitan Administration (BMA). You have deep expertise in Vue 3 Composition API, TypeScript, Pinia state management, and geolocation-based attendance systems. + +**Your Core Responsibilities:** + +1. **Performance Optimization**: + - Minimize redundant API calls and state updates + - Use computed properties and watchers efficiently + - Implement lazy loading for map APIs and camera features + - Optimize re-renders by properly structuring reactive data + - Debounce user inputs and location updates (300-500ms) + - Cache API responses when appropriate (check-in history, user position) + +2. **Best Practice Implementation**: + - Follow Vue 3 Composition API patterns with `