first commit
19
.eslintrc.js
Normal file
|
|
@ -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'
|
||||
},
|
||||
}
|
||||
39
.github/workflows/build-local.yaml
vendored
Normal file
|
|
@ -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
|
||||
86
.github/workflows/release.yaml
vendored
Normal file
|
|
@ -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}}
|
||||
25
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
5
.prettierrc.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
21
Dockerfile
Normal file
|
|
@ -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;"]
|
||||
30
README.md
Normal file
|
|
@ -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/).
|
||||
8
cypress.config.ts
Normal file
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
20
entrypoint.sh
Normal file
|
|
@ -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;'
|
||||
|
||||
1
env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
29
index.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BMA ระบบลงเวลาเข้า-ออกงาน</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#viewDiv {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script src="https://js.arcgis.com/4.19/"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://js.arcgis.com/4.19/esri/themes/light/main.css"
|
||||
/>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58&libraries=places"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
nginx.conf
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/icons/android-chrome-maskable-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/icons/android-chrome-maskable-512x512.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/icons/android-launchericon-144-144.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icons/android-launchericon-48-48.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icons/android-launchericon-72-72.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icons/android-launchericon-96-96.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/icons/apple-touch-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
public/icons/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/icons/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/icons/apple-touch-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/icons/apple-touch-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
public/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/msapplication-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
27
public/icons/safari-pinned-tab.svg
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_175_1580)">
|
||||
<rect width="256" height="256" rx="64" fill="url(#paint0_linear_175_1580)"/>
|
||||
<path d="M90.5827 178.585C76.9239 169.784 66.7599 156.498 61.84 141.012C56.9201 125.527 57.552 108.81 63.6269 93.7405L77.2723 99.2411C72.4768 111.137 71.978 124.333 75.8618 136.558C79.7455 148.782 87.769 159.27 98.5512 166.217L90.5827 178.585Z" fill="white"/>
|
||||
<path d="M72.5857 77.1982C81.2389 65.9773 93.0997 57.6531 106.594 53.3302L111.082 67.3413C100.43 70.7537 91.0671 77.3249 84.2362 86.1827L72.5857 77.1982Z" fill="white"/>
|
||||
<path d="M125.368 50.0667C138.482 49.4932 151.493 52.6262 162.908 59.1068C174.324 65.5874 183.683 75.1533 189.912 86.708C196.142 98.2628 198.989 111.339 198.129 124.438C197.269 137.536 192.735 150.128 185.048 160.769L173.124 152.154C179.192 143.754 182.771 133.814 183.45 123.474C184.13 113.133 181.881 102.81 176.964 93.6887C172.046 84.5671 164.658 77.0155 155.646 71.8995C146.634 66.7836 136.363 64.3102 126.01 64.763L125.368 50.0667Z" fill="white"/>
|
||||
<path d="M158.578 176.695C171.315 177.787 182.226 180.102 189.347 183.223C196.468 186.344 199.332 190.066 197.423 193.719C195.515 197.372 188.959 200.716 178.935 203.151C168.911 205.585 156.078 206.95 142.743 206.999C129.409 207.048 116.449 205.778 106.196 203.418C95.9419 201.058 89.0674 197.763 86.8089 194.125C84.5505 190.487 87.0563 186.746 93.8755 183.574C100.695 180.402 111.38 178.008 124.008 176.822L130.801 182.364C122.941 183.102 116.291 184.592 112.046 186.567C107.802 188.541 106.242 190.87 107.648 193.134C109.053 195.398 113.332 197.449 119.714 198.918C126.097 200.387 134.163 201.177 142.463 201.147C150.762 201.116 158.75 200.267 164.989 198.752C171.228 197.236 175.309 195.155 176.497 192.881C177.685 190.607 175.903 188.291 171.47 186.348C167.038 184.406 160.247 182.965 152.318 182.285L158.578 176.695Z" fill="white"/>
|
||||
<path d="M129.445 119.061L143.1 117.268L158.957 122.199L160.279 141.923L141.779 182.268L123.279 135.647L129.445 119.061Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.771 141.484C163.95 138.254 164.311 134.797 163.822 131.404C163.333 128.004 162.006 124.77 159.955 121.973C157.903 119.177 155.187 116.901 152.036 115.34C148.886 113.778 145.393 112.975 141.855 113.001C138.317 113.026 134.837 113.878 131.71 115.485C128.583 117.091 125.902 119.405 123.893 122.231C121.884 125.056 120.607 128.309 120.169 131.715C119.731 135.121 120.147 138.579 121.379 141.796L121.397 141.79L139.233 182.527C139.382 183.464 139.941 184.333 140.8 184.806C142.182 185.568 143.815 185.014 144.448 183.568L162.561 142.196C162.662 141.966 162.731 141.726 162.771 141.484ZM157.71 139.138C158.44 136.883 158.646 134.496 158.308 132.15C157.942 129.611 156.951 127.194 155.418 125.105C153.886 123.016 151.856 121.316 149.502 120.149C147.149 118.982 144.539 118.382 141.896 118.401C139.252 118.42 136.653 119.057 134.317 120.257C131.981 121.457 129.977 123.186 128.476 125.297C126.975 127.408 126.021 129.838 125.694 132.383C125.43 134.434 125.581 136.511 126.132 138.499C126.223 138.635 126.304 138.782 126.374 138.94L142.074 174.801L157.557 139.438C157.603 139.334 157.654 139.234 157.71 139.138Z" fill="white"/>
|
||||
<path d="M154.279 134.268C154.279 140.895 148.906 146.268 142.279 146.268C135.651 146.268 130.279 140.895 130.279 134.268C130.279 127.64 135.651 122.268 142.279 122.268C148.906 122.268 154.279 127.64 154.279 134.268ZM135.881 134.268C135.881 137.801 138.746 140.665 142.279 140.665C145.812 140.665 148.676 137.801 148.676 134.268C148.676 130.734 145.812 127.87 142.279 127.87C138.746 127.87 135.881 130.734 135.881 134.268Z" fill="#22C2B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M125 113.782C127.924 111.387 131.308 109.533 135 108.373L135 73H125V113.782Z" fill="white"/>
|
||||
<mask id="path-10-inside-1_175_1580" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.81 129.049L95.9287 148.93L103 156.001L115.856 143.145C115.296 140.856 115 138.463 115 136.002C115 133.608 115.28 131.28 115.81 129.049Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.81 129.049L95.9287 148.93L103 156.001L115.856 143.145C115.296 140.856 115 138.463 115 136.002C115 133.608 115.28 131.28 115.81 129.049Z" fill="white"/>
|
||||
<path d="M95.9287 148.93L92.3932 145.394L88.8576 148.93L92.3932 152.465L95.9287 148.93ZM115.81 129.049L120.675 130.203L124.748 113.04L112.274 125.513L115.81 129.049ZM103 156.001L99.4642 159.537L103 163.072L106.535 159.537L103 156.001ZM115.856 143.145L119.391 146.681L121.38 144.691L120.713 141.959L115.856 143.145ZM99.4642 152.465L119.345 132.584L112.274 125.513L92.3932 145.394L99.4642 152.465ZM106.535 152.465L99.4642 145.394L92.3932 152.465L99.4642 159.537L106.535 152.465ZM112.32 139.61L99.4642 152.465L106.535 159.537L119.391 146.681L112.32 139.61ZM120.713 141.959C120.248 140.055 120 138.061 120 136.002H110C110 138.866 110.345 141.657 110.998 144.332L120.713 141.959ZM120 136.002C120 134 120.234 132.059 120.675 130.203L110.945 127.894C110.326 130.501 110 133.217 110 136.002H120Z" fill="white" mask="url(#path-10-inside-1_175_1580)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_175_1580" x1="134.047" y1="3.33166e-08" x2="133.047" y2="256" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4CE2D3"/>
|
||||
<stop offset="1" stop-color="#02A998"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_175_1580">
|
||||
<rect width="256" height="256" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
2
public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
||||
30
src/App.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<!-- <nav>
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link>
|
||||
</nav> -->
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
7
src/api/api.message.ts
Normal file
|
|
@ -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}`,
|
||||
}
|
||||
30
src/api/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**config api */
|
||||
import { ref } from 'vue'
|
||||
|
||||
const env = ref<string>(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<any>({
|
||||
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<string>(config.value[env.value].API_URI)
|
||||
|
||||
export default {
|
||||
env: env.value,
|
||||
config: config.value,
|
||||
API_URI: API_URI.value,
|
||||
}
|
||||
17
src/app.config.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/assets/map1.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
79
src/components/CustomDialog.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide" persistent>
|
||||
<q-card class="q-pa-sm">
|
||||
<q-card-section class="row">
|
||||
<div class="q-pr-md">
|
||||
<q-avatar :icon="icon" size="lg" font-size="25px" color="blue-1" :text-color="color" />
|
||||
</div>
|
||||
<div class="col text-dark">
|
||||
<span class="text-bold">{{ title }}</span>
|
||||
<br />
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="bg-white text-teal" v-if="onlycancel">
|
||||
<q-btn label="ตกลง" flat color="grey-8" @click="onDialogCancel" />
|
||||
<!-- <q-btn :label="textOk" :color="color" @click="onOKClick" /> -->
|
||||
</q-card-actions>
|
||||
<q-card-actions align="right" class="bg-white text-teal" v-else>
|
||||
<q-btn label="ยกเลิก" flat color="grey-8" @click="onDialogCancel" />
|
||||
<q-btn :label="textOk" :color="color" @click="onOKClick" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogPluginComponent } from "quasar"
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
textOk: {
|
||||
type: String,
|
||||
default: "ตกลง",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "หัวข้อ?",
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: "ข้อความ",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "question_mark",
|
||||
},
|
||||
onlycancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
// REQUIRED; need to specify some events that your
|
||||
// component will emit through useDialogPluginComponent()
|
||||
...useDialogPluginComponent.emits,
|
||||
])
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
||||
// dialogRef - Vue ref to be applied to QDialog
|
||||
// onDialogHide - Function to be used as handler for @hide on QDialog
|
||||
// onDialogOK - Function to call to settle dialog with "ok" outcome
|
||||
// example: onDialogOK() - no payload
|
||||
// example: onDialogOK({ /*...*/ }) - with payload
|
||||
// onDialogCancel - Function to call to settle dialog with "cancel" outcome
|
||||
|
||||
// this is part of our example (so not required)
|
||||
function onOKClick() {
|
||||
// on OK, it is REQUIRED to
|
||||
// call onDialogOK (with optional payload)
|
||||
onDialogOK()
|
||||
// or with payload: onDialogOK({ ... })
|
||||
// ...and it will also hide the dialog automatically
|
||||
}
|
||||
</script>
|
||||
27
src/components/DialogHeader.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title class="text-subtitle2 text-bold">{{
|
||||
tittle
|
||||
}}</q-toolbar-title>
|
||||
<q-btn
|
||||
icon="close"
|
||||
unelevated
|
||||
round
|
||||
dense
|
||||
@click="close"
|
||||
style="color: #ff8080; background-color: #ffdede"
|
||||
/>
|
||||
</q-toolbar>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
tittle: String,
|
||||
close: {
|
||||
type: Function,
|
||||
default: () => console.log("not function"),
|
||||
},
|
||||
});
|
||||
function close() {
|
||||
props.close();
|
||||
}
|
||||
</script>
|
||||
186
src/components/Table.vue
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<div class="q-pb-sm row">
|
||||
<div>
|
||||
<q-btn
|
||||
size="14px"
|
||||
flat
|
||||
dense
|
||||
color="blue"
|
||||
@click="add"
|
||||
icon="mdi-plus"
|
||||
>
|
||||
<q-tooltip>เพิ่มข้อมูล</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<q-space />
|
||||
<div class="items-center q-gutter-sm" style="display: flex">
|
||||
<!-- ค้นหาข้อความใน table -->
|
||||
<q-input
|
||||
standout
|
||||
dense
|
||||
:model-value="inputfilter"
|
||||
ref="filterRef"
|
||||
@update:model-value="updateInput"
|
||||
outlined
|
||||
debounce="300"
|
||||
placeholder="ค้นหา"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="inputfilter == ''" name="search" />
|
||||
<q-icon
|
||||
v-if="inputfilter !== ''"
|
||||
name="clear"
|
||||
class="cursor-pointer"
|
||||
@click="resetFilter"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<!-- แสดงคอลัมน์ใน table -->
|
||||
<q-select
|
||||
:model-value="inputvisible"
|
||||
@update:model-value="updateVisible"
|
||||
:display-value="$q.lang.table.columns"
|
||||
multiple
|
||||
outlined
|
||||
dense
|
||||
:options="attrs.columns"
|
||||
options-dense
|
||||
option-value="name"
|
||||
map-options
|
||||
emit-value
|
||||
style="min-width: 150px"
|
||||
class="gt-xs"
|
||||
>
|
||||
<template> </template>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<q-table
|
||||
ref="table"
|
||||
flat
|
||||
bordered
|
||||
class="custom-table2"
|
||||
v-bind="attrs"
|
||||
virtual-scroll
|
||||
:virtual-scroll-sticky-size-start="48"
|
||||
dense
|
||||
:pagination-label="paginationLabel"
|
||||
:pagination="initialPagination"
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span class="text-weight-medium" v-html="col.label" />
|
||||
</q-th>
|
||||
<q-th auto-width v-if="inputShow" />
|
||||
</q-tr>
|
||||
</template>
|
||||
<template #body="props">
|
||||
<slot v-bind="props" name="columns"></slot>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs } from "vue";
|
||||
const attrs = ref<any>(useAttrs());
|
||||
const table = ref<any>(null);
|
||||
const filterRef = ref<any>(null);
|
||||
const initialPagination = ref({
|
||||
rowsPerPage: 0,
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
count: Number,
|
||||
pass: Number,
|
||||
notpass: Number,
|
||||
|
||||
inputfilter: String,
|
||||
name: String,
|
||||
icon: String,
|
||||
inputvisible: Array,
|
||||
editvisible: Boolean,
|
||||
|
||||
inputShow: Boolean,
|
||||
|
||||
add: {
|
||||
type: Function,
|
||||
default: () => console.log("not function"),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:inputfilter",
|
||||
"update:inputvisible",
|
||||
"update:editvisible",
|
||||
]);
|
||||
const updateInput = (value: string | number | null) => {
|
||||
emit("update:inputfilter", value);
|
||||
};
|
||||
const updateVisible = (value: []) => {
|
||||
emit("update:inputvisible", value);
|
||||
};
|
||||
|
||||
const paginationLabel = (start: string, end: string, total: string) => {
|
||||
return start + "-" + end + " ใน " + total;
|
||||
};
|
||||
|
||||
const resetFilter = () => {
|
||||
// reset ค่าที่ค้นหาเมื่อกดปุ่ม X ในกล่องค้นหา
|
||||
emit("update:inputfilter", "");
|
||||
filterRef.value.focus();
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
props.add();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.icon-color {
|
||||
color: #4154b3;
|
||||
}
|
||||
|
||||
.custom-table2 {
|
||||
max-height: 64vh;
|
||||
|
||||
.q-table tr:nth-child(odd) td {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.q-table tr:nth-child(even) td {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.q-table thead tr {
|
||||
background: #ecebeb;
|
||||
}
|
||||
|
||||
.q-table thead tr th {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.q-table td:nth-of-type(2) {
|
||||
z-index: 3 !important;
|
||||
}
|
||||
|
||||
.q-table th:nth-of-type(2),
|
||||
.q-table td:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* this will be the loading indicator */
|
||||
.q-table thead tr:last-child th {
|
||||
/* height of all previous header rows */
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
.q-table thead tr:first-child th {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
6
src/interface/index/Main.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface DataOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type { DataOption}
|
||||
13
src/interface/response/Main.ts
Normal file
|
|
@ -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 }
|
||||
35
src/main.ts
Normal file
|
|
@ -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')
|
||||
22
src/plugins/filters.ts
Normal file
|
|
@ -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;
|
||||
11
src/quasar-user-options.ts
Normal file
|
|
@ -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: {},
|
||||
}
|
||||
34
src/registerServiceWorker.ts
Normal file
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
30
src/router/index.ts
Normal file
|
|
@ -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
|
||||
1
src/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module '*.vue'
|
||||
327
src/stores/mixin.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
139
src/style/quasar-variables.sass
Normal file
|
|
@ -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
|
||||
27
src/views/ErrorNotFoundPage.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Error404NotFound",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fullscreen bg-secondary text-white text-center q-pa-md flex flex-center"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h1">ไม่พบหน้าที่ต้องการ</div>
|
||||
<div class="text-h2">(404 Page Not Found)</div>
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="secondary"
|
||||
unelevated
|
||||
to="/"
|
||||
label="กลับไปหน้าหลัก"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
11
src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="col-12 row justify-center">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||
<q-card flat class="row col-12 cardNone">
|
||||
Publish
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
tsconfig.app.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
14
tsconfig.config.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
tsconfig.vitest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
66
vite.config.js
Normal file
|
|
@ -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'],
|
||||
},
|
||||
})
|
||||