start project

This commit is contained in:
Warunee Tamkoo 2024-12-16 17:12:39 +07:00
commit ca3c00b6a1
42 changed files with 1310 additions and 0 deletions

7
.env.example Normal file
View file

@ -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"

30
.gitignore vendored Normal file
View file

@ -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

30
README.md Normal file
View file

@ -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/).

8
cypress.config.ts Normal file
View 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',
},
})

19
docker/Dockerfile Normal file
View file

@ -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;"]

18
docker/entrypoint.sh Normal file
View file

@ -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;'

30
docker/nginx.conf Normal file
View 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;
}
}
}

1
env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

34
index.html Normal file
View file

@ -0,0 +1,34 @@
<!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>HRMS SSO</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>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
crossorigin="anonymous"
></script>
</body>
</html>

53
package.json Normal file
View file

@ -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"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Disallow:

26
src/App.vue Normal file
View file

@ -0,0 +1,26 @@
<template>
<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.org.ts Normal file
View file

@ -0,0 +1,7 @@
import env from "./index";
const org = `${env.API_URI}/org`;
// const log = `${env.API_URI}/log`;
export default {
org,
};

29
src/api/index.ts Normal file
View file

@ -0,0 +1,29 @@
/**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: 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,
}

13
src/app.config.ts Normal file
View file

@ -0,0 +1,13 @@
/**ใช้รวมไฟล์ย่อยๆ ของ api แต่ละไฟล์ */
/** API ระบบลงเวลา */
import org from "@/api/api.org";
const API = {
/**message */
...org,
};
export default {
API: API,
};

51
src/assets/key/BMA Normal file
View file

@ -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-----

1
src/assets/key/BMA.pub Normal file
View file

@ -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

View file

@ -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-----

BIN
src/assets/line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
src/assets/sso.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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,
}

View file

@ -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 }

38
src/main.ts Normal file
View file

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

24
src/plugins/axios.ts Normal file
View file

@ -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

20
src/plugins/filters.ts Normal file
View file

@ -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

39
src/plugins/http.ts Normal file
View file

@ -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<any>) {
config.headers = config.headers ?? {};
return config;
},
function (error: any) {
return Promise.reject(error);
}
);
http.interceptors.response.use(
function (response: AxiosResponse<any, any>) {
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;

70
src/plugins/keycloak.ts Normal file
View file

@ -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,
// }

View 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: {},
}

View 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)
},
})
}

29
src/router/index.ts Normal file
View file

@ -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;

1
src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.vue'

13
src/stores/main.ts Normal file
View file

@ -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 {
}
})

91
src/stores/mixin.ts Normal file
View file

@ -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,
};
});

View 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

View 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>

210
src/views/login.vue Normal file
View file

