commit ca3c00b6a1c7ac116630cae7befc0c87e4264b42 Author: waruneeauy Date: Mon Dec 16 17:12:39 2024 +0700 start project diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..871e4af --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +KC_URL="https://keycloak-server.com" +KC_REALMS="your-realm" + +VITE_API_URI_CONFIG="https://api-server/api/v1" +VITE_CLIENTID_KEYCLOAK="your-client-id" + +SSO_COOKIE_NAME="sso" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1f03e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..05b99c6 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# HRMS SSO + +## Project setup + +``` +npm install +``` + +### Compiles and hot-reloads for development + +``` +npm run dev +``` + +### Compiles and minifies for production + +``` +npm run build +npm run preview +``` + +### Format and fixes files + +``` +npm run format +``` + +### Customize configuration + +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000..4a22885 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', + baseUrl: 'http://localhost:4173', + }, +}) diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..cfa0071 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,19 @@ +# docker buildx build --platform=linux/amd64 -f docker/Dockerfile . -t hrms-git.chin.in.th/bma-hrms/hrms-checkin:0.1 + +# Build Stage +FROM node:20-alpine as build-stage +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY ./ . +RUN npm run build + +# Production Stage +FROM nginx:stable-alpine AS production-stage +RUN mkdir /app +COPY --from=build-stage /app/dist /app +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod u+x /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..ea4c9ca --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +ROOT_DIR=/app + +# Replace env vars in JavaScript files +echo "Replacing env constants in JS" +for file in $ROOT_DIR/assets/app.*.js* $ROOT_DIR/js/app.*.js* $ROOT_DIR/index.html $ROOT_DIR/precache-manifest*.js $ROOT_DIR/assets/index*.js*; +do + echo "Processing $file ..."; + + sed -i 's|VITE_API_URI_CONFIG|'${VITE_API_URI_CONFIG}'|g' $file + sed -i 's|VITE_URL_SSO|'${VITE_URL_SSO}'|g' $file + +done + +echo "Starting Nginx" +nginx -g 'daemon off;' + diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6f61d6c --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,30 @@ +user nginx; +worker_processes 1; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; +events { + worker_connections 1024; +} +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + server { + listen 80; + server_name localhost; + location / { + root /app; + index index.html; + try_files $uri $uri/ /index.html; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/index.html b/index.html new file mode 100644 index 0000000..12d62fd --- /dev/null +++ b/index.html @@ -0,0 +1,34 @@ + + + + + + + HRMS SSO + + + +
+ + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e58ff7e --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "hrms-sso", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "run-p build-only", + "preview": "vite preview --port 3002", + "build-only": "vite build", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier ./src --write" + }, + "dependencies": { + "@arcgis/core": "^4.28.10", + "@quasar/extras": "^1.15.8", + "@vuepic/vue-datepicker": "^5.2.1", + "keycloak-js": "^22.0.2", + "moment": "^2.29.4", + "pinia": "^2.1.4", + "quasar": "^2.17.5", + "register-service-worker": "^1.7.2", + "simple-vue-camera": "^1.1.3", + "vite-plugin-pwa": "^0.16.7", + "vue": "^3.4.15", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@quasar/vite-plugin": "^1.3.0", + "@rushstack/eslint-patch": "^1.1.4", + "@types/jsdom": "^20.0.1", + "@types/node": "^18.18.10", + "@types/vue-router": "^2.0.0", + "@vitejs/plugin-vue": "^4.0.0", + "@vitejs/plugin-vue-jsx": "^3.0.0", + "@vue/eslint-config-prettier": "^7.0.0", + "@vue/eslint-config-typescript": "^11.0.0", + "@vue/test-utils": "^2.2.6", + "@vue/tsconfig": "^0.1.3", + "cypress": "^12.0.2", + "eslint": "^8.22.0", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-vue": "^9.3.0", + "jsdom": "^20.0.3", + "npm-run-all": "^4.1.5", + "prettier": "^2.7.1", + "sass": "^1.32.12", + "start-server-and-test": "^1.15.2", + "typescript": "~4.7.4", + "vite": "^4.0.0", + "vitest": "^0.25.6", + "vue-tsc": "^1.0.12" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4d31d3e Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..ea76d33 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/api/api.org.ts b/src/api/api.org.ts new file mode 100644 index 0000000..086e557 --- /dev/null +++ b/src/api/api.org.ts @@ -0,0 +1,7 @@ +import env from "./index"; +const org = `${env.API_URI}/org`; +// const log = `${env.API_URI}/log`; + +export default { + org, +}; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..8069fca --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,29 @@ +/**config api */ +import { ref } from 'vue' + +const env = ref(process.env.NODE_ENV || 'development') +export const apiUrlConfig = import.meta.env.VITE_API_URI_CONFIG +// if (process.env.VUE_APP_TEST) { +// env = "test"; +// } + +const config = ref({ + development: { + // API_URI: "https://localhost:7260/api", + API_URI: 'https://bma-ehr.frappet.synology.me/api/v1', + }, + test: { + API_URI: 'http://localhost:5010/api/v1', + }, + production: { + API_URI: apiUrlConfig, + }, +}) + +const API_URI = ref(config.value[env.value].API_URI) + +export default { + env: env.value, + config: config.value, + API_URI: API_URI.value, +} diff --git a/src/app.config.ts b/src/app.config.ts new file mode 100644 index 0000000..42aca54 --- /dev/null +++ b/src/app.config.ts @@ -0,0 +1,13 @@ +/**ใช้รวมไฟล์ย่อยๆ ของ api แต่ละไฟล์ */ + +/** API ระบบลงเวลา */ +import org from "@/api/api.org"; + +const API = { + /**message */ + ...org, +}; + +export default { + API: API, +}; diff --git a/src/assets/key/BMA b/src/assets/key/BMA new file mode 100644 index 0000000..5088b1a --- /dev/null +++ b/src/assets/key/BMA @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEApVYvTREuM3rWeab26+NP1Vg7t8Y79tvfPhMkhqhRv2E4fWuq +csyGsazRz7mb0B9qe/QdsRNh5rgKkyxUIUYVIhSgWl2uEzjVzXBevvm8u7Akg1q+ +Wk7SPIEf4GixxMIfNWQwebpFXhndg7WcQRz86AQULykLhJFD9aibDUQQTWLkkaun +VxL8r9iCuEWj6JlhHX5ao1fugQPgOhrTqt3XPbEUqyxaTGlWTGN/H2TxDjPHwSUW +XR37+5LVtR+pJRKirfJDrY1wr40SpTVJ+h191/lejWLv3CXWSxY9oVnYBP3P5j+4 +Kq0Z7Et1gvBy+3mOsBTrvD8T0InBf0eOLD896Q4s79pkU905ext24FXfDpS8ZN0r +i2OVoLmc+y87z/TfpTgZdvbBzrdsGXnmYrfre4//DfigtYuydoKZRp9AqG3pI0sv +GNVcwlmTfhCl7LcS6Xu2o8WpsNgQuJ3Pgm7AA+FOrICe6oruMXyo4DaNmGKxt8kl +TfqjGA87dD+T/meV3hQ38KIXD750JEbbDVzSk+yRIg66mRg8f6oOHj5qq12Iv5Cf +7n3JQNB4zEsvPpBWUq3s6riuxWtrJQA0ofPebEEuVug/eSy2LOD5HhVzcqtC9hbi +dg8gBeAnPqI0oLpLBUJnROOhbtELrdhILBxElewh2l4t2xvGG9hpAvjPp00CAwEA +AQKCAgB2N+WiaUJrtM1eNrAfgm020twAT1HY9OXu8KkRT5EEEnPd2foKE4vLxxJO +QRzT92KgNrB0SLOb0MRe7zdIg1/g+nadppYtUFovhsV4MAFvAkdZVKz+zZUthfZQ +8wsI8PR3rKesoi+vVTc6UcTkGeIL077K6cI+i8/X+zLCjYRKkQd10RLaA82BvoHZ +WJIWYnU6LXqJiPoUbb0KTtxCNFUO22s36YK3WCpIfGwM+pQR35xY0jfnZOUjwJ3l +4DmFNIn1bmBN2/BS1cAbOLsoh6XPo5Kj7bYr5zIOhlyS3jbDeugIbk801IjQmDPx +6BOPFB+eb5cPBtsYJSeh5nwVzzJoJ3Dx1t4U2g3uEzf4McGYvosNNEVMCzxlVWc4 +VEqmzCZuUojFkXIPV0/boxFWS+Q/4Epv6lqSLSiRgKa9WIdZHwF9xyF9SnZjNQOK +K6wlH6uw57aHTBfuCCXWUBnigNt7SuDNnEiSGnQUjwqi+Z+U9HDMow3VaaE6g7qs +OzPM8fDd02mqLOwnZKN2/xecIg+YFPblziX/7nlYlyqHW1mkjJucsFdi6JCtKhK5 +AgYe3RFqdH4hMv9w9T0aw/EIH2Z8AW/Z9aPlKYft0qmgDODYbyfY7z4gWVbAV1nW +kb2UPAVTJ1thCz7W2ea1RYEXsMtacf6o+D1YN9DgKJZapRwFAQKCAQEAzeDCpgiD +Q4Kcz8N/jBm6ZDhuFeZlB0SIF76/3p9PZ5Vq3uqC5eVwuM0BWR7/7j4zI63lvQyS +2tfcVlXgCuXXetpMwikA2eNtP5sVq+vsB2Sbcp3E4YxIln8htP2ktFVxgx7WcubF +XRcxDXGvgyQOegZ0l3BdmlM2jqcqauFuaDkPaaLyvY4+TCTz+add4BOwzIZKVZV6 +kyrmkdjJeQpg5t3/rrdMuTHVEn/JoPekRaONuBu5uKpE6swUlcWu6qE26y0FzXmR +I9IuYMXPLZMvCqbwRmsdETwfd2W+xwrqUeormEFlR/rJhN26+r+cX9FJFh1w3D63 +GQQI9nqNNkUCkQKCAQEAzZa1NuTaMWBJ6YHMYS0BJmCK8Nz//CoFag3h1RBYp/cF +ycscCyM1Rrd3ID/DTYUZBcBczhtVPJs84O/exXQAv92UH2THCqQnY8AqEy7SJE1i ++St+h6PaXllUAkbT0GkmiSBL5bIfb+5jmuccv4xV20OROsirSDdiNQ0WmUfmhfDs +cxRynRu6yAU4ddrWkmy19A8ngBbyknqls1fBsdAWgh3h4orCJ0HLQ/MuTMgKrsfL +Pka5z7LXDkdZTzTdkyKHp3MVVI/1pBRxvFzmdbbvKwrbcpf/Snc4IBtYgJsotWCd +SG6u6BzmJrdRIpiB0+c1gyAVNREeWD0JdMmIpK0+/QKCAQA0wBofoJ7BdX3oXhcY +Np9jfnH2eon4Sr70FpPi3r7hs48mfr/7V8aCE0T9KMw6pwVDZxMuVUJrgFOca3R0 +Vl/XwodYWFk3euZLHdl3q4NWgZiyzWncwKz4oqpoTXUeH6ZuCkC4QBjhuUeAQljO +KTbsXSsSgl/5Ysjf1EUyDYDUg4pHbtDzcLbVm8JHfXK4L1NllCMHur0laCCbzggR +U29wuAEDK0QlT3dgvg1TiSA2F6oAOlpjznzKDHBZz8T5qUUBDRAnjbZ6jygC86wZ +6VRsTknSQS+5csY9OXygU1OmmXGCGX9x6fgoawe1p9LRWjZ3zCNWy1rutfH19YCp +HxWBAoIBAQCqkHJfzJZJiL1JgWpy5Mejc01Sb8fhCWvchQ/rmNg04fhnZp8pjlhR +Bz1KABykX9xWrTVRudOJqLFlXRzRbGCCze5p7U5FQdN8Kp29tIabn6iRWMhs+D/f +LvVHvkNVESfrdGQDeTgjwP/aMAvlzyQb+X6v6nRQQcK0iNtK6CAU18ET6M7+EVdx +QwOIo7qJWK/MgBYhauhtJlv64r/MKfvCj9AsBzr1HtzozwSGpyBVyWSRklPuQU2y +hvdNg2qg+3DYN95mfdkp+9wwjlKVLuRWLXfLJteijC6AVK+kYxXvBOz4fvuVjwRS +8pvZu/VaPORkmWV+1Wj7hAgoYFuBZEpxAoIBABvpjbSKAvpBFsFlveLRTKtMgUtB +adawHhYLWygzjhpZvbttAYSJjXAdd1vHxOtGCZjuaSclysnz0xJyP0ZCCXvjeVWZ +WB74LvVCwDktr3qlHEq2fw1T6IKsdZHETQylTPvdkt8YP/NFBSUYd5fUKK0RH1iA +ayPJwLCHzuuB78N8ZXsZZysvUDNvcQH4dMxw2DmEnZAn8O8L4/EJcjODI+rEFuq+ ++1jzIlxACR1ewqwHcKs/AhQ4CrEbW1g8KoIr6D+p7Snhd3WggGNOf5+tKk/yqUZk +HM2xQmTUqRj8HNHcTFkKGCvSYD++5eCr785IAq8Hag8sOmSgP+clq7F7V5Q= +-----END RSA PRIVATE KEY----- diff --git a/src/assets/key/BMA.pub b/src/assets/key/BMA.pub new file mode 100644 index 0000000..3260601 --- /dev/null +++ b/src/assets/key/BMA.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQClVi9NES4zetZ5pvbr40/VWDu3xjv2298+EySGqFG/YTh9a6pyzIaxrNHPuZvQH2p79B2xE2HmuAqTLFQhRhUiFKBaXa4TONXNcF6++by7sCSDWr5aTtI8gR/gaLHEwh81ZDB5ukVeGd2DtZxBHPzoBBQvKQuEkUP1qJsNRBBNYuSRq6dXEvyv2IK4RaPomWEdflqjV+6BA+A6GtOq3dc9sRSrLFpMaVZMY38fZPEOM8fBJRZdHfv7ktW1H6klEqKt8kOtjXCvjRKlNUn6HX3X+V6NYu/cJdZLFj2hWdgE/c/mP7gqrRnsS3WC8HL7eY6wFOu8PxPQicF/R44sPz3pDizv2mRT3Tl7G3bgVd8OlLxk3SuLY5WguZz7LzvP9N+lOBl29sHOt2wZeeZit+t7j/8N+KC1i7J2gplGn0CobekjSy8Y1VzCWZN+EKXstxLpe7ajxamw2BC4nc+CbsAD4U6sgJ7qiu4xfKjgNo2YYrG3ySVN+qMYDzt0P5P+Z5XeFDfwohcPvnQkRtsNXNKT7JEiDrqZGDx/qg4ePmqrXYi/kJ/ufclA0HjMSy8+kFZSrezquK7Fa2slADSh895sQS5W6D95LLYs4PkeFXNyq0L2FuJ2DyAF4Cc+ojSguksFQmdE46Fu0Qut2EgsHESV7CHaXi3bG8Yb2GkC+M+nTQ== waruneeta@AUYs-MacBook-Pro.local diff --git a/src/assets/key/BMA.pub.pem b/src/assets/key/BMA.pub.pem new file mode 100644 index 0000000..36cff09 --- /dev/null +++ b/src/assets/key/BMA.pub.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApVYvTREuM3rWeab26+NP +1Vg7t8Y79tvfPhMkhqhRv2E4fWuqcsyGsazRz7mb0B9qe/QdsRNh5rgKkyxUIUYV +IhSgWl2uEzjVzXBevvm8u7Akg1q+Wk7SPIEf4GixxMIfNWQwebpFXhndg7WcQRz8 +6AQULykLhJFD9aibDUQQTWLkkaunVxL8r9iCuEWj6JlhHX5ao1fugQPgOhrTqt3X +PbEUqyxaTGlWTGN/H2TxDjPHwSUWXR37+5LVtR+pJRKirfJDrY1wr40SpTVJ+h19 +1/lejWLv3CXWSxY9oVnYBP3P5j+4Kq0Z7Et1gvBy+3mOsBTrvD8T0InBf0eOLD89 +6Q4s79pkU905ext24FXfDpS8ZN0ri2OVoLmc+y87z/TfpTgZdvbBzrdsGXnmYrfr +e4//DfigtYuydoKZRp9AqG3pI0svGNVcwlmTfhCl7LcS6Xu2o8WpsNgQuJ3Pgm7A +A+FOrICe6oruMXyo4DaNmGKxt8klTfqjGA87dD+T/meV3hQ38KIXD750JEbbDVzS +k+yRIg66mRg8f6oOHj5qq12Iv5Cf7n3JQNB4zEsvPpBWUq3s6riuxWtrJQA0ofPe +bEEuVug/eSy2LOD5HhVzcqtC9hbidg8gBeAnPqI0oLpLBUJnROOhbtELrdhILBxE +lewh2l4t2xvGG9hpAvjPp00CAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/assets/line.png b/src/assets/line.png new file mode 100644 index 0000000..e8950bf Binary files /dev/null and b/src/assets/line.png differ diff --git a/src/assets/sso.png b/src/assets/sso.png new file mode 100644 index 0000000..3b1adc0 Binary files /dev/null and b/src/assets/sso.png differ diff --git a/src/interface/index/Main.ts b/src/interface/index/Main.ts new file mode 100644 index 0000000..14def36 --- /dev/null +++ b/src/interface/index/Main.ts @@ -0,0 +1,57 @@ +interface DataOption { + id: string + name: string +} + +interface DataDateMonthObject { + month: number + year: number +} +interface FormRef { + date: object | null + reason: object | null + [key: string]: any +} +interface notiType { + id: string + sender: string + body: string + timereceive: Date + isOpen: boolean +} + +interface LocationObject { + latitude: number + longitude: number +} + +interface Pagination { + sortBy: string | null + descending: boolean + page: number + rowsPerPage: number | undefined +} +interface DataCheckIn { + checkInDate: string + checkInDateTime: string + checkInId: string + checkInLocation: string + checkInStatus: string + checkInTime: string + checkOutLocation: string + checkOutStatus: string + checkOutTime: string + editReason: string + editStatus: string + isEdit: boolean +} + +export type { + DataOption, + FormRef, + notiType, + DataDateMonthObject, + LocationObject, + Pagination, + DataCheckIn, +} diff --git a/src/interface/response/Main.ts b/src/interface/response/Main.ts new file mode 100644 index 0000000..574f78b --- /dev/null +++ b/src/interface/response/Main.ts @@ -0,0 +1,13 @@ +interface Noti { + id: string + body: string + receiverUserId: string + type: string + payload: null + isOpen: false + receiveDate: Date + openDate: null + createdFullName: string +} + +export type { Noti } diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..fb28e50 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,38 @@ +import { createApp, defineAsyncComponent } from 'vue' +import App from '@/App.vue' +import '@/registerServiceWorker' +import router from '@/router' +import { createPinia } from 'pinia' +import { Quasar, Dialog, Notify, Loading } from 'quasar' +import '@vuepic/vue-datepicker/dist/main.css' +import quasarUserOptions from '@/quasar-user-options' + +import 'quasar/src/css/index.sass' +import th from 'quasar/lang/th' + +import http from '@/plugins/http' + +const app = createApp(App) +const pinia = createPinia() + +app.use(router) +app.use(pinia) + +app.use(Quasar, { + ...quasarUserOptions, + plugins: { + Notify, + Dialog, + Loading, + }, + lang: th, +}) + +app.component( + 'datepicker', + defineAsyncComponent(() => import('@vuepic/vue-datepicker')) +) + +app.config.globalProperties.$http = http + +app.mount('#app') diff --git a/src/plugins/axios.ts b/src/plugins/axios.ts new file mode 100644 index 0000000..36f4b2f --- /dev/null +++ b/src/plugins/axios.ts @@ -0,0 +1,24 @@ +import axios from 'axios' +// import { dotnetPath } from "../path/axiosPath"; +// import { getToken } from "@baloise/vue-keycloak"; +import { getToken } from './auth' + +const axiosInstance = axios.create({ + withCredentials: false, +}) + +// axiosInstance.defaults.baseURL = dotnetPath; +axiosInstance.interceptors.request.use( + async (config) => { + const token = await getToken() + config.headers = { + Authorization: `Bearer ${token}`, + } + return config + }, + (error) => { + Promise.reject(error) + } +) + +export default axiosInstance diff --git a/src/plugins/filters.ts b/src/plugins/filters.ts new file mode 100644 index 0000000..04d9354 --- /dev/null +++ b/src/plugins/filters.ts @@ -0,0 +1,20 @@ +/** + * GLOABL Filters + * - ไฟล์นี้จะไว้เก็บฟังก์ชันง่าย ๆ พวก Helper Functions ทั้งหลาย + */ + +const filters = { + /** + * ฟังก์ชัน compactNumber ใช้แปลงตัวเลขยาว ๆ ให้กลายเป็นเลขสั้น ๆ แบบที่พวก Social Media ชอบใช้กัน เช่น 1,000 แปลงเป็น 1K หรือ 1,000,000 แปลงเป็น 1M + * วิธีใช้ : {{ $filters.compactNumber(value) }} + * + * @param val รับค่าพารามิเตอร์เป็นตัวแปรชนิดตัวเลข + * @returns คืนค่าเป็นตัวเลขที่แปลงค่าแล้ว + */ + compactNumber(val: number) { + const formatter = Intl.NumberFormat('en', { notation: 'compact' }) + return formatter.format(val) + }, +} + +export default filters diff --git a/src/plugins/http.ts b/src/plugins/http.ts new file mode 100644 index 0000000..1120346 --- /dev/null +++ b/src/plugins/http.ts @@ -0,0 +1,39 @@ +import Axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; + +const http = Axios.create({ + timeout: 1000000000, // เพิ่มค่า timeout + headers: { + "X-Requested-With": "XMLHttpRequest", + }, +}); + +http.interceptors.request.use( + async function (config: AxiosRequestConfig) { + config.headers = config.headers ?? {}; + + return config; + }, + function (error: any) { + return Promise.reject(error); + } +); + +http.interceptors.response.use( + function (response: AxiosResponse) { + return response; + }, + function (error: any) { + if (typeof error !== undefined) { + // eslint-disable-next-line no-prototype-builtins + if (error.hasOwnProperty("response")) { + if (error.response.status === 401 || error.response.status === 403) { + // Store.commit("SET_ERROR_MESSAGE", error.response.data.message); + // Store.commit("REMOVE_ACCESS_TOKEN") + } + } + } + return Promise.reject(error); + } +); + +export default http; diff --git a/src/plugins/keycloak.ts b/src/plugins/keycloak.ts new file mode 100644 index 0000000..d882c3e --- /dev/null +++ b/src/plugins/keycloak.ts @@ -0,0 +1,70 @@ +// authen with keycloak client +import Keycloak from "keycloak-js"; + +// const ACCESS_TOKEN = 'BMAHRIS_KEYCLOAK_IDENTITY' +// const REFRESH_TOKEN = 'BMAHRIS_KEYCLOAK_REFRESH' +// const keycloakConfig = { +// url: import.meta.env.VITE_URL_KEYCLOAK, +// realm: import.meta.env.VITE_REALM_KEYCLOAK, +// clientId: import.meta.env.VITE_CLIENTID_KEYCLOAK, +// clientSecret: import.meta.env.VITE_CLIENTSECRET_KEYCLOAK, +// } + +// const keycloak = new Keycloak(keycloakConfig) + +// async function kcAuthen(access_token: string, refresh_token: string) { +// await setCookie(ACCESS_TOKEN, access_token, 1) +// await setCookie(REFRESH_TOKEN, refresh_token, 1) +// window.location.href = '/' +// } + +// async function kcLogout() { +// await deleteCookie(ACCESS_TOKEN) +// await deleteCookie(REFRESH_TOKEN) +// if (keycloak.authenticated !== undefined) { +// keycloak.logout() +// } +// window.location.href = '/login' +// } + +// async function getToken() { +// return { +// token: getCookie(ACCESS_TOKEN), +// refresh_token: getCookie(REFRESH_TOKEN), +// } +// } + +// function setCookie(name: string, value: any, days: number) { +// let expires = '' +// if (days) { +// const date = new Date() +// date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000) +// expires = '; expires=' + date.toUTCString() +// } +// document.cookie = name + '=' + (value || '') + expires + '; path=/' +// } + +// function getCookie(name: string) { +// const nameEQ = name + '=' +// const ca = document.cookie.split(';') +// for (let i = 0; i < ca.length; i++) { +// let c = ca[i] +// while (c.charAt(0) == ' ') c = c.substring(1, c.length) +// if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length) +// } +// return null +// } + +// function deleteCookie(name: string) { +// document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` +// } + +// export default keycloak +// export { +// keycloakConfig, +// getToken, +// kcAuthen, +// kcLogout, +// ACCESS_TOKEN, +// REFRESH_TOKEN, +// } diff --git a/src/quasar-user-options.ts b/src/quasar-user-options.ts new file mode 100644 index 0000000..56dff8b --- /dev/null +++ b/src/quasar-user-options.ts @@ -0,0 +1,11 @@ +// import "./styles/quasar.scss" +import '@quasar/extras/material-icons/material-icons.css' +import '@quasar/extras/material-icons-outlined/material-icons-outlined.css' +import '@quasar/extras/fontawesome-v5/fontawesome-v5.css' +import '@quasar/extras/mdi-v4/mdi-v4.css' + +// To be used on app.use(Quasar, { ... }) +export default { + config: {}, + plugins: {}, +} diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts new file mode 100644 index 0000000..adbd55d --- /dev/null +++ b/src/registerServiceWorker.ts @@ -0,0 +1,34 @@ +/* eslint-disable no-console */ + +import { register } from 'register-service-worker' + +if (process.env.NODE_ENV === 'production') { + register('registerSW.js', { + ready() { + console.log( + 'App is being served from cache by a service worker.\n' + + 'For more details, visit https://goo.gl/AFskqB' + ) + }, + registered() { + console.log('Service worker has been registered.') + }, + cached() { + console.log('Content has been cached for offline use.') + }, + updatefound() { + console.log('New content is downloading.') + }, + updated() { + console.log('New content is available; please refresh.') + }, + offline() { + console.log( + 'No internet connection found. App is running in offline mode.' + ) + }, + error(error) { + console.error('Error during service worker registration:', error) + }, + }) +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..ea578e6 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,29 @@ +import { createRouter, createWebHistory } from "vue-router"; +const loginView = () => import("@/views/login.vue"); + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + name: "loginMain", + component: loginView, + children: [ + /** + * 404 Not Found + * ref: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route + */ + { + path: "/:pathMatch(.*)*", + name: "NotFound", + component: () => import("@/views/ErrorNotFoundPage.vue"), + meta: { + Auth: true, + }, + }, + ], + }, + ], +}); + +export default router; diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts new file mode 100644 index 0000000..01b57df --- /dev/null +++ b/src/shims-vue.d.ts @@ -0,0 +1 @@ +declare module '*.vue' diff --git a/src/stores/main.ts b/src/stores/main.ts new file mode 100644 index 0000000..f4b0ae6 --- /dev/null +++ b/src/stores/main.ts @@ -0,0 +1,13 @@ +import { defineStore } from 'pinia' +import { useCounterMixin } from '@/stores/mixin' + +const mixin = useCounterMixin() + +/** store for checkin history*/ +export const useSsoHrms = defineStore('hrmssso', () => { + + + return { + + } +}) diff --git a/src/stores/mixin.ts b/src/stores/mixin.ts new file mode 100644 index 0000000..573e404 --- /dev/null +++ b/src/stores/mixin.ts @@ -0,0 +1,91 @@ +import { defineStore } from "pinia"; +import { Loading, QSpinnerCube } from "quasar"; + +export const useCounterMixin = defineStore("mixin", () => { + function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) { + if (srcDate == null) { + return null; + ` + `; + } + const date = new Date(srcDate); + const isValidDate = Boolean(+date); + if (!isValidDate) return srcDate.toString(); + if (isValidDate && date.getFullYear() < 1000) return srcDate.toString(); + const fullMonthThai = [ + "มกราคม", + "กุมภาพันธ์", + "มีนาคม", + "เมษายน", + "พฤษภาคม", + "มิถุนายน", + "กรกฎาคม", + "สิงหาคม", + "กันยายน", + "ตุลาคม", + "พฤศจิกายน", + "ธันวาคม", + ]; + const abbrMonthThai = [ + "ม.ค.", + "ก.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "ก.ค.", + "ส.ค.", + "ก.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค.", + ]; + let dstYear = 0; + if (date.getFullYear() > 2500) { + dstYear = date.getFullYear(); + } else { + dstYear = date.getFullYear() + 543; + } + let dstMonth = ""; + if (isFullMonth) { + dstMonth = fullMonthThai[date.getMonth()]; + } else { + dstMonth = abbrMonthThai[date.getMonth()]; + } + let dstTime = ""; + if (isTime) { + const H = date.getHours().toString().padStart(2, "0"); + const M = date.getMinutes().toString().padStart(2, "0"); + // const S = date.getSeconds().toString().padStart(2, "0") + // dstTime = " " + H + ":" + M + ":" + S + " น." + dstTime = " " + H + ":" + M + " น."; + } + return ( + date.getDate().toString().padStart(2, "0") + + " " + + dstMonth + + " " + + dstYear + + dstTime + ); + } + + const showLoader = () => { + Loading.show({ + spinner: QSpinnerCube, + spinnerSize: 140, + spinnerColor: "primary", + backgroundColor: "white", + }); + }; + + const hideLoader = () => { + Loading.hide(); + }; + + return { + date2Thai, + showLoader, + hideLoader, + }; +}); diff --git a/src/style/quasar-variables.sass b/src/style/quasar-variables.sass new file mode 100644 index 0000000..f2c3dc4 --- /dev/null +++ b/src/style/quasar-variables.sass @@ -0,0 +1,139 @@ +// FILE (create it): src/quasar-variables.sass + +$primary: #02A998 +$secondary: #016987 +$accent: #9C27B0 + +// $dark: #1D1D1D +$dark: #35473C + +$positive: #21BA45 +$negative: #C10015 +$info: #31CCEC +$warning: #F2C037 + +$add: #00aa86 +.text-add + color: $add !important +.bg-add + background: $add !important + +$edit: #019fc4 +.text-edit + color: $edit !important +.bg-edit + background: $edit !important + +$public: #016987 +.text-public + color: $public !important +.bg-public + background: $public !important + +$save: #4154b3 +.text-save + color: $save !important +.bg-save + background: $save !important + +$nativetab: #c8d3db +.text-nativetab + color: $nativetab !important +.bg-nativetab + background: $nativetab !important + +$activetab: #4a5568 +.text-activetab + color: $activetab !important +.bg-activetab + background: $activetab !important + +.inputgreen .q-field__prefix, +.inputgreen .q-field__suffix, +.inputgreen .q-field__input, +.inputgreen .q-field__native + + color: rgb(6, 136, 77) + +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100;200;300;400;500;600;700;800;900&display=swap') + +$noto-thai: 'Noto Sans Thai', sans-serif + +#azay-app, +div + font-family: $noto-thai !important + text-rendering: optimizeLegibility + -webkit-font-smoothing: antialiased + -moz-osx-font-smoothing: grayscale + +$separator-color: #EDEDED !default + +.bg-teal-1 + background: #e0f2f1a6 !important + +.table_ellipsis + max-width: 200px + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + +.table_ellipsis:hover + word-wrap: break-word + overflow: visible + white-space: normal + +.table_ellipsis2 + max-width: 25vw + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + +.table_ellipsis2:hover + word-wrap: break-word + overflow: visible + white-space: normal + transition: width 2s + +$muti-tab: #87d4cc +.text-muti-tab + color: $muti-tab !important +.bg-muti-tab + background: $muti-tab !important + + +/* editor */ + +.q-editor + font-size: 1rem + line-height: 1.5rem + font-weight: 400 + +.q-editor h1, .q-menu h1 + font-size: 1.5rem + line-height: 2rem + font-weight: 400 + margin-block-start: 0em + margin-block-end: 0em + +.q-editor h2, .q-menu h2 + font-size: 1.25rem + line-height: 1.5rem + font-weight: 400 + margin-block-start: 0em + margin-block-end: 0em + + +.q-editor h3, .q-menu h3 + font-size: 1.1rem + line-height: 1.5rem + font-weight: 400 + margin-block-start: 0em + margin-block-end: 0em + +.q-editor p, .q-menu p + margin: 0 + +/* q-tree */ + +.q-tree + color: #c8d3db diff --git a/src/views/ErrorNotFoundPage.vue b/src/views/ErrorNotFoundPage.vue new file mode 100644 index 0000000..4768349 --- /dev/null +++ b/src/views/ErrorNotFoundPage.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/views/login.vue b/src/views/login.vue new file mode 100644 index 0000000..1912c6c --- /dev/null +++ b/src/views/login.vue @@ -0,0 +1,210 @@ + + + + + + diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..bde017e --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.web.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "esModuleInterop": true, + "ignoreDeprecations": "5.0", + "allowSyntheticDefaultImports": true, + "lib": ["dom", "es2015", "es2018", "es2018.promise"] + } +} diff --git a/tsconfig.config.json b/tsconfig.config.json new file mode 100644 index 0000000..48ba8af --- /dev/null +++ b/tsconfig.config.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.node.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "types": ["node"], + "ignoreDeprecations": "5.0", + "verbatimModuleSyntax": true, + "module": "esnext" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..31f9003 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.config.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] +} diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json new file mode 100644 index 0000000..d080d61 --- /dev/null +++ b/tsconfig.vitest.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.app.json", + "exclude": [], + "compilerOptions": { + "composite": true, + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..292aa7d --- /dev/null +++ b/vite.config.js @@ -0,0 +1,66 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import { quasar, transformAssetUrls } from '@quasar/vite-plugin' +import { VitePWA } from 'vite-plugin-pwa' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue({ + template: { transformAssetUrls }, + }), + quasar({ + sassVariables: 'src/style/quasar-variables.sass', + }), + vueJsx(), + VitePWA({ + registerType: 'autoUpdate', + injectRegister: 'auto', + workbox: { + cleanupOutdatedCaches: true, + globPatterns: ['**/*.*'], + }, + includeAssets: ['icons/safari-pinned-tab.svg'], + manifest: { + name: 'BMA-Checkin', + short_name: 'EHR Checkin', + theme_color: '#ffffff', + icons: [ + { + src: 'icons/android-chrome-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'icons/android-chrome-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + { + src: 'icons/android-chrome-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: ['any', 'maskable'], + }, + ], + }, + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + build: { + target: 'esnext', + }, + server: { + port: 3002, + }, + optimizeDeps: { + include: ['esri-loader'], + }, +})