commit 288971d1d7b501ae9a318f7ee1d588db61b6ee9e Author: waruneeta Date: Wed Dec 20 10:04:43 2023 +0700 first commit diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a0b479f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { + node: true, + es2022: true, + }, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/typescript/recommended', + ], + rules: { + // 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + // 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + 'vue/no-mutating-props': 'off', + // '@typescript-eslint/no-explicit-any': 'off', + 'vue/multi-word-component-names': 'off' + }, +} diff --git a/.github/workflows/build-local.yaml b/.github/workflows/build-local.yaml new file mode 100644 index 0000000..973c40f --- /dev/null +++ b/.github/workflows/build-local.yaml @@ -0,0 +1,39 @@ +# use for local build with act +name: build-local +run-name: build-local ${{ github.actor }} +on: + workflow_dispatch: +env: + REGISTRY: docker.frappet.com + IMAGE_NAME: demo/bma-ehr-app +jobs: + # act workflow_dispatch -W .github/workflows/build-local.yaml --input IMAGE_VER=test-v1 + build-local: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # skip Set up QEMU because it fail on act and container + - name: Gen Version + id: gen_ver + run: | + if [[ $GITHUB_REF == 'refs/tags/'* ]]; then + IMAGE_VER='${GITHUB_REF/refs\/tags\//}' + else + IMAGE_VER=${{ github.event.inputs.IMAGE_VER }} + fi + if [[ $IMAGE_VER == '' ]]; then + IMAGE_VER='test-vBeta' + fi + echo '::set-output name=image_ver::'$IMAGE_VER + - name: Test Version + run: | + echo $GITHUB_REF + echo ${{ steps.gen_ver.outputs.image_ver }} + + - name: Build and load local docker image + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64 + load: true + tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..36aa8aa --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,86 @@ +name: release-test +run-name: release-test ${{ github.actor }} +on: + # push: + # tags: + # - 'v[0-9]+.[0-9]+.[0-9]+' + # tags-ignore: + # - '2.*' + # Allow run workflow manually from Action tab + workflow_dispatch: +env: + REGISTRY: docker.frappet.com + IMAGE_NAME: ehr/bma-ehr-checkin + DEPLOY_HOST: frappet.com + COMPOSE_PATH: /home/frappet/docker/bma-ehr-checkin + TOKEN_LINE: uxuK5hDzS2DsoC5piJBrWRLiz8GgY7iMZZldOWsDDF0 +jobs: + # act workflow_dispatch -W .github/workflows/release.yaml --input IMAGE_VER=test-v1 -s DOCKER_USER=sorawit -s DOCKER_PASS=P@ssword -s SSH_PASSWORD=P@ssw0rd + release-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # skip Set up QEMU because it fail on act and container + # Gen Version try to get version from tag or inut + - name: Gen Version + id: gen_ver + run: | + if [[ $GITHUB_REF == 'refs/tags/'* ]]; then + IMAGE_VER='${GITHUB_REF/refs\/tags\//}' + else + IMAGE_VER=${{ github.event.inputs.IMAGE_VER }} + fi + if [[ $IMAGE_VER == '' ]]; then + IMAGE_VER='test-vBeta' + fi + echo '::set-output name=image_ver::'$IMAGE_VER + - name: Check Version + run: | + echo $GITHUB_REF + echo ${{ steps.gen_ver.outputs.image_ver }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login in to registry + uses: docker/login-action@v2 + with: + registry: ${{env.REGISTRY}} + username: ${{secrets.DOCKER_USER}} + password: ${{secrets.DOCKER_PASS}} + - name: Build and push docker image + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest + - name: Remote Deployment + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{env.DEPLOY_HOST}} + username: frappet + password: ${{ secrets.SSH_PASSWORD }} + port: 22 + script: | + cd "${{env.COMPOSE_PATH}}" + docker-compose pull + docker-compose up -d + echo "${{ steps.gen_ver.outputs.image_ver }}"> success + - uses: snow-actions/line-notify@v1.1.0 + if: success() + with: + access_token: ${{ env.TOKEN_LINE }} + message: | + -Success✅✅✅ + Image: ${{env.IMAGE_NAME}} + Version: ${{ github.event.inputs.IMAGE_VER }} + By: ${{secrets.DOCKER_USER}} + - uses: snow-actions/line-notify@v1.1.0 + if: failure() + with: + access_token: ${{ env.TOKEN_LINE }} + message: | + -Failure❌❌❌ + Image: ${{env.IMAGE_NAME}} + Version: ${{ github.event.inputs.IMAGE_VER }} + By: ${{secrets.DOCKER_USER}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af75793 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +node_modules +/dist + + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f8dec5b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f2553e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# docker build . -t docker.frappet.com/demo/fe:latest +FROM node:lts as build-stage +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY ./ . + +RUN npm run build + +FROM nginx as production-stage + +RUN mkdir /app +COPY --from=build-stage /app/dist /app +COPY nginx.conf /etc/nginx/nginx.conf + +COPY 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;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..579ee97 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# BMA ระบบลงเวลาเข้า-ออกงาน + +## 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/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..7f8e2db --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,20 @@ +#!/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_REALM_KEYCLOAK|'${VITE_REALM_KEYCLOAK}'|g' $file + sed -i 's|VITE_CLIENTID_KEYCLOAK|'${VITE_CLIENTID_KEYCLOAK}'|g' $file + sed -i 's|VITE_URL_KEYCLOAK|'${VITE_URL_KEYCLOAK}'|g' $file + sed -i 's|VITE_API_URI_CONFIG|'${VITE_API_URI_CONFIG}'|g' $file + +done + +echo "Starting Nginx" +nginx -g 'daemon off;' + 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..52d9075 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + + + BMA ระบบลงเวลาเข้า-ออกงาน + + + +
+ + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6f61d6c --- /dev/null +++ b/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/package.json b/package.json new file mode 100644 index 0000000..36d9cc2 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "bma-ehr-publish", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "run-p build-only", + "preview": "vite preview --port 3008", + "test:unit": "vitest --environment jsdom --root src/", + "test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'", + "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier . --write" + }, + "dependencies": { + "@quasar/extras": "^1.15.8", + "@vuepic/vue-datepicker": "^5.2.1", + "moment": "^2.29.4", + "pinia": "^2.1.4", + "quasar": "^2.11.1", + "register-service-worker": "^1.7.2", + "vite-plugin-pwa": "^0.16.7", + "vue": "^3.2.45", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@quasar/vite-plugin": "^1.3.0", + "@rushstack/eslint-patch": "^1.1.4", + "@types/jsdom": "^20.0.1", + "@types/node": "^18.18.10", + "@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/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png new file mode 100644 index 0000000..49fef58 Binary files /dev/null and b/public/icons/android-chrome-192x192.png differ diff --git a/public/icons/android-chrome-512x512.png b/public/icons/android-chrome-512x512.png new file mode 100644 index 0000000..9ec80df Binary files /dev/null and b/public/icons/android-chrome-512x512.png differ diff --git a/public/icons/android-chrome-maskable-192x192.png b/public/icons/android-chrome-maskable-192x192.png new file mode 100644 index 0000000..49fef58 Binary files /dev/null and b/public/icons/android-chrome-maskable-192x192.png differ diff --git a/public/icons/android-chrome-maskable-512x512.png b/public/icons/android-chrome-maskable-512x512.png new file mode 100644 index 0000000..9ec80df Binary files /dev/null and b/public/icons/android-chrome-maskable-512x512.png differ diff --git a/public/icons/android-launchericon-144-144.png b/public/icons/android-launchericon-144-144.png new file mode 100644 index 0000000..e64e3d7 Binary files /dev/null and b/public/icons/android-launchericon-144-144.png differ diff --git a/public/icons/android-launchericon-48-48.png b/public/icons/android-launchericon-48-48.png new file mode 100644 index 0000000..93d8166 Binary files /dev/null and b/public/icons/android-launchericon-48-48.png differ diff --git a/public/icons/android-launchericon-72-72.png b/public/icons/android-launchericon-72-72.png new file mode 100644 index 0000000..108e5ba Binary files /dev/null and b/public/icons/android-launchericon-72-72.png differ diff --git a/public/icons/android-launchericon-96-96.png b/public/icons/android-launchericon-96-96.png new file mode 100644 index 0000000..fa14036 Binary files /dev/null and b/public/icons/android-launchericon-96-96.png differ diff --git a/public/icons/apple-touch-icon-120x120.png b/public/icons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..332f8a1 Binary files /dev/null and b/public/icons/apple-touch-icon-120x120.png differ diff --git a/public/icons/apple-touch-icon-152x152.png b/public/icons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..eefa563 Binary files /dev/null and b/public/icons/apple-touch-icon-152x152.png differ diff --git a/public/icons/apple-touch-icon-180x180.png b/public/icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..407a6e0 Binary files /dev/null and b/public/icons/apple-touch-icon-180x180.png differ diff --git a/public/icons/apple-touch-icon-60x60.png b/public/icons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..b9f4deb Binary files /dev/null and b/public/icons/apple-touch-icon-60x60.png differ diff --git a/public/icons/apple-touch-icon-76x76.png b/public/icons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..cbb0b26 Binary files /dev/null and b/public/icons/apple-touch-icon-76x76.png differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..85ec878 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png new file mode 100644 index 0000000..a36e824 Binary files /dev/null and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png new file mode 100644 index 0000000..4d31d3e Binary files /dev/null and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/msapplication-icon-144x144.png b/public/icons/msapplication-icon-144x144.png new file mode 100644 index 0000000..e64e3d7 Binary files /dev/null and b/public/icons/msapplication-icon-144x144.png differ diff --git a/public/icons/mstile-150x150.png b/public/icons/mstile-150x150.png new file mode 100644 index 0000000..5911a17 Binary files /dev/null and b/public/icons/mstile-150x150.png differ diff --git a/public/icons/safari-pinned-tab.svg b/public/icons/safari-pinned-tab.svg new file mode 100644 index 0000000..8f5e6d4 --- /dev/null +++ b/public/icons/safari-pinned-tab.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..1643ebf --- /dev/null +++ b/src/App.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/api/api.message.ts b/src/api/api.message.ts new file mode 100644 index 0000000..21c6813 --- /dev/null +++ b/src/api/api.message.ts @@ -0,0 +1,7 @@ +import env from './index' +const message = `${env.API_URI}/message` + +export default { + msgNotificate: `${message}/my-notifications`, + msgId: (id: string) => `${message}/my-notifications/${id}`, +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..1a28e27 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,30 @@ +/**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: "https://localhost:5010", + 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..fa91f82 --- /dev/null +++ b/src/app.config.ts @@ -0,0 +1,17 @@ +/**ใช้รวมไฟล์ย่อยๆ ของ api แต่ละไฟล์ */ + +/** API ระบบลงเวลา */ +import message from '@/api/api.message' + +// environment variables +export const s3ClusterUrl = import.meta.env.VITE_S3CLUSTER_PUBLIC_URL + +const API = { + /**message */ + ...message, +} + +export default { + API: API, + s3ClusterUrl, +} diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..65f5a68 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/map1.png b/src/assets/map1.png new file mode 100644 index 0000000..34d643d Binary files /dev/null and b/src/assets/map1.png differ diff --git a/src/components/CustomDialog.vue b/src/components/CustomDialog.vue new file mode 100644 index 0000000..675761c --- /dev/null +++ b/src/components/CustomDialog.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/DialogHeader.vue b/src/components/DialogHeader.vue new file mode 100644 index 0000000..14758aa --- /dev/null +++ b/src/components/DialogHeader.vue @@ -0,0 +1,27 @@ + + diff --git a/src/components/Table.vue b/src/components/Table.vue new file mode 100644 index 0000000..e47031e --- /dev/null +++ b/src/components/Table.vue @@ -0,0 +1,186 @@ + + + diff --git a/src/interface/index/Main.ts b/src/interface/index/Main.ts new file mode 100644 index 0000000..0147e23 --- /dev/null +++ b/src/interface/index/Main.ts @@ -0,0 +1,6 @@ +interface DataOption { + id: string + name: string +} + +export type { DataOption} diff --git a/src/interface/response/Main.ts b/src/interface/response/Main.ts new file mode 100644 index 0000000..b3a1426 --- /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 | null + openDate: null + createdFullName: string +} + +export type { Noti } diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..633ed26 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,35 @@ +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' + +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.mount('#app') diff --git a/src/plugins/filters.ts b/src/plugins/filters.ts new file mode 100644 index 0000000..d5f1d75 --- /dev/null +++ b/src/plugins/filters.ts @@ -0,0 +1,22 @@ +/** + * 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; \ No newline at end of file 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..6608f2e --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,30 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '@/views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView, + meta: { + Auth: true, + }, + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/ErrorNotFoundPage.vue'), + meta: { + Auth: true, + }, + }, + ], +}) + +router.beforeEach((to, from, next) => { + next(); +}) + +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/mixin.ts b/src/stores/mixin.ts new file mode 100644 index 0000000..77ef612 --- /dev/null +++ b/src/stores/mixin.ts @@ -0,0 +1,327 @@ +import { defineStore } from 'pinia' +import CustomComponent from '@/components/CustomDialog.vue' +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() + } + + function covertDateObject(date: string) { + if (date) { + const dateParts = date.split('/') + // ประกาศตัวแปรเพื่อเก็บค่าวันที่, เดือน, และ ปี + const day = parseInt(dateParts[0], 10) + const month = parseInt(dateParts[1], 10) - 1 + const year = parseInt(dateParts[2], 10) + 2500 + // สร้างอ็อบเจ็กต์ Date ด้วยค่าที่ได้ + const dateObject = new Date(year, month, day) + return date2Thai(dateObject) + } + } + + type OkCallback = () => void + type CancelCallback = () => void + function dialogConfirm( + q: any, + ok?: OkCallback, + title?: string, // ถ้ามี cancel action ใส่เป็น null + desc?: string, // ถ้ามี cancel action ใส่เป็น null + cancel?: CancelCallback + ) { + q.dialog({ + component: CustomComponent, + componentProps: { + title: title && title != null ? title : 'ยืนยันการบันทึก', + message: + desc && desc != null + ? desc + : 'ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?', + icon: 'info', + color: 'public', + textOk: 'ตกลง', + onlycancel: false, + }, + }) + .onOk(() => { + if (ok) ok() + }) + .onCancel(() => { + if (cancel) cancel() + }) + } + + const messageError = (q: any, e: any = '') => { + // q.dialog.hide(); + if (e.response !== undefined) { + if (e.response.data.status !== undefined) { + if (e.response.data.status == 401) { + //invalid_token + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } else { + const message = e.response.data.result ?? e.response.data.message + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `${message}`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } + } else { + if (e.response.status == 401) { + //invalid_token + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } else if (e.response.data.successful === false) { + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: e.response.data.message, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } else { + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } + } + } else { + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`, + icon: 'warning', + color: 'red', + onlycancel: true, + }, + }) + } + } + + const success = (q: any, val: string) => { + // useQuasar ไม่สามารถใช้นอกไฟล์ .vue + if (val !== '') { + return q.notify({ + message: val, + color: 'primary', + icon: 'mdi-information', + position: 'bottom-right', + multiLine: true, + timeout: 1000, + badgeColor: 'positive', + classes: 'my-notif-class', + }) + } + } + + function notify(q: any, val: string) { + if (val !== '') { + q.notify({ + color: 'teal-10', + message: val, + icon: 'mdi-information', + position: 'bottom-right', + multiLine: true, + timeout: 7000, + actions: [{ label: 'ปิด', color: 'white', handler: () => {} }], + }) + } + } + + function dialogRemove( + q: any, + ok?: () => void, + title?: string, // ถ้ามี cancel action ใส่เป็น null + desc?: string, // ถ้ามี cancel action ใส่เป็น null + cancel?: () => void + ) { + q.dialog({ + component: CustomComponent, + componentProps: { + title: title && title != null ? title : 'ยืนยันการลบข้อมูล', + message: + desc && desc != null + ? desc + : 'ต้องการยืนยันการลบข้อมูลนี้ใช่หรือไม่?', + icon: 'delete', + color: 'red', + textOk: 'ตกลง', + onlycancel: false, + }, + }) + .onOk(() => { + if (ok) ok() + }) + .onCancel(() => { + if (cancel) cancel() + }) + } + + function monthYear2Thai(month: number, year: number, isFullMonth = false) { + const date = new Date(`${year}-${month + 1}-1`) + 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()] + } + return dstMonth + ' ' + dstYear + } + + return { + date2Thai, + monthYear2Thai, + showLoader, + hideLoader, + covertDateObject, + dialogConfirm, + messageError, + success, + notify, + dialogRemove, + } +}) 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..1be87ac --- /dev/null +++ b/src/views/ErrorNotFoundPage.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000..6c19dcb --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,11 @@ + + + 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..6b5a208 --- /dev/null +++ b/tsconfig.config.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.node.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "ignoreDeprecations": "5.0", + "types": ["node"] + } +} 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..760872f --- /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: 3011, + }, + optimizeDeps: { + include: ['esri-loader'], + }, +})