@ -0,0 +1,210 @@
<!-- authen with keycloak client -->
<script setup lang="ts">
import { onMounted, ref } from "vue";
import axios from "axios";
import { useCounterMixin } from "@/stores/mixin";
import config from "@/app.config";
const mixin = useCounterMixin();
const { showLoader, hideLoader } = mixin;
const msgError = ref<string>("");
const username = ref<string>("");
const password = ref<string>("");
async function onSubmit() {
showLoader();
const formdata = new URLSearchParams();
formdata.append("username", username.value);
formdata.append("password", password.value);
await axios
.post(`${config.API.org}/login`, formdata, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then(async (res) => {
// setAuthen(res.data.result);
})
.catch((err) => {
// $q.dialog({
// component: CustomComponent,
// componentProps: {
// title: ``,
// message: `${err.response.data.message}`,
// icon: "warning",
// color: "red",
// onlycancel: true,
// },
// });
})
.finally(() => {
hideLoader();
});
}
onMounted(async () => {
// const checkAuthen = await authenticated();
// if (checkAuthen) {
// router.push("/");
// }
});
</script>
<template>
<section class="vh-100" style="overflow: hidden">
<div class="container-fluid h-custom">
<div class="row d-flex justify-content-center align-items-center h-100">
<!-- 1 sec -->
<div
class="col-md-12 col-lg-6 bg-img d-none d-lg-block position-relative"
>
<img src="@/assets/line.png" class="img_absolute_line" />
<div
class="d-flex flex-column justify-content-center align-items-center vh-100"
>
<div class="text-white position-relative">
<img src="@/assets/sso.png" class="img_absolute" />
<h4>ระบบบรหารจดการการใชงาน</h4>
<h4 class="pb-2">ระบบสารสนเทศสนบสนนการเชอมโยง</h4>
<p class="mb-0 txt_detail">Bangkok Metropolitan Administration</p>
<p class="mb-0 txt_detail">Single Sign-On</p>
</div>
</div>
</div>
<!-- 2 sec -->
<div class="col-10 col-md-8 col-lg-5 offset-lg-1">
<form class="bg_md" id="contact-form">
<div class="d_over">
<div class="text-center">
<img src="@/assets/sso.png" />
</div>
<div class="my-3 text-center" style="color: #00aa86">
<h5>ระบบบรหารจดการการใชงาน</h5>
<h5 class="pb-1">ระบบสารสนเทศสนบสนนการเชอมโยง</h5>
<p class="mb-0 txt_detail">
Bangkok Metropolitan Administration
</p>
<p class="mb-0 txt_detail">Single Sign-On</p>
</div>
</div>
<div
class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start"
>
<p class="lead fw-normal mb-0 me-3 mb-2">
<strong>เขาใชงานระบบ</strong>
</p>
</div>
<div class="row g-4">
<div class="col-md-12 col-lg-9">
<input
type="text"
id="username"
value=""
class="form-control form-control-lg"
placeholder="ชื่อผู้ใช้"
/>
</div>
<div class="col-md-12 col-lg-9">
<input
type="password"
id="password"
value=""
class="form-control form-control-lg"
placeholder="รหัสผ่าน"
/>
</div>
<div class="col-md-12 col-lg-9 d-grid">
<button type="submit" class="btn_custom">เขาระบบ</button>
<div id="response-message">{{ msgError }}</div>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
.img_absolute_line {
position: absolute;
height: auto;
width: 50%;
left: 0;
bottom: 0;
}
.txt_detail {
color: #c0c0c0;
}
.img_absolute {
position: absolute;
top: -170px;
}
.d_over {
display: none;
}
.btn_custom {
background-color: #00aa86;
cursor: pointer;
border: 0;
padding: 8px 0 8px 0;
border-radius: 12px;
color: #fff;
font-size: 18px;
font-weight: 500;
}
.btn_custom:hover {
background-color: #00ca9f;
}
.bg-img {
background: rgb(30, 50, 49);
background: linear-gradient(
0deg,
rgba(30, 50, 49, 1) 0%,
rgba(20, 120, 99, 1) 100%
);
}
body {
font-family: "Noto Sans Thai", sans-serif !important;
}
.h-custom {
height: calc(100% - 73px);
}
@media (max-width: 450px) {
.h-custom {
height: 100%;
}
}
@media (max-width: 991px) {
/* Up to medium screens */
section.vh-100 {
background: rgb(30, 50, 49);
background: linear-gradient(
0deg,
rgba(30, 50, 49, 1) 0%,
rgba(20, 120, 99, 1) 100%
);
}
.bg_md {
background-color: #fff;
padding: 20px;
border-radius: 20px;
}
.d_over {
display: block;
}
.txt_detail {
color: #949494;
font-size: 12px;
}
}
#response-message {
color: red;
text-align: center;
}
</style>

16
tsconfig.app.json Normal file
View 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"]
}
}

16
tsconfig.config.json Normal file
View file

@ -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"
}
}

14
tsconfig.json Normal file
View 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
View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.app.json",
"exclude": [],
"compilerOptions": {
"composite": true,
"lib": [],
"types": ["node", "jsdom"]
}
}

66
vite.config.js Normal file
View 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: 3002,
},
optimizeDeps: {
include: ['esri-loader'],
},
})