client initial commit
28
Services/client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 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?
|
||||
5
Services/client/.prettierrc.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
103
Services/client/README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# ระบบ BMA eHR (eHR กรุงเทพมหานคร)
|
||||
|
||||
สำหรับ User
|
||||
|
||||
## โครงสร้างโฟลเดอร์
|
||||
|
||||
ภายใต้ src จะประกอบด้วยโฟลเดอร์ที่จำเป็นดังนี้
|
||||
|
||||
1. **assets** : สำหรับเก็บไฟล์ภาพกราฟิก + css
|
||||
2. **components** : สำหรับเก็บ Component ส่วนกลางที่สามารถเรียกใช้จากระบบใดก็ได้
|
||||
3. **helpers** : สำหรับเก็บ filter, composable ที่เรียกใช้จากระบบใดก็ได้
|
||||
4. **modules** : สำหรับเก็บไฟล์โค้ดหลัก ภายในจะแบ่งเป็นโฟลเดอร์ย่อยของระบบต่าง ๆ ทั้ง 17 ระบบ 17 โฟลเดอร์ ภายใต้โฟลเดอร์ย่อยของระบบจะเป็นโครงสร้างแบบเดียวกับ vue template เช่น ภายใต้โฟลเดอร์ 01_metadata (ระบบข้อมูลหลัก) ประกอบด้วยโฟลเดอร์
|
||||
- **components** : โฟลเดอร์เก็บ Component ที่ใช้เฉพาะระบบข้อมูลหลัก
|
||||
- **views** : โฟลเดอร์เก็บ View/Layout ที่ใช้เฉพาะระบบข้อมูลหลัก
|
||||
|
||||
## รูปแบบ SFC ที่แนะนำให้ใช้ใน Vue 3
|
||||
|
||||
ลำดับการวางโค้ดใน Vue 3 จะย้ายแท็ก script ขึ้นก่อนแท็ก template ส่วนแท็ก style จะอยู่ลำดับล่างสุดเหมือนเดิม
|
||||
|
||||
- แท็ก script ควรมี setup และ lang="ts" บอกเสมอ
|
||||
- แท็ก style ควรกำหนด lang และ scoped ด้วย
|
||||
|
||||
ตัวอย่างด้านล่าง
|
||||
|
||||
```
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
```
|
||||
|
||||
## การอ้างอิง Path
|
||||
|
||||
Vue Template ได้สร้าง shortcut ไว้ให้แล้วด้วยการใส่เครื่องหมาย '@' นำหน้า ซึ่ง @ แทน src ดังนั้นพาธใด ๆ ก็ต้องจะอยู่ภายใน @ ทั้งหมด เช่น ต้องการเรียก src/components ให้ใส่ว่า "@/components" ถ้าเป็นไปได้ไม่ควรใช้ relative path
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
|
||||
|
||||
```sh
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
This runs the end-to-end tests against the Vite development server.
|
||||
It is much faster than the production build.
|
||||
|
||||
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
8
Services/client/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',
|
||||
},
|
||||
})
|
||||
8
Services/client/cypress/e2e/example.cy.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// https://docs.cypress.io/api/introduction/api.html
|
||||
|
||||
describe('My First Test', () => {
|
||||
it('visits the app root url', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('h1', 'You did it!')
|
||||
})
|
||||
})
|
||||
10
Services/client/cypress/e2e/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["./**/*", "../support/**/*"],
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
5
Services/client/cypress/fixtures/example.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
39
Services/client/cypress/support/commands.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {}
|
||||
20
Services/client/cypress/support/e2e.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
1
Services/client/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
13
Services/client/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!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>ระบบทรัพยากรบุคคล</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
10255
Services/client/package-lock.json
generated
Normal file
59
Services/client/package.json
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "ehr_portfolio",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"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": {
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.8",
|
||||
"@fullcalendar/list": "^6.1.8",
|
||||
"@fullcalendar/timegrid": "^6.1.8",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@quasar/extras": "^1.15.8",
|
||||
"@vuepic/vue-datepicker": "^5.2.1",
|
||||
"axios": "^1.6.2",
|
||||
"keycloak-js": "^22.0.5",
|
||||
"pinia": "^2.1.4",
|
||||
"quasar": "^2.11.1",
|
||||
"vite-plugin-pwa": "^0.16.7",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/vite-plugin": "^1.6.0",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/node": "^18.11.12",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
6056
Services/client/pnpm-lock.yaml
generated
Normal file
BIN
Services/client/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
Services/client/public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Services/client/public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
Services/client/public/icons/android-chrome-maskable-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Services/client/public/icons/android-chrome-maskable-512x512.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
Services/client/public/icons/android-launchericon-144-144.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Services/client/public/icons/android-launchericon-48-48.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Services/client/public/icons/android-launchericon-72-72.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
Services/client/public/icons/android-launchericon-96-96.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
Services/client/public/icons/apple-touch-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
Services/client/public/icons/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
Services/client/public/icons/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Services/client/public/icons/apple-touch-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
Services/client/public/icons/apple-touch-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
Services/client/public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
Services/client/public/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
Services/client/public/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Services/client/public/icons/msapplication-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Services/client/public/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
27
Services/client/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 |
8
Services/client/public/keycloak.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"realm": "vue",
|
||||
"auth-server-url": "https://edm-id.frappet.synology.me/",
|
||||
"ssl-required": "external",
|
||||
"resource": "vuejs",
|
||||
"public-client": true,
|
||||
"confidential-port": 0
|
||||
}
|
||||
2
Services/client/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
||||
13
Services/client/src/App.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div id="azay-admin-app">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
BIN
Services/client/src/assets/avatar_user.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
Services/client/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
219
Services/client/src/components/FileItem.vue
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import FileItemAction from '@/components/FileItemAction.vue'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
|
||||
|
||||
const { currentFolder, currentFile, currentDept } = storeToRefs(
|
||||
useTreeDataStore()
|
||||
)
|
||||
const { getFolder, createFolder, editFolder } = useTreeDataStore()
|
||||
const drawer = ref<boolean>(false)
|
||||
const drawerStatus = ref<'edit' | 'create'>('create')
|
||||
const input = ref<string>('')
|
||||
const editPathname = ref<string>('')
|
||||
const currentIcon = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'mdi-file-cabinet'
|
||||
: currentDept.value === 1
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
)
|
||||
const props = withDefaults(
|
||||
defineProps<{ action: boolean; viewMode: 'view_list' | 'view_module' }>(),
|
||||
{
|
||||
action: false,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div
|
||||
:key="value.name"
|
||||
v-for="value in currentFolder"
|
||||
class="inline-block"
|
||||
>
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="() => getFolder(value.pathname)">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon :name="currentIcon" size="6em" color="primary" />
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
:editname="value.name"
|
||||
:pathname="value.pathname"
|
||||
@editname="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus = 'edit'
|
||||
editPathname = value.pathname
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-center q-pt-md">{{ value.name }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-block" v-if="props.action && currentDept < 4">
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card
|
||||
flat
|
||||
@click="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus = 'create'
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon
|
||||
:name="currentIcon"
|
||||
class="add-icon"
|
||||
size="6em"
|
||||
color="primary"
|
||||
/>
|
||||
<q-btn round class="add-button" color="white" size="10px">
|
||||
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
|
||||
</q-btn>
|
||||
<span class="text-center q-pt-md"
|
||||
>สร้าง{{ DEPT_NAME[currentDept] }}ใหม่</span
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div v-for="value in currentFile" :key="value.title" class="inline-block">
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon name="description" size="6em" color="primary" />
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<!-- TODO: Edit file data -->
|
||||
</div>
|
||||
<span class="text-center q-pt-md">{{ value.title }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-block" v-if="props.action && currentDept > 2">
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card flat @click="() => (drawer = !drawer)">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon
|
||||
name="description"
|
||||
class="add-icon"
|
||||
size="6em"
|
||||
color="primary"
|
||||
/>
|
||||
<q-btn round class="add-button" color="white" size="10px">
|
||||
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
|
||||
</q-btn>
|
||||
<span class="text-center q-pt-md">สร้างไฟล์ใหม่</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-drawer
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
v-model="drawer"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold" v-if="drawerStatus == 'edit'"
|
||||
>แก้ไขชื่อ{{ DEPT_NAME[currentDept] }}</span
|
||||
>
|
||||
<span class="text-weight-bold" v-if="drawerStatus == 'create'"
|
||||
>สร้าง{{ DEPT_NAME[currentDept] }}</span
|
||||
>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
color="red"
|
||||
@click="drawer = !drawer"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<span class="text-weight-bold">ชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="input"
|
||||
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus === 'create'
|
||||
? createFolder(input)
|
||||
: editFolder(input, editPathname)
|
||||
input = ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
display: inline-block;
|
||||
border: 2px solid #f1f2f4;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashed {
|
||||
opacity: 0.4;
|
||||
display: inline-block;
|
||||
border: 2px solid #babdc3;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
margin: auto auto;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
right: 45px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
92
Services/client/src/components/FileItemAction.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const { currentDept } = storeToRefs(useTreeDataStore())
|
||||
const { deleteFolder, editFolder } = useTreeDataStore()
|
||||
defineProps<{
|
||||
pathname: string
|
||||
editname: string
|
||||
}>()
|
||||
defineEmits([
|
||||
'editname',
|
||||
'deletename',
|
||||
])
|
||||
|
||||
const editdrawer = ref(false)
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
|
||||
const editInput = ref<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn @click.stop icon="more_vert" color="grey" flat dense>
|
||||
<q-menu auto-close>
|
||||
<q-list dense>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => (
|
||||
$emit('editname')
|
||||
)">
|
||||
<div class="row items-center">
|
||||
<q-icon name="edit" color="positive" />
|
||||
<span class="q-ml-sm">แก้ไข</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => deleteFolder(pathname)">
|
||||
<div class="row items-center">
|
||||
<q-icon name="delete" color="negative" />
|
||||
<span class="q-ml-sm">ลบ</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<!-- <q-drawer
|
||||
class="q-pa-md"
|
||||
side="left"
|
||||
v-model="editdrawer"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold">แก้ไขชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
color="red"
|
||||
@click="editdrawer = !editdrawer"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<span class="text-weight-bold">ชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="editInput"
|
||||
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
editdrawer = !editdrawer
|
||||
editFolder(editname,editInput)
|
||||
editInput = ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-drawer> -->
|
||||
</template>
|
||||
36
Services/client/src/components/FileSearched.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
|
||||
const { foundFile } = storeToRefs(useSearchDataStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-mt-md" v-if="foundFile.length > 0">
|
||||
<div class="q-gutter-md">
|
||||
<div v-for="value in foundFile" :key="value.title" class="inline-block">
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon name="description" size="6em" color="primary" />
|
||||
<span class="text-center q-pt-md">{{ value.title }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" v-if="foundFile.length == 0">
|
||||
<span>ไม่พบรายการที่ค้นหา</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.box {
|
||||
display: inline-block;
|
||||
border: 2px solid #f1f2f4;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
22
Services/client/src/components/FullLoader.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { watch, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visibility: Boolean,
|
||||
})
|
||||
|
||||
const loaderVisibility = ref<boolean>(props.visibility)
|
||||
|
||||
watch(props, () => (loaderVisibility.value = props.visibility))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-inner-loading :showing="loaderVisibility" class="loader">
|
||||
<q-spinner-cube size="80px" color="primary" />
|
||||
</q-inner-loading>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.loader
|
||||
z-index: 1000
|
||||
</style>
|
||||
122
Services/client/src/components/PageLayout.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import FileItem from '@/components/FileItem.vue'
|
||||
import TreeExplorer from '@/components/TreeExplorer.vue'
|
||||
import SearchBar from '@/modules/01_user/components/SearchBar.vue'
|
||||
import FileSearched from '@/components/FileSearched.vue'
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย', 'ไฟล์']
|
||||
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { data, currentDept } = storeToRefs(useTreeDataStore())
|
||||
const { getCabinet, gotoParent } = useTreeDataStore()
|
||||
const viewMode = ref<'view_list' | 'view_module'>('view_list')
|
||||
const inputSearch = ref<string>()
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
||||
onMounted(getCabinet)
|
||||
</script>
|
||||
<template>
|
||||
<section id="header" class="q-px-md q-pt-md q-pb-none">
|
||||
<div class="q-my-md row items-center">
|
||||
<div class="col">
|
||||
<h5 class="q-my-none" v-if="mode === 'admin'">จัดเก็บเอกสาร</h5>
|
||||
<h5 class="q-my-none" v-else>สืบค้นเอกสาร</h5>
|
||||
</div>
|
||||
<div class="col-3" v-if="mode === 'admin'">
|
||||
<q-input
|
||||
rounded
|
||||
outlined
|
||||
dense
|
||||
label="ค้นหา"
|
||||
bg-color="white"
|
||||
v-model="inputSearch"
|
||||
>
|
||||
<template v-slot:append><q-icon name="search" /></template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="row q-col-gutter-md q-pa-md"
|
||||
:class="{ reverse: props.mode === 'user' }"
|
||||
id="cabinet"
|
||||
>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="bg-white rounded-borders shadow-5">
|
||||
<div
|
||||
class="q-px-md q-py-sm text-primary bg-grey-1"
|
||||
id="container-header"
|
||||
>
|
||||
<span class="block q-my-sm text-weight-bold">ตู้จัดเก็บเอกสาร</span>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="q-pa-md">
|
||||
<tree-explorer :data="data" :level="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!-- <file-download /> -->
|
||||
<div class="bg-white rounded-borders shadow-5 relative">
|
||||
<search-bar v-if="mode === 'user'" />
|
||||
<div class="bg-white q-pa-md">
|
||||
<div class="row items-center justify-between">
|
||||
<span class="text-h6">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
v-if="currentDept > 0"
|
||||
@click="() => gotoParent()"
|
||||
>
|
||||
<q-icon name="arrow_back" size="1rem" color="primary"
|
||||
/></q-btn>
|
||||
<span>{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-btn
|
||||
v-if="mode === 'admin' && viewMode === 'view_module'"
|
||||
class="q-px-md q-ml-md"
|
||||
label="สร้างตู้เก็บเอกสาร"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
icon="add"
|
||||
@click=""
|
||||
/>
|
||||
</span>
|
||||
<q-btn
|
||||
flat
|
||||
color="blue-grey-2"
|
||||
:icon="viewMode"
|
||||
@click="
|
||||
() => {
|
||||
viewMode =
|
||||
viewMode === 'view_list' ? 'view_module' : 'view_list'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<file-searched v-if="isSearch === true" />
|
||||
<file-item
|
||||
:viewMode="viewMode"
|
||||
:action="props.mode === 'admin'"
|
||||
v-if="isSearch === false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
62
Services/client/src/components/Profile.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
const dropdownOpen = ref<boolean>(false)
|
||||
const accountName = ref<string>()
|
||||
|
||||
accountName.value = KeyCloakService.GetUserName()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
@click="() => (dropdownOpen = !dropdownOpen)"
|
||||
class="row q-px-md cursor"
|
||||
id="app-toolbar-title"
|
||||
>
|
||||
<div class="col">
|
||||
<q-avatar>
|
||||
<img :src="`https://cdn.quasar.dev/img/avatar1.jpg`" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cow">
|
||||
<div class="row q-pl-sm">
|
||||
<span class="text-body1">
|
||||
{{ accountName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row q-pl-sm">
|
||||
<span class="text-caption text-grey"> เจ้าหน้าที่ </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-btn-dropdown stretch flat v-model="dropdownOpen">
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
tabindex="0"
|
||||
@click="
|
||||
() => {
|
||||
KeyCloakService.CallLogOut()
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="logout" color="primary" text-color="white" caption>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Logout</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator inset spaced></q-separator>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
52
Services/client/src/components/TreeExplorer.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
|
||||
const { getFolder } = useTreeDataStore()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: TreeDataFolder[]
|
||||
level: number
|
||||
}>(),
|
||||
{
|
||||
level: 0,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-list v-for="folder in data" class="rounded-borders">
|
||||
<q-expansion-item
|
||||
@click="() => getFolder(folder.pathname, false)"
|
||||
:header-inset-level="level * 0.25"
|
||||
:group="level.toString()"
|
||||
:hide-expand-icon="level === 4"
|
||||
:icon="
|
||||
level === 1
|
||||
? 'mdi-file-cabinet'
|
||||
: level === 2
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
"
|
||||
:label="folder.name"
|
||||
v-model="folder.status"
|
||||
>
|
||||
<tree-explorer
|
||||
v-if="folder.folder"
|
||||
:data="folder.folder"
|
||||
:level="props.level + 1"
|
||||
/>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-item[aria-expanded='true'] {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.q-item[aria-expanded='true'] .q-icon {
|
||||
color: $primary;
|
||||
}
|
||||
</style>
|
||||
44
Services/client/src/main.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createApp, defineAsyncComponent } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { Dialog, Quasar, Loading } from 'quasar'
|
||||
import th from 'quasar/lang/th'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import HttpService from '@/services/HttpService'
|
||||
import quasarUserOptions from './quasar-user-options'
|
||||
import router from './router'
|
||||
|
||||
import 'quasar/src/css/index.sass'
|
||||
import '@vuepic/vue-datepicker/dist/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
app.use(Quasar, {
|
||||
...quasarUserOptions,
|
||||
plugins: {
|
||||
Dialog,
|
||||
Loading,
|
||||
},
|
||||
lang: th,
|
||||
})
|
||||
|
||||
app.component(
|
||||
'full-loader',
|
||||
defineAsyncComponent(() => import('@/components/FullLoader.vue'))
|
||||
)
|
||||
|
||||
app.component(
|
||||
'datepicker',
|
||||
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
|
||||
)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
HttpService.configureAxiosKeycloak()
|
||||
|
||||
console.log(import.meta.env)
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isAdvncSearchClick = ref<boolean>(false)
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
]
|
||||
const optionsOp = [
|
||||
{ label: 'และ', value: 'AND' },
|
||||
{ label: 'หรือ', value: 'OR' },
|
||||
]
|
||||
const advSearchData = ref<
|
||||
{
|
||||
op: 'AND' | 'OR'
|
||||
field: 'title' | 'keyword'
|
||||
value: string
|
||||
}[]
|
||||
>([])
|
||||
const props = defineProps<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
searchSubmit: Function
|
||||
submitSearchData: {
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
}[]
|
||||
OR: {
|
||||
field: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
clearAdvData,
|
||||
advSearchData,
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
'update:field',
|
||||
'update:value',
|
||||
'update:keyword',
|
||||
'update:description',
|
||||
])
|
||||
|
||||
function addSearchData() {
|
||||
advSearchData.value.push({
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
|
||||
function clearAdvData() {
|
||||
advSearchData.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-btn
|
||||
outline
|
||||
color="primary"
|
||||
icon="mdi-tools"
|
||||
label="ค้นหาขั้นสูง"
|
||||
@click="() => (isAdvncSearchClick = true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="isAdvncSearchClick">
|
||||
<q-card style="width: 1000px; max-width: 80vw">
|
||||
<q-toolbar class="bg-grey-1 q-py-sm q-px-lg">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold">ค้นหาขั้นสูง</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn flat round dense icon="close" v-close-popup color="red" />
|
||||
</q-toolbar>
|
||||
<q-separator />
|
||||
|
||||
<div class="col">
|
||||
<q-card-section class="row q-mt-sm">
|
||||
<div class="col">
|
||||
<div class="row q-col-gutter-md q-mb-md items-center">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="optionsField"
|
||||
:model-value="props.field"
|
||||
@update:model-value="(value) => $emit('update:field', value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="เอกสาร"
|
||||
:model-value="props.value"
|
||||
@update:model-value="(value) => $emit('update:value', value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-btn
|
||||
dense
|
||||
color="teal"
|
||||
icon="mdi-plus"
|
||||
v-if="advSearchData.length === 0"
|
||||
@click="addSearchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row q-col-gutter-md q-mb-md items-center"
|
||||
v-for="(_, index) in advSearchData"
|
||||
>
|
||||
<div class="col-4 col-md-2">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="advSearchData[index].op"
|
||||
:options="optionsOp"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-8 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="advSearchData[index].field"
|
||||
:options="optionsField"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="advSearchData[index].value"
|
||||
placeholder="เอกสาร"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-btn
|
||||
dense
|
||||
color="teal"
|
||||
icon="mdi-plus"
|
||||
v-if="index === advSearchData.length - 1"
|
||||
@click="addSearchData"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="mdi-minus"
|
||||
color="red"
|
||||
v-if="index === advSearchData.length - 1"
|
||||
@click="() => advSearchData.pop()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="row q-mb-md">
|
||||
<div class="col">
|
||||
<span>อื่น ๆ</span>
|
||||
<q-separator class="q-mb-lg" />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3 q-gutter-y-sm">
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="กรอกคำสำคัญ"
|
||||
:model-value="props.keyword"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:keyword', value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 q-gutter-y-sm">
|
||||
<span class="text-weight-bold">รายละเอียดของเอกสาร</span>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="กรอกรายละเอียด"
|
||||
:model-value="props.description"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:description', value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-toolbar class="row justify-end">
|
||||
<div class="q-py-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
v-close-popup
|
||||
@click="() => props.searchSubmit()"
|
||||
/>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
188
Services/client/src/modules/01_user/components/SearchBar.vue
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
|
||||
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { getFoundFile } = useSearchDataStore()
|
||||
const advSearchComp = ref<InstanceType<typeof AdvancedSearch>>()
|
||||
const isAdvSearchCall = ref<boolean>(false)
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
]
|
||||
const searchData = ref<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
}>({
|
||||
field: 'title',
|
||||
value: '',
|
||||
keyword: '',
|
||||
description: '',
|
||||
})
|
||||
const submitSearchData = ref<{
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
}[]
|
||||
OR: { field: string; value: string }[]
|
||||
}>({
|
||||
AND: [],
|
||||
OR: [],
|
||||
})
|
||||
|
||||
function clearSearchData() {
|
||||
searchData.value = {
|
||||
field: 'title',
|
||||
value: '',
|
||||
keyword: '',
|
||||
description: '',
|
||||
}
|
||||
submitSearchData.value = {
|
||||
AND: [],
|
||||
OR: [],
|
||||
}
|
||||
advSearchComp.value?.clearAdvData()
|
||||
isAdvSearchCall.value = false
|
||||
isSearch.value = false
|
||||
}
|
||||
|
||||
async function searchSubmit() {
|
||||
submitSearchData.value = {
|
||||
AND: [],
|
||||
OR: [],
|
||||
}
|
||||
if (searchData.value.field && searchData.value) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: searchData.value.field,
|
||||
value: searchData.value.value,
|
||||
})
|
||||
if (searchData.value.keyword) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: 'keyword',
|
||||
value: searchData.value.keyword,
|
||||
})
|
||||
}
|
||||
if (searchData.value.description) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: 'description',
|
||||
value: searchData.value.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdvSearchCall.value) {
|
||||
if (
|
||||
advSearchComp.value?.advSearchData &&
|
||||
advSearchComp.value.advSearchData.length > 0
|
||||
) {
|
||||
advSearchComp.value?.advSearchData.forEach(
|
||||
(d: { field: string; value: string; op: string }) => {
|
||||
if (d.field && d.value) {
|
||||
if (d.op === 'AND') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
})
|
||||
} else if (d.op === 'OR') {
|
||||
submitSearchData.value.OR.push({ field: d.field, value: d.value })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
try {
|
||||
loaderStore.show()
|
||||
isAdvSearchCall.value = true
|
||||
const res = await axios.post<EhrFile[]>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}/search`,
|
||||
submitSearchData.value
|
||||
)
|
||||
getFoundFile(res.data)
|
||||
isSearch.value = true
|
||||
} catch (error) {
|
||||
console.error('Error during the request', error)
|
||||
} finally {
|
||||
loaderStore.hide()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-py-lg q-px-md bg-grey-1">
|
||||
<div class="row items-center q-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
class="bg-white"
|
||||
v-model="searchData.field"
|
||||
:options="optionsField"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-grow">
|
||||
<q-input
|
||||
class="bg-white"
|
||||
dense
|
||||
outlined
|
||||
v-model="searchData.value"
|
||||
placeholder="เอกสาร"
|
||||
@keydown.enter.prevent="searchSubmit"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAdvSearchCall === false">
|
||||
<q-btn
|
||||
class="col-12 col-md-2"
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="() => searchSubmit()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isAdvSearchCall"
|
||||
class="row items-center justify-between q-gutter-y-md q-mt-xs"
|
||||
>
|
||||
<div>
|
||||
<advanced-search
|
||||
ref="advSearchComp"
|
||||
:searchSubmit="searchSubmit"
|
||||
:submit-search-data="submitSearchData"
|
||||
v-model:field="searchData.field"
|
||||
v-model:value="searchData.value"
|
||||
v-model:keyword="searchData.keyword"
|
||||
v-model:description="searchData.description"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-gutter-x-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="searchSubmit"
|
||||
/>
|
||||
<q-btn
|
||||
color="orange-12"
|
||||
label="ล้างข้อมูล"
|
||||
icon="close"
|
||||
@click="clearSearchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</template>
|
||||
9
Services/client/src/modules/01_user/router.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// const adminHomePage = () => import('@/modules/02_transfer/views/Main.vue')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
name: 'UserHomePage',
|
||||
component: () => import('@/modules/01_user/views/homePage.vue')
|
||||
}
|
||||
]
|
||||
8
Services/client/src/modules/01_user/views/homePage.vue
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import PageLayout from '@/components/PageLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<page-layout mode="user" />
|
||||
</template>
|
||||
<style scoped></style>
|
||||
9
Services/client/src/modules/02_admin/router.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// const adminHomePage = () => import('@/modules/02_transfer/views/Main.vue')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'AdminHomePage',
|
||||
component: () => import('@/modules/02_admin/views/homePage.vue')
|
||||
}
|
||||
]
|
||||
18
Services/client/src/modules/02_admin/views/homePage.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import PageLayout from '@/components/PageLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<page-layout mode="admin" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#container > [class^='col'] > * {
|
||||
border: 1px solid $separator-color;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container-header {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
||||
10
Services/client/src/quasar-user-options.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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: {},
|
||||
}
|
||||
55
Services/client/src/router/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import UserModule from '@/modules/01_user/router'
|
||||
import AdminModule from '@/modules/02_admin/router'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'UserModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
children: [...UserModule],
|
||||
meta: {
|
||||
statusAccount: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'AdminModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
beforeEnter: (_to, _from, next) => {
|
||||
const token = KeyCloakService.GetAccesToken()
|
||||
if (token) {
|
||||
next()
|
||||
} else {
|
||||
KeyCloakService.CallLogin(() => {
|
||||
const tokenAfterLogin = KeyCloakService.GetAccesToken()
|
||||
if (tokenAfterLogin) {
|
||||
next()
|
||||
} else {
|
||||
console.error('ไม่สามารถดึง Token หลังจากล็อกอินได้')
|
||||
next('/')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
statusAccount: true,
|
||||
},
|
||||
children: [...AdminModule],
|
||||
},
|
||||
/**
|
||||
* 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'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
12
Services/client/src/router/loader.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
***** DEPRECATED - Must be delete later *****
|
||||
*/
|
||||
|
||||
/**โหลดหน้าแบบ async/await
|
||||
* @param view "ชี่อไฟล์".vue
|
||||
* @param folder "folderในsrc" [Ex. /src/"folder"] default=views เมื่อไม่ส่งค่า
|
||||
*/
|
||||
export function load(view: string, folder: string = 'views') {
|
||||
// console.log(`@/${folder}/${view}.vue`);
|
||||
return async () => await import(`@/${folder}/${view}.vue`)
|
||||
}
|
||||
37
Services/client/src/services/HttpService.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
|
||||
const HttpMethods = {
|
||||
GET: 'GET',
|
||||
POST: 'POST',
|
||||
DELETE: 'DELETE',
|
||||
}
|
||||
|
||||
const _axios = axios.create()
|
||||
const cb = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers.Authorization = `Bearer ${KeyCloakService.GetAccesToken()}`
|
||||
console.log(config.headers)
|
||||
return config
|
||||
}
|
||||
|
||||
const configureAxiosKeycloak = (): void => {
|
||||
_axios.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||
if (KeyCloakService.IsLoggedIn()) {
|
||||
KeyCloakService.UpdateToken(cb(config))
|
||||
}
|
||||
return config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getAxiosClient = (): AxiosInstance => _axios
|
||||
|
||||
const HttpService = {
|
||||
HttpMethods,
|
||||
configureAxiosKeycloak,
|
||||
getAxiosClient,
|
||||
}
|
||||
|
||||
export default HttpService
|
||||
71
Services/client/src/services/KeyCloakService.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import Keycloak from "keycloak-js";
|
||||
|
||||
const keycloakInstance = new Keycloak();
|
||||
|
||||
interface CallbackOneParam<T1 = void, T2 = void> {
|
||||
(param1: T1): T2;
|
||||
}
|
||||
/**
|
||||
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
|
||||
*
|
||||
* @param onAuthenticatedCallback
|
||||
*/
|
||||
const Login = (onAuthenticatedCallback: CallbackOneParam): void => {
|
||||
keycloakInstance
|
||||
.init({ onLoad: "login-required" })
|
||||
.then(function (authenticated) {
|
||||
authenticated ? onAuthenticatedCallback() : alert("non authenticated");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.dir(e);
|
||||
console.log(`keycloak init exception: ${e}`);
|
||||
});
|
||||
};
|
||||
|
||||
const UserName = (): string | undefined =>
|
||||
keycloakInstance?.tokenParsed?.preferred_username;
|
||||
|
||||
const Token = (): string | undefined => keycloakInstance?.token;
|
||||
const IdToken = (): string | undefined => keycloakInstance?.idToken;
|
||||
|
||||
const LogOut = () => keycloakInstance.logout();
|
||||
|
||||
/*
|
||||
const UserRoles = (): string[] | undefined => {
|
||||
if (keycloakInstance.resourceAccess === undefined) return undefined;
|
||||
if (keycloakInstance.resourceAccess["express-client"] === undefined) return undefined;
|
||||
|
||||
return keycloakInstance.resourceAccess["express-client"].roles;
|
||||
};
|
||||
*/
|
||||
const UserRoles = ():string[] =>{
|
||||
return DecodeToken()?.role
|
||||
}
|
||||
|
||||
|
||||
const updateToken = (successCallback: any) =>
|
||||
keycloakInstance.updateToken(5).then(successCallback).catch(doLogin);
|
||||
|
||||
const doLogin = keycloakInstance.login;
|
||||
|
||||
const isLoggedIn = () => !!keycloakInstance.token;
|
||||
|
||||
const DecodeToken = ()=>{return keycloakInstance.tokenParsed}
|
||||
const DecodeIdToken = ()=>{return keycloakInstance.idTokenParsed}
|
||||
|
||||
const KeycloakService = {
|
||||
CallLogin: Login,
|
||||
GetUserName: UserName,
|
||||
GetAccesToken: Token,
|
||||
GetIdToken: IdToken,
|
||||
CallLogOut: LogOut,
|
||||
GetUserRoles: UserRoles,
|
||||
UpdateToken: updateToken,
|
||||
IsLoggedIn: isLoggedIn,
|
||||
GetDecodeToken:DecodeToken,
|
||||
GetDecodeIdToken:DecodeIdToken
|
||||
};
|
||||
|
||||
export default KeycloakService;
|
||||
|
||||
|
||||
10
Services/client/src/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// Mocks all files ending in `.vue` showing them as plain Vue instances
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
20
Services/client/src/stores/loader.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useLoader = defineStore('data', () => {
|
||||
const loader = ref<boolean>(false)
|
||||
|
||||
function show() {
|
||||
loader.value = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
loader.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
loader,
|
||||
show,
|
||||
hide,
|
||||
}
|
||||
})
|
||||
18
Services/client/src/stores/searched-data.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
|
||||
export const useSearchDataStore = defineStore('searched', () => {
|
||||
const foundFile = ref<EhrFile[]>([])
|
||||
const isSearch = ref<Boolean>(false)
|
||||
|
||||
async function getFoundFile(data: EhrFile[]) {
|
||||
foundFile.value = data
|
||||
}
|
||||
|
||||
return {
|
||||
isSearch,
|
||||
foundFile,
|
||||
getFoundFile,
|
||||
}
|
||||
})
|
||||
246
Services/client/src/stores/tree-data.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
import HttpService from '@/services/HttpService'
|
||||
|
||||
const axiosClient = HttpService.getAxiosClient()
|
||||
const apiEndpoint: string = import.meta.env.VITE_API_ENDPOINT
|
||||
|
||||
export interface EhrFolder {
|
||||
pathname: string
|
||||
name: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export interface EhrFile {
|
||||
pathname: string
|
||||
|
||||
fileName: string
|
||||
fileSize: string
|
||||
fileType: string
|
||||
|
||||
title: string
|
||||
description: string
|
||||
category: string[]
|
||||
keyword: string[]
|
||||
|
||||
updatedAt: string
|
||||
updatedBy: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export interface TreeDataFolder {
|
||||
pathname: string
|
||||
name: string
|
||||
status: boolean
|
||||
folder: TreeDataFolder[]
|
||||
file?: EhrFile[]
|
||||
}
|
||||
|
||||
export const useTreeDataStore = defineStore('changeCabinet', () => {
|
||||
const loader = useLoader()
|
||||
|
||||
const data = ref<TreeDataFolder[]>([])
|
||||
const currentFolder = ref<TreeDataFolder[]>([])
|
||||
const currentFile = ref<EhrFile[]>([])
|
||||
const currentPath = ref<string>('')
|
||||
const currentDept = ref<number>(0)
|
||||
|
||||
async function getCabinet() {
|
||||
const res = await axiosClient.get<EhrFolder[]>(`${apiEndpoint}cabinet`)
|
||||
|
||||
data.value = currentFolder.value = res.data.map((v) => ({
|
||||
...v,
|
||||
status: false,
|
||||
folder: [],
|
||||
}))
|
||||
}
|
||||
|
||||
async function getFolder(pathname: string, updateStatus = true) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
currentDept.value = pathArray.length
|
||||
|
||||
if (pathArray.length >= 4) {
|
||||
currentFolder.value = []
|
||||
|
||||
await getFile(pathname)
|
||||
|
||||
let current: (typeof data.value)[number] | undefined
|
||||
current = data.value.find((v) => v.name === pathArray[0])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[1])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[2])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[3])
|
||||
if (current && updateStatus) current.status = true
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}/drawer`
|
||||
if (pathArray.length >= 2) requestPath += `/${pathArray[1]}/folder`
|
||||
if (pathArray.length >= 3) requestPath += `/${pathArray[2]}/subfolder`
|
||||
|
||||
currentPath.value = pathname
|
||||
|
||||
const res = await axiosClient.get<EhrFolder[]>(
|
||||
`${apiEndpoint}${requestPath}`
|
||||
)
|
||||
|
||||
const list = res.data.map((v) => ({
|
||||
...v,
|
||||
status: false,
|
||||
folder: [],
|
||||
}))
|
||||
|
||||
let current: (typeof data.value)[number] | undefined
|
||||
|
||||
if (pathArray.length >= 1) {
|
||||
current = data.value.find((v) => v.name === pathArray[0])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
|
||||
if (pathArray.length >= 2) {
|
||||
current = current?.folder.find((v) => v.name === pathArray[1])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
if (pathArray.length >= 3) {
|
||||
current = current?.folder.find((v) => v.name === pathArray[2])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
|
||||
if (current) current.folder = list
|
||||
|
||||
currentFolder.value = list
|
||||
|
||||
await getFile(pathname)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function getFile(pathname: string) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathArray.length <= 2) {
|
||||
currentFile.value = []
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
requestPath += '/file'
|
||||
|
||||
const res = await axiosClient.get<EhrFile[]>(`${apiEndpoint}${requestPath}`)
|
||||
|
||||
currentFile.value = res.data
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function createFolder(name: string | undefined) {
|
||||
loader.show()
|
||||
|
||||
if (!name) return loader.hide()
|
||||
|
||||
const pathArray: string[] = currentPath.value.split('/').filter(Boolean)
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}/drawer`
|
||||
if (pathArray.length >= 2) requestPath += `/${pathArray[1]}/folder`
|
||||
if (pathArray.length >= 3) requestPath += `/${pathArray[2]}/subfolder`
|
||||
|
||||
await axiosClient.post(`${apiEndpoint}${requestPath}`, {
|
||||
name,
|
||||
})
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function editFolder(name: string | undefined, pathname: string) {
|
||||
loader.show()
|
||||
|
||||
if (!name) return loader.hide()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}`
|
||||
if (pathArray.length >= 2) requestPath += `/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
|
||||
await axiosClient.put(`${apiEndpoint}${requestPath}`, {
|
||||
name: name,
|
||||
})
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function deleteFolder(pathname: string) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}`
|
||||
if (pathArray.length >= 2) requestPath += `/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
|
||||
await axiosClient.delete(`${apiEndpoint}${requestPath}`)
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function gotoParent() {
|
||||
const pathname = currentPath.value.split('/').filter(Boolean)
|
||||
|
||||
pathname.pop()
|
||||
|
||||
if (pathname.length === 0) {
|
||||
const cabinet = currentFolder.value.find((v) => v.status === true)
|
||||
if (cabinet) {
|
||||
cabinet.status = false
|
||||
}
|
||||
}
|
||||
|
||||
await getFolder(pathname.join('/') + '/')
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
currentFolder,
|
||||
currentFile,
|
||||
currentDept,
|
||||
getCabinet,
|
||||
getFolder,
|
||||
gotoParent,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
editFolder,
|
||||
}
|
||||
})
|
||||
24
Services/client/src/style/quasar-variables.sass
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
$primary: #2196f3
|
||||
$secondary: #f7f9fa
|
||||
$accent: #9C27B0
|
||||
|
||||
$dark: #35473C
|
||||
|
||||
$positive: #21BA45
|
||||
$negative: #C10015
|
||||
$info: #31CCEC
|
||||
$warning: #F2C037
|
||||
|
||||
@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
|
||||
|
||||
body
|
||||
font-family: $noto-thai !important
|
||||
text-rendering: optimizeLegibility
|
||||
-webkit-font-smoothing: antialiased
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
|
||||
$separator-color: #EDEDED !default
|
||||
|
||||
$shadow-color: #c1c1c7 !default
|
||||
|
||||
27
Services/client/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>
|
||||
44
Services/client/src/views/MainLayout.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import { useLoader } from '@/stores/loader'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import profile from '@/components/Profile.vue'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { loader } = storeToRefs(loaderStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout view="hHh lpR fFf">
|
||||
<q-header class="bg-white text-black" bordered>
|
||||
<q-toolbar class="q-py-sm">
|
||||
<q-img
|
||||
src="@/assets/logo.png"
|
||||
spinner-color="white"
|
||||
style="height: 32px; max-width: 32px"
|
||||
/>
|
||||
<div class="column q-px-md" id="app-toolbar-title">
|
||||
<span class="text-body1">ระบบทรัพยากรบุคคล</span>
|
||||
<span class="text-caption text-grey">
|
||||
จัดเก็บข้อมูลผลการประเมิน
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-space></q-space>
|
||||
<profile v-if="$route.meta.statusAccount" />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<QPage>
|
||||
<router-view :key="$route.fullPath" />
|
||||
</QPage>
|
||||
</q-page-container>
|
||||
<full-loader :visibility="loader" />
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.q-layout {
|
||||
background: var(--q-secondary);
|
||||
}
|
||||
</style>
|
||||
13
Services/client/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["dom", "es2015", "es2018", "es2018.promise"]
|
||||
}
|
||||
}
|
||||
14
Services/client/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
Services/client/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
Services/client/tsconfig.vitest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
60
Services/client/vite.config.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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 EHR Portfolio',
|
||||
short_name: 'EHR Portfolio',
|
||||
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)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3010,
|
||||
},
|
||||
})
|
||||