Merge branch 'development'
This commit is contained in:
commit
31944ab7c8
55 changed files with 7423 additions and 1969 deletions
6
Services/.dockerignore
Normal file
6
Services/.dockerignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
32
Services/Dockerfile
Normal file
32
Services/Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# docker build -t docker.frappet.com/edm/core .
|
||||
FROM node:20-slim AS base
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as server
|
||||
COPY ./server .
|
||||
|
||||
FROM server AS deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM server AS build
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base as client
|
||||
COPY ./client .
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base as prod
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY --from=server /app/static /app/static
|
||||
COPY --from=client /app/dist /app/static
|
||||
|
||||
CMD ["node", "./dist/app.js"]
|
||||
194
Services/client/package-lock.json
generated
194
Services/client/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "ehr_portfolio",
|
||||
"version": "0.0.0",
|
||||
"name": "edm-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ehr_portfolio",
|
||||
"version": "0.0.0",
|
||||
"name": "edm-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
"@fullcalendar/list": "^6.1.8",
|
||||
"@fullcalendar/timegrid": "^6.1.8",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@johmun/vue-tags-input": "^2.1.0",
|
||||
"@mayank1513/vue-tag-input": "^1.2.0",
|
||||
"@quasar/extras": "^1.15.8",
|
||||
"@vuepic/vue-datepicker": "^5.2.1",
|
||||
"axios": "^1.6.2",
|
||||
|
|
@ -22,7 +24,8 @@
|
|||
"quasar": "^2.11.1",
|
||||
"vite-plugin-pwa": "^0.16.7",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/vite-plugin": "^1.6.0",
|
||||
|
|
@ -31,6 +34,7 @@
|
|||
"@types/node": "^18.11.12",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@voerro/vue-tagsinput": "^2.7.1",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.2.6",
|
||||
|
|
@ -44,6 +48,8 @@
|
|||
"prettier": "^2.7.1",
|
||||
"sass": "^1.32.12",
|
||||
"start-server-and-test": "^1.15.2",
|
||||
"typedoc": "^0.25.3",
|
||||
"typedoc-plugin-vue": "^1.1.0",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.6",
|
||||
|
|
@ -2237,6 +2243,36 @@
|
|||
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@johmun/vue-tags-input": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@johmun/vue-tags-input/-/vue-tags-input-2.1.0.tgz",
|
||||
"integrity": "sha512-Fdwfss/TqCqMJbGAkmlzKbcG/ia1MstYjhqPBj+zG7h/166tIcE1TIftUxhT9LZ+RWjRSG0EFA1UyaHQSr3k3Q==",
|
||||
"dependencies": {
|
||||
"vue": "^2.6.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@johmun/vue-tags-input/node_modules/@vue/compiler-sfc": {
|
||||
"version": "2.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.15.tgz",
|
||||
"integrity": "sha512-FCvIEevPmgCgqFBH7wD+3B97y7u7oj/Wr69zADBf403Tui377bThTjBvekaZvlRr4IwUAu3M6hYZeULZFJbdYg==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.18.4",
|
||||
"postcss": "^8.4.14",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@johmun/vue-tags-input/node_modules/vue": {
|
||||
"version": "2.7.15",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.15.tgz",
|
||||
"integrity": "sha512-a29fsXd2G0KMRqIFTpRgpSbWaNBK3lpCTOLuGLEDnlHWdjB8fwl6zyYZ8xCrqkJdatwZb4mGHiEfJjnw0Q6AwQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "2.7.15",
|
||||
"csstype": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
|
|
@ -2289,6 +2325,18 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mayank1513/vue-tag-input": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mayank1513/vue-tag-input/-/vue-tag-input-1.2.0.tgz",
|
||||
"integrity": "sha512-COWPFJ4MsPVy84Z+RIC2nzu188AwkJZJQ021Jz04uLfgxbmLrCXhrNfCIrjMD7bb6/Fb2kgybQILIiPqcNJc9w==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mayank1513"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -2826,6 +2874,12 @@
|
|||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@voerro/vue-tagsinput": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@voerro/vue-tagsinput/-/vue-tagsinput-2.7.1.tgz",
|
||||
"integrity": "sha512-G7ClILHgtc/D1vEw9qp6dlsAstlhwLDkPvEp6w1iY/dmSzu4hhxaMk8jyahjxQRDgiLlrAikESOsfkVhy18YwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "1.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.10.tgz",
|
||||
|
|
@ -3246,6 +3300,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-sequence-parser": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
|
||||
"integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
|
|
@ -3873,6 +3933,14 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/click-outside-vue3": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/click-outside-vue3/-/click-outside-vue3-4.0.1.tgz",
|
||||
"integrity": "sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
|
@ -6667,6 +6735,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
|
|
@ -7039,6 +7113,12 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
|
|
@ -7056,6 +7136,18 @@
|
|||
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/memorystream": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||
|
|
@ -8337,6 +8429,18 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz",
|
||||
"integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-sequence-parser": "^1.1.0",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"vscode-oniguruma": "^1.7.0",
|
||||
"vscode-textmate": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
|
|
@ -9096,6 +9200,60 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz",
|
||||
"integrity": "sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.3.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"shiki": "^0.14.1"
|
||||
},
|
||||
"bin": {
|
||||
"typedoc": "bin/typedoc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc-plugin-vue": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typedoc-plugin-vue/-/typedoc-plugin-vue-1.1.0.tgz",
|
||||
"integrity": "sha512-zbc2jFH3K2e/rRerV8crWY16M2MfZAze5bV6s2Ti0c8De+MjBn7K0EuK3eGoJXkY1n22flYgBz65j0pYu+ffmA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"typedoc": "0.25.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
|
|
@ -9430,6 +9588,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-oniguruma": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
|
||||
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vscode-textmate": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
|
||||
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz",
|
||||
|
|
@ -9612,6 +9782,20 @@
|
|||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue3-tags-input": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz",
|
||||
"integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==",
|
||||
"dependencies": {
|
||||
"click-outside-vue3": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "edm-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
|
|
@ -17,47 +18,42 @@
|
|||
"docs:typedoc": "typedoc && scp -r fe-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r fe-typedoc"
|
||||
},
|
||||
"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",
|
||||
"@quasar/extras": "^1.16.8",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"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"
|
||||
"keycloak-js": "^23.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"quasar": "^2.14.0",
|
||||
"vite-plugin-pwa": "^0.17.2",
|
||||
"vue": "^3.3.9",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mayank1513/vue-tag-input": "^1.2.0",
|
||||
"@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",
|
||||
"@rushstack/eslint-patch": "^1.6.0",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^20.10.0",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.2",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^13.6.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"jsdom": "^23.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.32.12",
|
||||
"start-server-and-test": "^1.15.2",
|
||||
"typedoc": "^0.25.3",
|
||||
"prettier": "^3.1.0",
|
||||
"sass": "^1.69.5",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-vue": "^1.1.0",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.6",
|
||||
"vue-tsc": "^1.0.12"
|
||||
"typescript": "~5.3.2",
|
||||
"vite": "^5.0.2",
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1986
Services/client/pnpm-lock.yaml
generated
1986
Services/client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"realm": "vue",
|
||||
"realm": "EDM",
|
||||
"auth-server-url": "https://edm-id.frappet.synology.me/",
|
||||
"ssl-required": "external",
|
||||
"resource": "vuejs",
|
||||
"resource": "EDM-V1",
|
||||
"public-client": true,
|
||||
"confidential-port": 0
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div id="azay-admin-app">
|
||||
<div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition>
|
||||
<component :is="Component" />
|
||||
|
|
@ -9,5 +7,3 @@
|
|||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
52
Services/client/src/components/DialogDelete.vue
Normal file
52
Services/client/src/components/DialogDelete.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
defineEmits(['confirm', 'cancel', 'update:open'])
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
persistent
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
:model-value="props.open"
|
||||
@update:model-value="(v) => $emit('update:open', v)"
|
||||
>
|
||||
<q-card style="width: 400px">
|
||||
<q-card-section>
|
||||
<span class="text-h6">
|
||||
<q-icon name="error" color="negative" size="2.5rem" />แจ้งเตือนการลบ
|
||||
</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
ถ้าดำเนินการต่อจะทำการลบ
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="bg-white text-primary">
|
||||
<q-space />
|
||||
<q-btn
|
||||
label="ยกเลิก"
|
||||
flat
|
||||
v-close-popup
|
||||
@click="() => ($emit('update:open', !open))"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
v-close-popup
|
||||
label="ลบ"
|
||||
class="text-red"
|
||||
@click="() => $emit('confirm')"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
181
Services/client/src/components/FileForm.vue
Normal file
181
Services/client/src/components/FileForm.vue
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
error: { fileExist?: boolean }
|
||||
mode: 'create' | 'edit'
|
||||
title?: string
|
||||
description?: string
|
||||
keyword?: string
|
||||
category?: string
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:open',
|
||||
'update:title',
|
||||
'update:description',
|
||||
'update:keyword',
|
||||
'update:category',
|
||||
'filechange',
|
||||
'submit',
|
||||
])
|
||||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.open === true) {
|
||||
emit('update:open', false)
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
file.value = undefined
|
||||
emit('update:title', '')
|
||||
emit('update:description', '')
|
||||
emit('update:keyword', '')
|
||||
emit('update:category', '')
|
||||
}
|
||||
|
||||
function submit() {
|
||||
emit('submit', {
|
||||
mode: props.mode,
|
||||
file: file.value,
|
||||
title: props.title ?? '',
|
||||
description: props.description ?? '',
|
||||
keyword: props.keyword ?? '',
|
||||
category: props.category ?? '',
|
||||
})
|
||||
reset()
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', keydown))
|
||||
onUnmounted(() => window.addEventListener('keydown', keydown))
|
||||
|
||||
const file = ref<File | undefined>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer
|
||||
overlay
|
||||
bordered
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
tabindex="0"
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
:model-value="open"
|
||||
@update:model-value="(v) => $emit('update:open', v)"
|
||||
>
|
||||
<q-form @submit.prevent="submit">
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold" v-if="mode === 'create'">
|
||||
สร้างเอกสาร
|
||||
</span>
|
||||
<span class="text-weight-bold" v-if="mode === 'edit'">
|
||||
แก้ไขเอกสาร
|
||||
</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
color="red"
|
||||
@click="() => ($emit('update:open', !open), reset())"
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold q-mb-sm block">อัพโหลดไฟล์</span>
|
||||
<q-file
|
||||
dense
|
||||
outlined
|
||||
v-model="file"
|
||||
@update:model-value="(v) => $emit('filechange', v.name)"
|
||||
:label="file?.name ? undefined : 'เลือกไฟล์'"
|
||||
:error="!!error.fileExist"
|
||||
:error-message="
|
||||
error.fileExist ? 'พบไฟล์ในระบบ ข้อมูลในระบบจะถูกเขียนทับ' : ''
|
||||
"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">ชื่อเรื่อง</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-my-sm"
|
||||
placeholder="กรอกชื่อเรื่อง"
|
||||
:model-value="title"
|
||||
@update:model-value="(v) => $emit('update:title', v)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">รายละเอียดของเอกสาร</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-mt-sm no-resize"
|
||||
type="textarea"
|
||||
placeholder="กรอกรายละเอียด"
|
||||
:model-value="description"
|
||||
@update:model-value="(v) => $emit('update:description', v)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">กลุ่ม/หมวดหมู่</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-mt-sm"
|
||||
placeholder="เลือกกลุ่ม/หมวดหมู่"
|
||||
:model-value="category"
|
||||
@update:model-value="(v) => $emit('update:category', v)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-mt-sm"
|
||||
placeholder="คำสำคัญ"
|
||||
:model-value="keyword"
|
||||
@update:model-value="(v) => $emit('update:keyword', v)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section :style="{ display: 'flex', gap: '.5rem' }">
|
||||
<q-btn label="บันทึก" type="submit" color="primary" />
|
||||
<q-btn
|
||||
label="ยกเลิก"
|
||||
type="reset"
|
||||
color="primary"
|
||||
flat
|
||||
@click="() => ($emit('update:open', false), reset())"
|
||||
/>
|
||||
</section>
|
||||
</q-form>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-resize :deep(textarea) {
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,33 +3,34 @@ import { useFileInfoStore } from '@/stores/file-info-data'
|
|||
|
||||
const { mimeFileMapping } = useFileInfoStore()
|
||||
|
||||
defineProps<{ fileMimeType: any; size: string }>()
|
||||
defineProps<{ fileMimeType: string | undefined; size: string }>()
|
||||
|
||||
function getIcon(mimeType: string) {
|
||||
if (mimeFileMapping.hasOwnProperty(mimeType)) {
|
||||
return mimeFileMapping[mimeType].icon
|
||||
} else {
|
||||
return 'mdi-file-question-outline'
|
||||
}
|
||||
return mimeType && mimeFileMapping.hasOwnProperty(mimeType)
|
||||
? mimeFileMapping[mimeType].icon
|
||||
: 'mdi-file-question-outline'
|
||||
}
|
||||
function getColor(mimeType: string) {
|
||||
if (mimeFileMapping.hasOwnProperty(mimeType)) {
|
||||
return mimeFileMapping[mimeType].color
|
||||
} else {
|
||||
return 'blue-11'
|
||||
}
|
||||
return mimeType && mimeFileMapping.hasOwnProperty(mimeType)
|
||||
? mimeFileMapping[mimeType].color
|
||||
: 'blue-11'
|
||||
}
|
||||
function getSize(s: string) {
|
||||
if (s === 'preview') {
|
||||
return '6em'
|
||||
function getIconSize(s: string) {
|
||||
type SizeMapping = {
|
||||
[key: string]: string
|
||||
}
|
||||
const sizeMapping: SizeMapping = {
|
||||
preview: '6em',
|
||||
list: '2em',
|
||||
}
|
||||
return sizeMapping[s]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-icon
|
||||
:name="getIcon(fileMimeType)"
|
||||
:color="getColor(fileMimeType)"
|
||||
:size="getSize(size)"
|
||||
:name="fileMimeType && getIcon(fileMimeType)"
|
||||
:color="fileMimeType && getColor(fileMimeType)"
|
||||
:size="getIconSize(size)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,333 +2,404 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
import FileItemAction from '@/components/FileItemAction.vue'
|
||||
import DialogDelete from '@/components/DialogDelete.vue'
|
||||
import FileForm from './FileForm.vue'
|
||||
import FolderForm from './FolderForm.vue'
|
||||
import UploadExistDialog from './UploadExistDialog.vue'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
|
||||
const { isPreview } = storeToRefs(useFileInfoStore())
|
||||
const { getFileInfo } = useFileInfoStore()
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
|
||||
|
||||
const { currentFolder, currentFile, currentDept } = storeToRefs(
|
||||
useTreeDataStore()
|
||||
)
|
||||
const { getFolder, createFolder, editFolder } = useTreeDataStore()
|
||||
const drawer = ref<boolean>(false)
|
||||
const drawerFile = ref<boolean>(false)
|
||||
const drawerStatus = ref<'edit' | 'create'>('create')
|
||||
const input = ref<string>('')
|
||||
const inputFile = ref<File>()
|
||||
const fileTitle = ref<string>('')
|
||||
const fileDesc = ref<string>('')
|
||||
const fileCategory = ref<string>('')
|
||||
const optionsCategory = [
|
||||
{ label: 'ศิลปะ', value: 'art' },
|
||||
{ label: 'ภาพวาด', value: 'drawing' },
|
||||
{ label: 'ภาษาไทย', value: 'thai' },
|
||||
]
|
||||
const fileKeyword = 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,
|
||||
}
|
||||
},
|
||||
)
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
|
||||
const { getFileInfo, getFileNameFormat } = useFileInfoStore()
|
||||
const { currentFolder, currentFile, currentDept, currentPath } =
|
||||
storeToRefs(useTreeDataStore())
|
||||
const {
|
||||
createFolder,
|
||||
editFolder,
|
||||
getFolder,
|
||||
deleteFolder,
|
||||
uploadFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
checkFile,
|
||||
} = useTreeDataStore()
|
||||
|
||||
const currentIcon = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'mdi-file-cabinet'
|
||||
: currentDept.value === 1
|
||||
? 'inbox'
|
||||
: 'o_folder_open',
|
||||
)
|
||||
|
||||
const dialogDeleteState = ref<boolean>(false)
|
||||
const deleteFormPath = ref<string>('')
|
||||
const deleteFormType = ref<'deleteFolder' | 'deleteFile'>()
|
||||
|
||||
const folderFormState = ref<boolean>(false)
|
||||
const folderFormPath = ref<string>('')
|
||||
const folderFormData = ref<{
|
||||
name?: string
|
||||
}>({})
|
||||
const folderFormType = ref<'edit' | 'create'>('create')
|
||||
const fileFormState = ref<boolean>(false)
|
||||
const fileFormPath = ref<string>('')
|
||||
const fileFormData = ref<{
|
||||
file?: File
|
||||
title?: string
|
||||
description?: string
|
||||
keyword?: string
|
||||
category?: string
|
||||
}>({})
|
||||
const fileFormType = ref<'edit' | 'create'>('create')
|
||||
const fileFormError = ref<{ fileExist?: boolean }>({})
|
||||
const fileExistNotification = ref<boolean>(false)
|
||||
|
||||
function triggerFolderDelete(pathname: string) {
|
||||
deleteFormType.value = 'deleteFolder'
|
||||
deleteFormPath.value = pathname
|
||||
dialogDeleteState.value = !dialogDeleteState.value
|
||||
}
|
||||
|
||||
function triggerFileDelete(pathname: string) {
|
||||
deleteFormType.value = 'deleteFile'
|
||||
deleteFormPath.value = pathname
|
||||
dialogDeleteState.value = !dialogDeleteState.value
|
||||
}
|
||||
|
||||
function triggerFolderCreate() {
|
||||
folderFormType.value = 'create'
|
||||
folderFormData.value = {}
|
||||
folderFormState.value = !folderFormState.value
|
||||
}
|
||||
|
||||
function triggerFolderEdit(name: string, pathname: string) {
|
||||
folderFormType.value = 'edit'
|
||||
folderFormPath.value = pathname
|
||||
folderFormData.value.name = name
|
||||
folderFormState.value = true
|
||||
}
|
||||
|
||||
async function submitFolderForm(value: {
|
||||
mode: 'create' | 'edit'
|
||||
name: string
|
||||
}) {
|
||||
if (value.mode === 'create') {
|
||||
await createFolder(value.name)
|
||||
} else {
|
||||
await editFolder(value.name, folderFormPath.value)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFileCreate() {
|
||||
fileFormType.value = 'create'
|
||||
fileFormData.value = {}
|
||||
fileFormState.value = !fileFormState.value
|
||||
}
|
||||
|
||||
function triggerFileEdit(
|
||||
value: {
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
},
|
||||
pathname: string,
|
||||
) {
|
||||
fileFormState.value = true
|
||||
fileFormType.value = 'edit'
|
||||
fileFormPath.value = pathname
|
||||
fileFormData.value = {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.keyword,
|
||||
}
|
||||
}
|
||||
|
||||
const currentParam = ref<Parameters<typeof submitFileForm>[0]>()
|
||||
|
||||
async function submitFileForm(
|
||||
value: {
|
||||
mode: 'create' | 'edit'
|
||||
file?: File
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
},
|
||||
force = false,
|
||||
) {
|
||||
currentParam.value = value
|
||||
|
||||
if (value.file && checkFile(value.file.name) && !force) {
|
||||
fileExistNotification.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (value.mode === 'create' && value.file) {
|
||||
await uploadFile(currentPath.value, value.file, {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
})
|
||||
} else {
|
||||
await updateFile(
|
||||
fileFormPath.value,
|
||||
{
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
},
|
||||
value.file,
|
||||
)
|
||||
}
|
||||
fileFormData.value = {}
|
||||
fileFormState.value = false
|
||||
currentParam.value = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div class="text-h6 q-mt-md" v-if="currentDept > 2">
|
||||
{{ DEPT_NAME[currentDept] }}
|
||||
</div>
|
||||
<div class="grid q-mt-md">
|
||||
<div v-for="value in currentFolder">
|
||||
<div
|
||||
:key="value.name"
|
||||
v-for="value in currentFolder"
|
||||
class="inline-block"
|
||||
:style="{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: currentDept > 2 ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
padding: currentDept > 2 ? '.5rem 0' : '.5rem',
|
||||
}"
|
||||
class="box"
|
||||
@click="() => getFolder(value.pathname)"
|
||||
>
|
||||
<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 class="q-px-md flex items-center justify-center">
|
||||
<q-icon
|
||||
:name="currentIcon"
|
||||
:size="currentDept > 2 ? '3em' : '6em'"
|
||||
color="primary"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute flex items-center justify-center"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
:style="{ bottom: currentDept > 2 ? '0.5rem' : 'unset' }"
|
||||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
@delete="() => triggerFolderDelete(value.pathname)"
|
||||
@edit="() => triggerFolderEdit(value.name, value.pathname)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block text-center"
|
||||
:class="{
|
||||
'q-px-md': currentDept < 3,
|
||||
'q-pr-xl': currentDept > 2,
|
||||
}"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
{{ value.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.action && currentDept < 4">
|
||||
<div
|
||||
class="inline-block"
|
||||
v-if="props.action && currentDept < 4"
|
||||
tabindex="0"
|
||||
@keydown.esc="() => (drawer = false)"
|
||||
class="dashed"
|
||||
:style="{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: currentDept > 2 ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
padding: currentDept > 2 ? '.5rem 0' : '.5rem',
|
||||
}"
|
||||
@click="() => triggerFolderCreate()"
|
||||
>
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card
|
||||
flat
|
||||
@click="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus = 'create'
|
||||
drawerFile = false
|
||||
}
|
||||
"
|
||||
<div
|
||||
class="q-px-md flex items-center justify-center"
|
||||
style="position: relative"
|
||||
>
|
||||
<q-icon
|
||||
:name="currentIcon"
|
||||
:size="currentDept > 2 ? '3em' : '6em'"
|
||||
color="primary"
|
||||
class="col"
|
||||
/>
|
||||
<q-btn
|
||||
round
|
||||
:dense="currentDept > 2"
|
||||
color="white"
|
||||
size="10px"
|
||||
style="position: absolute; bottom: 0"
|
||||
:style="{ right: currentDept > 2 ? '.5rem' : '1.75rem' }"
|
||||
>
|
||||
<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>
|
||||
<q-icon name="add" color="primary" size="1.5rem" />
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block text-center"
|
||||
:class="{
|
||||
'q-px-md': currentDept < 3,
|
||||
'q-pr-xl': currentDept > 2,
|
||||
}"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
สร้าง{{ DEPT_NAME[currentDept] }}ใหม่
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div
|
||||
style="grid-column: 1 / span 5"
|
||||
class="text-h6 q-mt-md"
|
||||
v-if="currentDept > 2"
|
||||
>
|
||||
เอกสาร
|
||||
</div>
|
||||
<div class="grid q-mt-md">
|
||||
<div v-for="value in currentFile">
|
||||
<div
|
||||
v-for="(value, index) in currentFile"
|
||||
:key="value.title"
|
||||
class="inline-block"
|
||||
:style="{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '1rem',
|
||||
maxWidth: '100%',
|
||||
}"
|
||||
class="box"
|
||||
@click="() => getFileInfo(value)"
|
||||
>
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card
|
||||
flat
|
||||
@click="
|
||||
() => {
|
||||
getFileInfo(currentFile[index])
|
||||
isPreview = true
|
||||
}
|
||||
<div class="q-px-md flex items-center justify-center">
|
||||
<file-icon
|
||||
size="preview"
|
||||
:fileMimeType="value.fileType ? value.fileType : 'unknow'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
@edit="
|
||||
() =>
|
||||
triggerFileEdit(
|
||||
{
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword.join(','),
|
||||
category: value.category.join(','),
|
||||
},
|
||||
value.pathname,
|
||||
)
|
||||
"
|
||||
>
|
||||
<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>
|
||||
@delete="() => triggerFileDelete(value.pathname)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block q-px-md text-center"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
{{ getFileNameFormat(value.fileName) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-block" v-if="props.action && currentDept > 2">
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card
|
||||
flat
|
||||
@click="
|
||||
() => {
|
||||
drawerFile = !drawerFile
|
||||
drawer = false
|
||||
}
|
||||
"
|
||||
</div>
|
||||
<div v-if="props.action && currentDept > 2">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '1rem',
|
||||
maxWidth: '100%',
|
||||
}"
|
||||
class="dashed"
|
||||
@click="() => triggerFileCreate()"
|
||||
>
|
||||
<div
|
||||
class="q-px-md flex items-center justify-center"
|
||||
style="position: relative"
|
||||
>
|
||||
<q-icon name="mdi-file" class="add-icon" size="6em" color="primary" />
|
||||
<q-btn
|
||||
round
|
||||
color="white"
|
||||
size="10px"
|
||||
style="position: absolute; right: 1.75rem; bottom: 0"
|
||||
>
|
||||
<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>
|
||||
<q-icon name="add" color="primary" size="1.5rem" />
|
||||
</q-btn>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block q-px-md text-center"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
สร้างไฟล์ใหม่
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-drawer
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
v-model="drawer"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
behavior="mobile"
|
||||
tabindex="0"
|
||||
@keydown.esc="
|
||||
() => {
|
||||
drawer = false
|
||||
input = ''
|
||||
}
|
||||
<file-form
|
||||
:mode="fileFormType"
|
||||
:error="fileFormError"
|
||||
v-model:open="fileFormState"
|
||||
v-model:title="fileFormData.title"
|
||||
v-model:description="fileFormData.description"
|
||||
v-model:keyword="fileFormData.keyword"
|
||||
v-model:category="fileFormData.category"
|
||||
@filechange="(name: string) => (fileFormError.fileExist = checkFile(name))"
|
||||
@submit="submitFileForm"
|
||||
/>
|
||||
|
||||
<folder-form
|
||||
:mode="folderFormType"
|
||||
:tree="DEPT_NAME[currentDept]"
|
||||
v-if="currentDept < 4"
|
||||
v-model:open="folderFormState"
|
||||
v-model:name="folderFormData.name"
|
||||
@submit="submitFolderForm"
|
||||
/>
|
||||
|
||||
<upload-exist-dialog
|
||||
v-model:notification="fileExistNotification"
|
||||
@confirm="() => currentParam && submitFileForm(currentParam, true)"
|
||||
@cancel="() => (currentParam = undefined)"
|
||||
/>
|
||||
|
||||
<dialog-delete
|
||||
v-model:open="dialogDeleteState"
|
||||
@confirm="
|
||||
() =>
|
||||
deleteFormType &&
|
||||
{ deleteFolder, deleteFile }[deleteFormType](deleteFormPath)
|
||||
"
|
||||
@keyup.enter="
|
||||
() => {
|
||||
drawer = false
|
||||
drawerStatus === 'create'
|
||||
? createFolder(input)
|
||||
: editFolder(input, editPathname)
|
||||
input = ''
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none" @keydown.esc="() => (drawer = false)">
|
||||
<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>
|
||||
<q-drawer
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
v-model="drawerFile"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold">สร้างเอกสาร</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
color="red"
|
||||
@click="() => (drawerFile = !drawerFile)"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<span class="text-weight-bold">อัพโหลดไฟล์</span>
|
||||
<q-input
|
||||
model-value="inputfile"
|
||||
class="q-my-md"
|
||||
outlined
|
||||
type="file"
|
||||
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
|
||||
dense
|
||||
/>
|
||||
<span class="text-weight-bold">ชื่อเรื่อง</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="fileTitle"
|
||||
placeholder="กรอกชื่อเรื่อง"
|
||||
dense
|
||||
/>
|
||||
<span class="text-weight-bold">รายละเอียดของเอกสาร</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
type="textarea"
|
||||
v-model="fileDesc"
|
||||
placeholder="กรอกรายละเอียด"
|
||||
dense
|
||||
/>
|
||||
<span class="text-weight-bold">กลุ่ม/หมวดหมู่</span>
|
||||
<q-select
|
||||
class="q-my-md"
|
||||
outlined
|
||||
:options="optionsCategory"
|
||||
v-model="fileCategory"
|
||||
placeholder="เลือกกลุ่ม/หมวดหมู่"
|
||||
dense
|
||||
/>
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="fileKeyword"
|
||||
placeholder="กรอกคำสำคัญ"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
drawerFile = !drawerFile
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-drawer>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
display: inline-block;
|
||||
border: 2px solid #f1f2f4;
|
||||
border: 2px solid $separator-color;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -336,7 +407,7 @@ const props = withDefaults(
|
|||
.dashed {
|
||||
opacity: 0.4;
|
||||
display: inline-block;
|
||||
border: 2px solid #babdc3;
|
||||
border: 2px solid #6985c2;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border-style: dashed;
|
||||
|
|
@ -348,8 +419,38 @@ const props = withDefaults(
|
|||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
right: 45px;
|
||||
top: 50%;
|
||||
right: 40%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.add-button-folder-level {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
top: 22px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.text-overflow-handle {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-md-min) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.grid .box {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,5 @@
|
|||
<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>()
|
||||
defineEmits(['edit', 'delete'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -23,9 +7,7 @@ const editInput = ref<string>()
|
|||
<q-menu auto-close>
|
||||
<q-list dense>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => (
|
||||
$emit('editname')
|
||||
)">
|
||||
<q-item-section @click.prevent.stop="() => $emit('edit')">
|
||||
<div class="row items-center">
|
||||
<q-icon name="edit" color="positive" />
|
||||
<span class="q-ml-sm">แก้ไข</span>
|
||||
|
|
@ -33,7 +15,7 @@ const editInput = ref<string>()
|
|||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => deleteFolder(pathname)">
|
||||
<q-item-section @click.prevent.stop="() => $emit('delete')">
|
||||
<div class="row items-center">
|
||||
<q-icon name="delete" color="negative" />
|
||||
<span class="q-ml-sm">ลบ</span>
|
||||
|
|
@ -43,50 +25,4 @@ const editInput = ref<string>()
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
import { storeToRefs } from 'pinia'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
|
||||
const { foundFile } = storeToRefs(useSearchDataStore())
|
||||
const { isPreview } = storeToRefs(useFileInfoStore())
|
||||
const { getFileInfo } = useFileInfoStore()
|
||||
</script>
|
||||
|
||||
|
|
@ -22,12 +22,15 @@ const { getFileInfo } = useFileInfoStore()
|
|||
@click="
|
||||
() => {
|
||||
getFileInfo(foundFile[index])
|
||||
isPreview = true
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon name="description" size="6em" color="primary" />
|
||||
<file-icon
|
||||
size="preview"
|
||||
:fileMimeType="value.fileType"
|
||||
ref="fileIconComp"
|
||||
/>
|
||||
<span class="text-center q-pt-md">{{ value.title }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
|
|||
103
Services/client/src/components/FolderForm.vue
Normal file
103
Services/client/src/components/FolderForm.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
mode: 'create' | 'edit'
|
||||
tree: 'ตู้เอกสาร' | 'ลิ้นชัก' | 'แฟ้ม' | 'แฟ้มย่อย'
|
||||
name?: string
|
||||
}>(),
|
||||
{
|
||||
open: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits(['update:open', 'update:name', 'submit'])
|
||||
|
||||
function reset() {
|
||||
emit('update:name', undefined)
|
||||
}
|
||||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.open === true) {
|
||||
emit('update:open', false)
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
emit('submit', {
|
||||
mode: props.mode,
|
||||
name: props.name,
|
||||
})
|
||||
reset()
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', keydown))
|
||||
onUnmounted(() => window.addEventListener('keydown', keydown))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer
|
||||
overlay
|
||||
bordered
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
tabindex="0"
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
:model-value="open"
|
||||
>
|
||||
<q-form @submit.prevent="submit">
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold" v-if="mode === 'create'">
|
||||
สร้าง{{ tree }}
|
||||
</span>
|
||||
<span class="text-weight-bold" v-if="mode === 'edit'">
|
||||
แก้ไข{{ tree }}
|
||||
</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
color="red"
|
||||
@click="() => $emit('update:open', !open)"
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
<section class="q-mb-md">
|
||||
<span class="text-weight-bold">ชื่อ{{ tree }}</span>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
class="q-my-sm"
|
||||
placeholder="กรอกชื่อ"
|
||||
:model-value="name"
|
||||
@update:model-value="(v) => $emit('update:name', v)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section :style="{ display: 'flex', gap: '.5rem' }">
|
||||
<q-btn label="บันทึก" type="submit" color="primary" />
|
||||
<q-btn
|
||||
label="ยกเลิก"
|
||||
type="reset"
|
||||
color="primary"
|
||||
flat
|
||||
@click="() => ($emit('update:open', false), reset())"
|
||||
/>
|
||||
</section>
|
||||
</q-form>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-resize :deep(textarea) {
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
39
Services/client/src/components/GlobalErrorDialog.vue
Normal file
39
Services/client/src/components/GlobalErrorDialog.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useErrorStore } from '@/stores/error'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
|
||||
const { visible, title, msg } = storeToRefs(useErrorStore())
|
||||
const loader = useLoader()
|
||||
|
||||
watch(visible, () => {
|
||||
loader.hide()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog transition-show="scale" transition-hide="scale" v-model="visible">
|
||||
<q-card style="width: 400px">
|
||||
<q-card-section>
|
||||
<span class="text-h6">
|
||||
<q-icon name="error" color="negative" size="2.5rem" />{{
|
||||
title
|
||||
}}</span
|
||||
>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">{{ msg }}</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="bg-white text-primary">
|
||||
<q-space />
|
||||
<q-btn
|
||||
flat
|
||||
v-close-popup
|
||||
label="ปิด"
|
||||
@click="() => (visible = !visible)"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
466
Services/client/src/components/ListView.vue
Normal file
466
Services/client/src/components/ListView.vue
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { QTableProps } from 'quasar'
|
||||
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
import DialogDelete from '@/components/DialogDelete.vue'
|
||||
import FileForm from './FileForm.vue'
|
||||
import FolderForm from './FolderForm.vue'
|
||||
|
||||
const { getFormatDate, getSize, getType, getFileInfo } = useFileInfoStore()
|
||||
const { listDataFile, listDataFolder, currentDept, currentPath } = storeToRefs(
|
||||
useTreeDataStore()
|
||||
)
|
||||
const {
|
||||
createFolder,
|
||||
editFolder,
|
||||
getFolder,
|
||||
deleteFolder,
|
||||
uploadFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
checkFile,
|
||||
} = useTreeDataStore()
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
||||
const currentLevel = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'ตู้จัดเก็บเอกสาร'
|
||||
: currentDept.value === 1
|
||||
? 'ลิ้นชัก'
|
||||
: currentDept.value === 2
|
||||
? 'แฟ้ม'
|
||||
: 'แฟ้มย่อย'
|
||||
)
|
||||
|
||||
const currentIcon = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'mdi-file-cabinet'
|
||||
: currentDept.value === 1
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
)
|
||||
|
||||
const folderFormState = ref<boolean>(false)
|
||||
const folderFormPath = ref<string>('')
|
||||
const folderFormData = ref<{
|
||||
name?: string
|
||||
}>({})
|
||||
const folderFormType = ref<'edit' | 'create'>('create')
|
||||
const fileFormError = ref<{ fileExist?: boolean }>({})
|
||||
const fileFormState = ref<boolean>(false)
|
||||
const fileFormPath = ref<string>('')
|
||||
const fileFormData = ref<{
|
||||
file?: File
|
||||
title?: string
|
||||
description?: string
|
||||
keyword?: string
|
||||
category?: string
|
||||
}>({})
|
||||
const fileFormType = ref<'edit' | 'create'>('create')
|
||||
|
||||
const dialogDeleteState = ref<boolean>(false)
|
||||
const deleteFormPath = ref<string>('')
|
||||
const deleteFormType = ref<'deleteFolder' | 'deleteFile'>()
|
||||
|
||||
function triggerFolderDelete(pathname: string) {
|
||||
deleteFormType.value = 'deleteFolder'
|
||||
deleteFormPath.value = pathname
|
||||
dialogDeleteState.value = !dialogDeleteState.value
|
||||
}
|
||||
|
||||
function triggerFileDelete(pathname: string) {
|
||||
deleteFormType.value = 'deleteFile'
|
||||
deleteFormPath.value = pathname
|
||||
dialogDeleteState.value = !dialogDeleteState.value
|
||||
}
|
||||
|
||||
function triggerFolderCreate() {
|
||||
folderFormType.value = 'create'
|
||||
folderFormData.value = {}
|
||||
folderFormState.value = !folderFormState.value
|
||||
}
|
||||
|
||||
function triggerFolderEdit(name: string, pathname: string) {
|
||||
folderFormType.value = 'edit'
|
||||
folderFormPath.value = pathname
|
||||
folderFormData.value.name = name
|
||||
folderFormState.value = true
|
||||
}
|
||||
|
||||
async function submitFolderForm(value: {
|
||||
mode: 'create' | 'edit'
|
||||
name: string
|
||||
}) {
|
||||
if (value.mode === 'create') {
|
||||
await createFolder(value.name)
|
||||
} else {
|
||||
await editFolder(value.name, folderFormPath.value)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFileCreate() {
|
||||
fileFormType.value = 'create'
|
||||
fileFormData.value = {}
|
||||
fileFormState.value = !fileFormState.value
|
||||
}
|
||||
|
||||
function triggerFileEdit(
|
||||
value: {
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
},
|
||||
pathname: string
|
||||
) {
|
||||
fileFormState.value = true
|
||||
fileFormType.value = 'edit'
|
||||
fileFormPath.value = pathname
|
||||
fileFormData.value = {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.keyword,
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFileForm(value: {
|
||||
mode: 'create' | 'edit'
|
||||
file: File
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
}) {
|
||||
if (value.mode === 'create') {
|
||||
await uploadFile(currentPath.value, value.file, {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
})
|
||||
} else {
|
||||
await updateFile(
|
||||
fileFormPath.value,
|
||||
{
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
},
|
||||
value.file
|
||||
)
|
||||
}
|
||||
fileFormData.value = {}
|
||||
fileFormState.value = false
|
||||
}
|
||||
|
||||
const columnsFolder: QTableProps['columns'] = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
label: 'ชื่อ',
|
||||
align: 'left',
|
||||
field: (row) => row.name,
|
||||
format: (val) => `${val}`,
|
||||
sortable: true,
|
||||
style: 'width: 1000px',
|
||||
},
|
||||
{
|
||||
name: 'createdBy',
|
||||
align: 'center',
|
||||
label: 'สร้างโดย',
|
||||
field: 'createdBy',
|
||||
style: 'width: 20px',
|
||||
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
align: 'center',
|
||||
label: 'วันที่สร้าง',
|
||||
field: 'createdAt',
|
||||
sortable: true,
|
||||
style: 'width: 20px',
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
align: 'center',
|
||||
label: '',
|
||||
field: '',
|
||||
style: 'width: 5px',
|
||||
},
|
||||
]
|
||||
|
||||
const columnsFile: QTableProps['columns'] = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
label: 'ชื่อไฟล์',
|
||||
align: 'left',
|
||||
field: (row) => row.fileName,
|
||||
format: (val) => `${val}`,
|
||||
sortable: true,
|
||||
style: 'width: 200px',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'center',
|
||||
label: 'ชื่อเรื่อง',
|
||||
field: 'title',
|
||||
style: 'width: 200px',
|
||||
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'fileType',
|
||||
align: 'center',
|
||||
label: 'ประเภทของไฟล์',
|
||||
field: 'fileType',
|
||||
sortable: true,
|
||||
style: 'width: 200px',
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
align: 'center',
|
||||
label: '',
|
||||
field: '',
|
||||
style: 'width: 20px',
|
||||
},
|
||||
]
|
||||
|
||||
const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
||||
getFolder(row.pathname)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-sm">
|
||||
<div
|
||||
class="flex flex-break d justify-between space-between"
|
||||
v-if="currentDept >= 1"
|
||||
>
|
||||
<div>
|
||||
<span class="text-h6">{{ currentLevel }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="props.mode == 'admin' && currentDept != 4"
|
||||
outline
|
||||
push
|
||||
class="q-px-md q-ml-md q-py-sm"
|
||||
:label="'สร้าง' + currentLevel"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
icon="add"
|
||||
@click="() => triggerFolderCreate()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:rows="listDataFolder"
|
||||
:columns="columnsFolder"
|
||||
row-key="name"
|
||||
hide-bottom
|
||||
:rows-per-page-options="[0]"
|
||||
@row-click="onRowClick"
|
||||
class="cursor"
|
||||
>
|
||||
<template v-slot:body-cell-name="nameRow">
|
||||
<q-td style="width: 50%">
|
||||
<q-icon :name="currentIcon" size="2em" color="primary" />
|
||||
{{ nameRow.row.name }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-createdAt="createdAtRow">
|
||||
<q-td>
|
||||
<div class="justify-center">
|
||||
{{ getFormatDate(createdAtRow.row.createdAt) }}
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-actions="actionsRow">
|
||||
<q-td class="justify-center">
|
||||
<div>
|
||||
<q-icon class="q-ma-sm" name="info" size="2em" color="primary" />
|
||||
<q-tooltip
|
||||
anchor="center left"
|
||||
self="center right"
|
||||
:offset="[5, 1]"
|
||||
>
|
||||
{{ actionsRow.row.name }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div v-if="props.mode === 'admin'">
|
||||
<q-btn
|
||||
flat
|
||||
color="positive"
|
||||
dense
|
||||
icon="edit"
|
||||
@click.stop="
|
||||
triggerFolderEdit(
|
||||
actionsRow.row.name,
|
||||
actionsRow.row.pathname
|
||||
)
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
dense
|
||||
icon="delete"
|
||||
@click.stop="triggerFolderDelete(actionsRow.row.pathname)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-pa-md" v-if="currentDept >= 3">
|
||||
<div class="q-gutter-sm">
|
||||
<div class="flex flex-break d justify-between space-between">
|
||||
<div>
|
||||
<span class="text-h6">เอกสาร</span>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="props.mode == 'admin'"
|
||||
outline
|
||||
push
|
||||
class="q-px-md q-ml-md q-py-sm"
|
||||
label="สร้างเอกสาร"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
icon="add"
|
||||
@click="() => triggerFileCreate()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
:rows="listDataFile"
|
||||
:columns="columnsFile"
|
||||
row-key="name"
|
||||
hide-bottom
|
||||
:rows-per-page-options="[0]"
|
||||
class="cursor"
|
||||
>
|
||||
<template v-slot:body-cell-name="nameRow">
|
||||
<q-td
|
||||
style="width: 50%"
|
||||
@click="
|
||||
() => {
|
||||
currentDept >= 3
|
||||
? getFileInfo(nameRow.row)
|
||||
: getFolder(nameRow.row.pathname)
|
||||
}
|
||||
"
|
||||
>
|
||||
<file-icon size="list" :fileMimeType="nameRow.row.fileType" />
|
||||
{{ nameRow.row.fileName }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-fileType="fileTypeRow">
|
||||
<q-td>
|
||||
<div class="justify-center">
|
||||
{{ getType(fileTypeRow.row.fileType) }}
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-actions="actionsRow">
|
||||
<q-td class="justify-center">
|
||||
<div>
|
||||
<q-icon class="q-ma-sm" name="info" size="2em" color="primary" />
|
||||
<q-tooltip
|
||||
anchor="center left"
|
||||
self="center right"
|
||||
:offset="[5, 1]"
|
||||
>
|
||||
{{ getSize(actionsRow.row.fileSize) }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div v-if="props.mode === 'admin'">
|
||||
<q-btn
|
||||
flat
|
||||
color="positive"
|
||||
dense
|
||||
icon="edit"
|
||||
@click="
|
||||
() => triggerFileEdit(actionsRow.row, actionsRow.row.pathname)
|
||||
"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
dense
|
||||
icon="delete"
|
||||
@click="() => triggerFileDelete(actionsRow.row.pathname)"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<file-form
|
||||
:mode="fileFormType"
|
||||
:error="fileFormError"
|
||||
v-model:open="fileFormState"
|
||||
v-model:title="fileFormData.title"
|
||||
v-model:description="fileFormData.description"
|
||||
v-model:keyword="fileFormData.keyword"
|
||||
v-model:category="fileFormData.category"
|
||||
@filechange="(name: string) => (fileFormError.fileExist = checkFile(name))"
|
||||
@submit="submitFileForm"
|
||||
/>
|
||||
|
||||
<folder-form
|
||||
:mode="folderFormType"
|
||||
:tree="DEPT_NAME[currentDept]"
|
||||
v-if="currentDept < 4"
|
||||
v-model:open="folderFormState"
|
||||
v-model:name="folderFormData.name"
|
||||
@submit="submitFolderForm"
|
||||
/>
|
||||
|
||||
<dialog-delete
|
||||
v-model:open="dialogDeleteState"
|
||||
@confirm="
|
||||
() =>
|
||||
deleteFormType &&
|
||||
(deleteFolder(deleteFormPath), deleteFile(deleteFormPath))
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.justify-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,24 +4,51 @@ import { storeToRefs } from 'pinia'
|
|||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import FileItem from '@/components/FileItem.vue'
|
||||
import TreeExplorer from '@/components/TreeExplorer.vue'
|
||||
|
||||
import FileItem from './FileItem.vue'
|
||||
import TreeExplorer from './TreeExplorer.vue'
|
||||
import FileSearched from '.FileSearched.vue'
|
||||
import ListView from './ListView.vue'
|
||||
import FolderForm from './FolderForm.vue'
|
||||
import GlobalErrorDialog from './GlobalErrorDialog.vue'
|
||||
|
||||
import SearchBar from '@/modules/01_user/components/SearchBar.vue'
|
||||
import FileSearched from '@/components/FileSearched.vue'
|
||||
import FileDownload from '@/modules/01_user/components/FileDownload.vue'
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย', 'ไฟล์']
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
|
||||
|
||||
const { isPreview } = storeToRefs(useFileInfoStore())
|
||||
const { isFilePreview } = storeToRefs(useFileInfoStore())
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { data, currentDept } = storeToRefs(useTreeDataStore())
|
||||
const { getCabinet, gotoParent } = useTreeDataStore()
|
||||
const { data, currentDept, currentPath } = storeToRefs(useTreeDataStore())
|
||||
const { createFolder, getCabinet, gotoParent, getFolder } = useTreeDataStore()
|
||||
|
||||
const viewMode = ref<'view_list' | 'view_module'>('view_list')
|
||||
const inputSearch = ref<string>()
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
||||
const folderFormState = ref<boolean>(false)
|
||||
const folderFormType = ref<'edit' | 'create'>('create')
|
||||
const folderFormData = ref<{
|
||||
name?: string
|
||||
}>({})
|
||||
|
||||
function triggerFolderCreate() {
|
||||
folderFormType.value = 'create'
|
||||
folderFormData.value = {}
|
||||
folderFormState.value = !folderFormState.value
|
||||
}
|
||||
|
||||
async function submitFolderForm(value: {
|
||||
mode: 'create' | 'edit'
|
||||
name: string
|
||||
}) {
|
||||
if (value.mode === 'create') {
|
||||
await createFolder(value.name)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(getCabinet)
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -65,58 +92,100 @@ onMounted(getCabinet)
|
|||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<file-download v-if="isPreview === true"/>
|
||||
<div class="bg-white rounded-borders shadow-5 relative" v-if="isPreview === false">
|
||||
<file-download v-if="isFilePreview === true" />
|
||||
<div
|
||||
class="bg-white rounded-borders shadow-5 relative"
|
||||
v-if="isFilePreview === false"
|
||||
>
|
||||
<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 && isSearch === false"
|
||||
@click="() => gotoParent()"
|
||||
>
|
||||
<q-icon name="arrow_back" size="1rem" color="primary"
|
||||
/></q-btn>
|
||||
<span v-if="isSearch === false">{{ DEPT_NAME[currentDept] }}</span>
|
||||
<span class="text-body1">
|
||||
<div class="row items-center">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
v-if="currentDept > 0 && isSearch === false"
|
||||
@click="() => gotoParent()"
|
||||
>
|
||||
<q-icon name="arrow_back" size="1rem" color="primary" />
|
||||
</q-btn>
|
||||
<q-breadcrumbs v-if="isSearch === false" active-color="primary">
|
||||
<q-breadcrumbs-el
|
||||
v-if="currentPath === '/' || !currentPath"
|
||||
label="ตู้เอกสารทั้งหมด"
|
||||
/>
|
||||
<q-breadcrumbs-el
|
||||
class="text-black"
|
||||
v-for="fragments in currentPath.split('/').filter(Boolean)"
|
||||
:label="fragments"
|
||||
/>
|
||||
</q-breadcrumbs>
|
||||
</div>
|
||||
<span v-if="isSearch === true">ผลการค้นหา</span>
|
||||
<q-btn
|
||||
v-if="mode === 'admin' && viewMode === 'view_module'"
|
||||
class="q-px-md q-ml-md"
|
||||
v-if="
|
||||
mode === 'admin' &&
|
||||
viewMode === 'view_module' &&
|
||||
currentDept === 0
|
||||
"
|
||||
class="q-px-md q-ml-md al"
|
||||
label="สร้างตู้เก็บเอกสาร"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
icon="add"
|
||||
@click=""
|
||||
@click="() => triggerFolderCreate()"
|
||||
/>
|
||||
</span>
|
||||
<q-btn
|
||||
flat
|
||||
color="blue-grey-2"
|
||||
:icon="viewMode"
|
||||
@click="
|
||||
() => {
|
||||
viewMode =
|
||||
viewMode === 'view_list' ? 'view_module' : 'view_list'
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="blue-grey-2"
|
||||
icon="refresh"
|
||||
class="q-mr-sm"
|
||||
@click="() => getFolder(currentPath)"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="blue-grey-2"
|
||||
:icon="viewMode"
|
||||
@click="
|
||||
() => {
|
||||
viewMode =
|
||||
viewMode === 'view_list' ? 'view_module' : 'view_list'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<file-searched v-if="isSearch === true" />
|
||||
<file-item
|
||||
:viewMode="viewMode"
|
||||
:action="props.mode === 'admin'"
|
||||
v-if="isSearch === false"
|
||||
v-if="isSearch === false && viewMode === 'view_list'"
|
||||
/>
|
||||
<list-view v-if="viewMode === 'view_module'" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<folder-form
|
||||
:mode="folderFormType"
|
||||
:tree="DEPT_NAME[currentDept]"
|
||||
v-if="currentDept < 4"
|
||||
v-model:open="folderFormState"
|
||||
v-model:name="folderFormData.name"
|
||||
@submit="submitFolderForm"
|
||||
/>
|
||||
|
||||
<global-error-dialog />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { getUsername, logout } from '@/services/KeyCloakService'
|
||||
import { ref } from 'vue'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
const dropdownOpen = ref<boolean>(false)
|
||||
const accountName = ref<string>()
|
||||
|
||||
accountName.value = KeyCloakService.GetUserName()
|
||||
const accountName = ref<string>(getUsername())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -32,16 +30,7 @@ accountName.value = KeyCloakService.GetUserName()
|
|||
</div>
|
||||
<q-btn-dropdown stretch flat v-model="dropdownOpen">
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
tabindex="0"
|
||||
@click="
|
||||
() => {
|
||||
KeyCloakService.CallLogOut()
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-item clickable v-close-popup tabindex="0" @click="() => logout()">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="logout" color="primary" text-color="white" caption>
|
||||
</q-avatar>
|
||||
|
|
@ -50,7 +39,6 @@ accountName.value = KeyCloakService.GetUserName()
|
|||
<q-item-label>Logout</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator inset spaced></q-separator>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</template>
|
||||
|
|
|
|||
40
Services/client/src/components/TagInput.vue
Normal file
40
Services/client/src/components/TagInput.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import TagInput from "@mayank1513/vue-tag-input";
|
||||
import "@mayank1513/vue-tag-input/style.css";
|
||||
|
||||
const autocompleteItems = [
|
||||
"No dependencies",
|
||||
"Autocompletion",
|
||||
"Keep Focused",
|
||||
"Fast Settup",
|
||||
"Mini Sized",
|
||||
"Customizable",
|
||||
"Backspace/Delete to remove tag",
|
||||
"Turns red when backspace/delete is pressed",
|
||||
"Examples",
|
||||
"Docs",
|
||||
"Copy/Paste",
|
||||
]
|
||||
const tags = ref<string[]>([]);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn @click="()=> {console.log(tags);
|
||||
}">test</q-btn>
|
||||
<tag-input tagBgColor="rgb(189, 184, 179)" :autocomplete-items="autocompleteItems" v-model="tags" />
|
||||
<!-- <q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
$emit('update:drawerFile')
|
||||
|
||||
}
|
||||
"
|
||||
/> -->
|
||||
</template>
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { isFilePreview } = storeToRefs(useFileInfoStore())
|
||||
const { getFolder } = useTreeDataStore()
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -9,7 +15,7 @@ const props = withDefaults(
|
|||
}>(),
|
||||
{
|
||||
level: 0,
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
@ -17,7 +23,13 @@ const props = withDefaults(
|
|||
<div>
|
||||
<q-list v-for="folder in data" class="rounded-borders">
|
||||
<q-expansion-item
|
||||
@click="() => getFolder(folder.pathname, false)"
|
||||
@click="
|
||||
() => {
|
||||
getFolder(folder.pathname, false)
|
||||
isSearch = false
|
||||
isFilePreview = false
|
||||
}
|
||||
"
|
||||
:header-inset-level="level * 0.25"
|
||||
:group="level.toString()"
|
||||
:hide-expand-icon="level === 4"
|
||||
|
|
@ -25,10 +37,11 @@ const props = withDefaults(
|
|||
level === 1
|
||||
? 'mdi-file-cabinet'
|
||||
: level === 2
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
"
|
||||
:label="folder.name"
|
||||
class="text-overflow-handle"
|
||||
v-model="folder.status"
|
||||
>
|
||||
<tree-explorer
|
||||
|
|
@ -41,12 +54,15 @@ const props = withDefaults(
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-item[aria-expanded='true'] {
|
||||
<style lang="scss" scoped>
|
||||
:deep(.q-item[aria-expanded='true']) {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.q-item[aria-expanded='true'] .q-icon {
|
||||
color: $primary;
|
||||
.text-overflow-handle,
|
||||
:deep(.q-item__label) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
49
Services/client/src/components/UploadExistDialog.vue
Normal file
49
Services/client/src/components/UploadExistDialog.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
notification: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['update:notification', 'confirm', 'cancel'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
:model-value="notification"
|
||||
@update:model-value="(v) => $emit('update:notification', v)"
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-card style="width: 400px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<q-icon
|
||||
name="error"
|
||||
color="warning"
|
||||
size="2.5rem"
|
||||
/>เตือนพบไฟล์ชื่อซ้ำในระบบ
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
หากดำเนินการต่อข้อมูลจะถูกเขียนทับ
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="bg-white text-primary">
|
||||
<q-space />
|
||||
<q-btn
|
||||
flat
|
||||
label="ยกเลิก"
|
||||
v-close-popup
|
||||
@click="() => $emit('cancel')"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
label="ดำเนินการต่อ"
|
||||
v-close-popup
|
||||
class="text-red"
|
||||
@click="() => $emit('confirm')"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -5,40 +5,30 @@ 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'
|
||||
import { login } from './services/KeyCloakService'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
login().then(async () => {
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use((await import('./router')).default)
|
||||
app.use(pinia)
|
||||
app.use(Quasar, {
|
||||
...quasarUserOptions,
|
||||
plugins: {
|
||||
Dialog,
|
||||
Loading,
|
||||
},
|
||||
lang: th,
|
||||
})
|
||||
|
||||
app.use(Quasar, {
|
||||
...quasarUserOptions,
|
||||
plugins: {
|
||||
Dialog,
|
||||
Loading,
|
||||
},
|
||||
lang: th,
|
||||
app.component(
|
||||
'full-loader',
|
||||
defineAsyncComponent(() => import('@/components/FullLoader.vue')),
|
||||
)
|
||||
|
||||
app.mount('#app')
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
divdivdivdivdivdiv
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const isAdvncSearchClick = ref<boolean>(false)
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
|
||||
const { isAdvSearchCall } = storeToRefs(useSearchDataStore())
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
|
|
@ -10,23 +14,32 @@ const optionsOp = [
|
|||
{ label: 'และ', value: 'AND' },
|
||||
{ label: 'หรือ', value: 'OR' },
|
||||
]
|
||||
const advSearchData = ref<
|
||||
const advSearchDataRow = ref<
|
||||
{
|
||||
op: 'AND' | 'OR'
|
||||
field: 'title' | 'keyword'
|
||||
value: string
|
||||
}[]
|
||||
>([])
|
||||
>([
|
||||
{
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
},
|
||||
])
|
||||
const advSearchDataField = ref<{
|
||||
keyword: string
|
||||
description: string
|
||||
}>({
|
||||
keyword: '',
|
||||
description: '',
|
||||
})
|
||||
const props = defineProps<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
searchSubmit: Function
|
||||
submitSearchData: {
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
field: string
|
||||
value: string
|
||||
}[]
|
||||
OR: {
|
||||
field: string
|
||||
|
|
@ -36,185 +49,164 @@ const props = defineProps<{
|
|||
}>()
|
||||
|
||||
defineExpose({
|
||||
clearAdvData,
|
||||
advSearchData,
|
||||
advSearchDataRow,
|
||||
advSearchDataField,
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
'update:field',
|
||||
'update:value',
|
||||
'update:keyword',
|
||||
'update:description',
|
||||
])
|
||||
|
||||
function addSearchData() {
|
||||
advSearchData.value.push({
|
||||
function addAdvSearchData() {
|
||||
advSearchDataRow.value.push({
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
|
||||
function clearAdvData() {
|
||||
advSearchData.value = []
|
||||
function delAdvSearchData(index: number) {
|
||||
advSearchDataRow.value.length > 1 && advSearchDataRow.value.splice(index, 1)
|
||||
}
|
||||
function clearAdvSearchData() {
|
||||
isAdvSearchCall.value = false
|
||||
advSearchDataRow.value = [
|
||||
{
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
advSearchDataField.value = {
|
||||
keyword: '',
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-btn
|
||||
outline
|
||||
flat
|
||||
color="primary"
|
||||
icon="mdi-tools"
|
||||
label="ค้นหาขั้นสูง"
|
||||
@click="() => (isAdvncSearchClick = true)"
|
||||
class="q-mt-sm"
|
||||
style="width: 148px"
|
||||
v-if="isAdvSearchCall === false"
|
||||
@click="() => (isAdvSearchCall = 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 v-if="isAdvSearchCall === true">
|
||||
<div class="column bg-white q-pa-sm">
|
||||
<div class="row items-center justify-between q-pb-md">
|
||||
<span class="text-primary text-weight-medium q-pl-md q-pt-xs"
|
||||
><q-icon name="mdi-tools" class="q-pr-sm" size="sm" />
|
||||
ค้นหาขั้นสูง</span
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="red"
|
||||
icon="close"
|
||||
@click="clearAdvSearchData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div class="column q-px-lg">
|
||||
<div
|
||||
class="row items-center q-pb-xs q-col-gutter-md"
|
||||
v-for="(item, index) in advSearchDataRow"
|
||||
:key="index"
|
||||
>
|
||||
<div class="row items-center" style="width: 45px; height: 45px">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
v-close-popup
|
||||
@click="() => props.searchSubmit()"
|
||||
dense
|
||||
color="teal"
|
||||
icon="mdi-plus"
|
||||
v-if="index === advSearchDataRow.length - 1"
|
||||
@click="addAdvSearchData"
|
||||
/>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
<div class="col-4 col-md-2">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="item.op"
|
||||
:options="optionsOp"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-grow col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="item.field"
|
||||
:options="optionsField"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<q-input dense outlined v-model="item.value" placeholder="เอกสาร"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
name="close"
|
||||
@click="() => (item.value = '')"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="mdi-trash-can-outline"
|
||||
color="red"
|
||||
@click="() => delAdvSearchData(index)"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-red"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[10, 10]"
|
||||
v-if="advSearchDataRow.length === 1"
|
||||
>
|
||||
<strong>ไม่สามารถลบได้</strong>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-md q-mt-sm" />
|
||||
|
||||
<div class="row q-col-gutter-md q-pb-md">
|
||||
<div class="col-12 col-md-5">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="คำสำคัญ:"
|
||||
v-model="advSearchDataField.keyword"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-grow">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="รายละเอียด:"
|
||||
v-model="advSearchDataField.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
<div class="row justify-end">
|
||||
<q-btn
|
||||
class="q-mt-md"
|
||||
style="width: 150px"
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="() => props.searchSubmit()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import axiosClient from '@/services/HttpService'
|
||||
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
|
||||
const { isPreview, fileInfo } = storeToRefs(useFileInfoStore())
|
||||
const { getType } = useFileInfoStore()
|
||||
const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
||||
const { isFilePreview, fileInfo } = storeToRefs(useFileInfoStore())
|
||||
const { getType, getFormatDate, getSize, getFileNameFormat } =
|
||||
useFileInfoStore()
|
||||
|
||||
async function downloadSubmit(path: any) {
|
||||
const [cabinet, drawer, folder, file] = path.split('/')
|
||||
const formatPath = `/cabinet/${cabinet}/drawer/${drawer}/folder/${folder}/file/${file}`
|
||||
const res = await axiosClient.get<EhrFile & { download: string }>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}${formatPath}`,
|
||||
)
|
||||
await axios
|
||||
.get(res.data.download, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: res.data.fileType,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const a = document.createElement('a')
|
||||
a.href = window.URL.createObjectURL(r.data)
|
||||
a.download = res.data.fileName
|
||||
a.click()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded-borders shadow-5 relative">
|
||||
<div class="bg-white rounded-borders shadow-5 relative q-pb-md">
|
||||
<div class="bg-white q-pa-md">
|
||||
<div class="row items-center justify-between">
|
||||
<div class="row items-center">
|
||||
<span class="text-h6">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
@click="() => (isPreview = false)"
|
||||
@click="() => (isFilePreview = false)"
|
||||
>
|
||||
<q-icon
|
||||
class="pointer"
|
||||
|
|
@ -28,43 +54,55 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
color="primary"
|
||||
/>
|
||||
</q-btn>
|
||||
{{ fileInfo?.title }}</span
|
||||
{{ getFileNameFormat(fileInfo?.fileName) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="q-pa-sm">
|
||||
<div class="col">
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat>
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<file-icon
|
||||
size="preview"
|
||||
:fileMimeType="fileInfo?.fileType"
|
||||
ref="fileIconComp"
|
||||
/>
|
||||
<div class="absolute" style="top: 0.5rem; right: 0.5rem">
|
||||
<file-item-action :edit="() => {}" :delete="() => {}" />
|
||||
</div>
|
||||
<span class="text-center q-pt-md text-h6">{{
|
||||
fileInfo?.title
|
||||
}}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<div class="row q-mt-lg justify-center">
|
||||
<div class="column q-pa-sm">
|
||||
<div class="grid">
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '1rem',
|
||||
maxWidth: '100%',
|
||||
}"
|
||||
class="box"
|
||||
>
|
||||
<div class="q-px-md flex items-center justify-center">
|
||||
<file-icon
|
||||
size="preview"
|
||||
:fileMimeType="
|
||||
fileInfo?.fileType ? fileInfo?.fileType : 'unknow'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block q-px-md text-center"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
{{ getFileNameFormat(fileInfo?.fileName) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column q-py-sm">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ดาวน์โหลด"
|
||||
icon="mdi-download"
|
||||
class="q-py-sm"
|
||||
@click="() => downloadSubmit(fileInfo?.pathname)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column q-py-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ดาวน์โหลด"
|
||||
icon="mdi-download"
|
||||
class="q-py-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-px-lg q-gutter-md">
|
||||
<div class="col-grow q-px-lg q-gutter-md q-pt-md">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>ชื่อไฟล์</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
|
|
@ -73,7 +111,7 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>ชื่อเรื่อง</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
|
|
@ -82,7 +120,7 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>รายละเอียด</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
|
|
@ -91,34 +129,44 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>กลุ่ม/หมวดหมู่</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.category }}</span>
|
||||
<span
|
||||
class="text-grey"
|
||||
v-for="category in fileInfo?.category"
|
||||
:key="category"
|
||||
>{{ category }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>คำสำคัญ</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.keyword }}</span>
|
||||
<span
|
||||
class="text-grey"
|
||||
v-for="keyword in fileInfo?.keyword"
|
||||
:key="keyword"
|
||||
>{{ keyword }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>ขนาดไฟล์</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.fileSize }}</span>
|
||||
<span class="text-grey">{{ getSize(fileInfo?.fileSize) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>ประเภทไฟล์</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
|
|
@ -127,11 +175,13 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
</div>
|
||||
<q-separator />
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="col-12 col-md-2">
|
||||
<span>วันที่อัปโหลด</span>
|
||||
</div>
|
||||
<div class="col-grow">
|
||||
<span class="text-grey">{{ fileInfo?.createdAt }}</span>
|
||||
<span class="text-grey">{{
|
||||
getFormatDate(fileInfo?.createdAt)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,7 +192,24 @@ const fileIconComp = ref<InstanceType<typeof FileIcon>>()
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
border: 2px solid #f1f2f4;
|
||||
border: 2px solid $separator-color;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text-overflow-handle {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
width: 250px;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grid .box {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import axiosClient from '@/services/HttpService'
|
||||
|
||||
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'
|
||||
|
||||
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { isSearch, isAdvSearchCall } = 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' },
|
||||
|
|
@ -21,106 +20,71 @@ const optionsField = [
|
|||
const searchData = ref<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
}>({
|
||||
field: 'title',
|
||||
value: '',
|
||||
keyword: '',
|
||||
description: '',
|
||||
})
|
||||
const submitSearchData = ref<{
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
}[]
|
||||
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({
|
||||
if (searchData.value.value.trim() !== '') {
|
||||
submitSearchData.value = { AND: [], OR: [] }
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
if (isAdvSearchCall.value && advSearchComp.value) {
|
||||
const advField = advSearchComp.value.advSearchDataField
|
||||
const advRow = advSearchComp.value.advSearchDataRow
|
||||
|
||||
advRow.forEach((d: { field: string; value: string; op: string }) => {
|
||||
if (d.field && d.value.trim() !== '') {
|
||||
const op = d.op === 'AND' ? 'AND' : 'OR'
|
||||
submitSearchData.value[op].push({ field: d.field, value: d.value })
|
||||
}
|
||||
)
|
||||
})
|
||||
if (advField.keyword.trim() !== '') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: 'keyword',
|
||||
value: advField.keyword,
|
||||
})
|
||||
}
|
||||
if (advField.description.trim() !== '') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: 'description',
|
||||
value: advField.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
loaderStore.show()
|
||||
const res = await axiosClient.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()
|
||||
}
|
||||
}
|
||||
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="q-pa-md bg-grey-1">
|
||||
<div class="row items-center q-col-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
|
|
@ -140,47 +104,36 @@ async function searchSubmit() {
|
|||
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"
|
||||
/>
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
name="close"
|
||||
@click="() => (searchData.value = '')"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</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 class="column">
|
||||
<div class="row items-center justify-between q-gutter-y-md q-pt-sm">
|
||||
<div class="column col-grow">
|
||||
<advanced-search
|
||||
ref="advSearchComp"
|
||||
:searchSubmit="searchSubmit"
|
||||
:submit-search-data="submitSearchData"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAdvSearchCall === false">
|
||||
<q-btn
|
||||
style="width: 150px"
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="searchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,60 @@
|
|||
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'
|
||||
import { getRole, getToken, login } from '@/services/KeyCloakService'
|
||||
|
||||
const history = createWebHistory(import.meta.env.BASE_URL)
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history,
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'UserModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
children: [...UserModule],
|
||||
meta: {
|
||||
statusAccount: false,
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
const token = await getToken()
|
||||
|
||||
if (token) return next()
|
||||
|
||||
await login(async () => {
|
||||
if (await getToken()) return next()
|
||||
return next('/')
|
||||
})
|
||||
},
|
||||
children: [...UserModule],
|
||||
meta: { statusAccount: false },
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'AdminModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
beforeEnter: (_to, _from, next) => {
|
||||
const token = KeyCloakService.GetAccesToken()
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
const token = await getToken()
|
||||
|
||||
if (token) {
|
||||
next()
|
||||
} else {
|
||||
KeyCloakService.CallLogin(() => {
|
||||
const tokenAfterLogin = KeyCloakService.GetAccesToken()
|
||||
if (tokenAfterLogin) {
|
||||
next()
|
||||
} else {
|
||||
console.error('ไม่สามารถดึง Token หลังจากล็อกอินได้')
|
||||
next('/')
|
||||
}
|
||||
})
|
||||
const roles = getRole()
|
||||
|
||||
if (token && roles.includes('admin')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
return next('/')
|
||||
}
|
||||
|
||||
await login(async () => {
|
||||
const token = await getToken()
|
||||
const roles = getRole()
|
||||
|
||||
if (token && roles.includes('admin')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
return next('/')
|
||||
})
|
||||
},
|
||||
meta: {
|
||||
statusAccount: true,
|
||||
},
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -1,37 +1,32 @@
|
|||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
import { getToken } from './KeyCloakService'
|
||||
import { useErrorStore } from '@/stores/error'
|
||||
|
||||
const HttpMethods = {
|
||||
GET: 'GET',
|
||||
POST: 'POST',
|
||||
DELETE: 'DELETE',
|
||||
}
|
||||
const error = useErrorStore()
|
||||
|
||||
const _axios = axios.create()
|
||||
const cb = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers.Authorization = `Bearer ${KeyCloakService.GetAccesToken()}`
|
||||
console.log(config.headers)
|
||||
const instance = axios.create()
|
||||
|
||||
instance.interceptors.request.use(async (config) => {
|
||||
config.headers.Authorization = `Bearer ${await getToken()}`
|
||||
return config
|
||||
}
|
||||
})
|
||||
|
||||
const configureAxiosKeycloak = (): void => {
|
||||
_axios.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||
if (KeyCloakService.IsLoggedIn()) {
|
||||
KeyCloakService.UpdateToken(cb(config))
|
||||
}
|
||||
return config
|
||||
instance.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
const status = err.response.status
|
||||
const data = err.response.data
|
||||
|
||||
error.title = 'เกิดข้อผิดพลาด'
|
||||
|
||||
if (status === 500) {
|
||||
error.msg = 'เกิดข้อผิดพลาด ไม่สามารถดำเนินการต่อได้ กรุณาลองใหม่อีกครั้ง'
|
||||
} else {
|
||||
error.msg = data.message
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getAxiosClient = (): AxiosInstance => _axios
|
||||
error.show()
|
||||
},
|
||||
)
|
||||
|
||||
const HttpService = {
|
||||
HttpMethods,
|
||||
configureAxiosKeycloak,
|
||||
getAxiosClient,
|
||||
}
|
||||
|
||||
export default HttpService
|
||||
export default instance
|
||||
|
|
|
|||
|
|
@ -1,71 +1,42 @@
|
|||
import Keycloak from "keycloak-js";
|
||||
import Keycloak from 'keycloak-js'
|
||||
|
||||
const keycloakInstance = new Keycloak();
|
||||
const keycloak = 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");
|
||||
export async function login(cb?: (...args: any[]) => void) {
|
||||
const auth = await keycloak
|
||||
.init({
|
||||
onLoad: 'login-required',
|
||||
responseMode: 'query',
|
||||
checkLoginIframe: false,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.dir(e);
|
||||
console.log(`keycloak init exception: ${e}`);
|
||||
});
|
||||
};
|
||||
.catch((e) => console.dir(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
|
||||
if (auth && cb) cb()
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await keycloak.logout()
|
||||
}
|
||||
|
||||
const updateToken = (successCallback: any) =>
|
||||
keycloakInstance.updateToken(5).then(successCallback).catch(doLogin);
|
||||
export async function getToken() {
|
||||
await keycloak.updateToken(60).catch(() => login())
|
||||
return keycloak.token
|
||||
}
|
||||
|
||||
const doLogin = keycloakInstance.login;
|
||||
export function getUsername(): string {
|
||||
return keycloak.tokenParsed?.preferred_username
|
||||
}
|
||||
|
||||
const isLoggedIn = () => !!keycloakInstance.token;
|
||||
export function getRole(): string[] {
|
||||
const decoded = keycloak.tokenParsed
|
||||
|
||||
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;
|
||||
if (decoded && decoded.resource_access && decoded.azp) {
|
||||
return decoded.resource_access[decoded.azp].roles
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return !!keycloak.token
|
||||
}
|
||||
|
|
|
|||
28
Services/client/src/stores/error.ts
Normal file
28
Services/client/src/stores/error.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useErrorStore = defineStore('error-data', () => {
|
||||
const visible = ref<boolean>(false)
|
||||
const title = ref<string>('')
|
||||
const msg = ref<string>('')
|
||||
|
||||
function set(obj: { title: string; msg: string }, timeout?: number) {
|
||||
title.value = obj.title
|
||||
msg.value = obj.msg
|
||||
show(timeout)
|
||||
}
|
||||
|
||||
function show(timeout: number = -1) {
|
||||
visible.value = true
|
||||
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => (visible.value = false), timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
return { visible, title, msg, show, hide, set }
|
||||
})
|
||||
|
|
@ -6,156 +6,182 @@ import type { EhrFile } from '@/stores/tree-data'
|
|||
export interface MimeMap {
|
||||
[key: string]: { icon: string; color: string; type: string }
|
||||
}
|
||||
export interface TypeSetting {
|
||||
[key: string]: { icon: string; color: string }
|
||||
}
|
||||
|
||||
export const useFileInfoStore = defineStore('info', () => {
|
||||
const fileInfo = ref<EhrFile>()
|
||||
const isPreview = ref<Boolean>(false)
|
||||
const isFilePreview = ref<Boolean>(false)
|
||||
const file: TypeSetting = {
|
||||
word: { icon: 'mdi-file-word-outline', color: 'blue-11' },
|
||||
excel: { icon: 'mdi-file-excel-outline', color: 'green-4' },
|
||||
powerpoint: { icon: 'mdi-file-powerpoint-outline', color: 'orange-4' },
|
||||
pdf: { icon: 'mdi-file-document-outline', color: 'red-11' },
|
||||
txt: { icon: 'mdi-file-document-outline', color: 'blue-11' },
|
||||
image: { icon: 'mdi-file-image-outline', color: 'blue-11' },
|
||||
}
|
||||
const mimeFileMapping: MimeMap = {
|
||||
'application/msword': {
|
||||
icon: 'mdi-file-word-outline',
|
||||
color: 'blue-11',
|
||||
...file.word,
|
||||
type: '.doc',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': {
|
||||
icon: 'mdi-file-word-outline',
|
||||
color: 'blue-11',
|
||||
...file.word,
|
||||
type: '.docx',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': {
|
||||
icon: 'mdi-file-word-outline',
|
||||
color: 'blue-11',
|
||||
...file.word,
|
||||
type: '.dotx',
|
||||
},
|
||||
'application/vnd.ms-word.document.macroEnabled.12': {
|
||||
icon: 'mdi-file-word-outline',
|
||||
color: 'blue-11',
|
||||
...file.word,
|
||||
type: '.docm',
|
||||
},
|
||||
'application/vnd.ms-word.template.macroEnabled.12': {
|
||||
icon: 'mdi-file-word-outline',
|
||||
color: 'blue-11',
|
||||
...file.word,
|
||||
type: '.dotm',
|
||||
},
|
||||
|
||||
'application/vnd.ms-excel': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xls',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xlsx',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xltx',
|
||||
},
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xlsm',
|
||||
},
|
||||
'application/vnd.ms-excel.template.macroEnabled.12': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xltm',
|
||||
},
|
||||
'application/vnd.ms-excel.addin.macroEnabled.12': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xlam',
|
||||
},
|
||||
'application/vnd.ms-excel.sheet.binary.macroEnabled.12': {
|
||||
icon: 'mdi-file-excel-outline',
|
||||
color: 'green-12',
|
||||
...file.excel,
|
||||
type: '.xlsb',
|
||||
},
|
||||
|
||||
'application/vnd.ms-powerpoint': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.ppt',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
||||
{
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.pptx',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.potx',
|
||||
},
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.ppsx',
|
||||
},
|
||||
'application/vnd.ms-powerpoint.addin.macroEnabled.12': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.ppam',
|
||||
},
|
||||
'application/vnd.ms-powerpoint.presentation.macroEnabled.12': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.pptm',
|
||||
},
|
||||
'application/vnd.ms-powerpoint.template.macroEnabled.12': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.potm',
|
||||
},
|
||||
'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': {
|
||||
icon: 'mdi-file-powerpoint-outline',
|
||||
color: 'orange-12',
|
||||
...file.powerpoint,
|
||||
type: '.ppsm',
|
||||
},
|
||||
|
||||
'application/pdf': {
|
||||
icon: 'mdi-file-document-outline',
|
||||
color: 'red-12',
|
||||
...file.pdf,
|
||||
type: '.pdf',
|
||||
},
|
||||
|
||||
'text/plain': {
|
||||
icon: 'mdi-file-document-outline',
|
||||
color: 'blue-11',
|
||||
...file.txt,
|
||||
type: '.txt',
|
||||
},
|
||||
|
||||
'image/x-png': {
|
||||
icon: 'mdi-file-image-outline',
|
||||
color: 'blue-11',
|
||||
...file.image,
|
||||
type: '.png',
|
||||
},
|
||||
'image/x-citrix-jpeg': {
|
||||
icon: 'mdi-file-image-outline',
|
||||
color: 'blue-11',
|
||||
...file.image,
|
||||
type: '.jpg, jpeg',
|
||||
},
|
||||
}
|
||||
|
||||
function getType(mimeType: any) {
|
||||
if (mimeFileMapping.hasOwnProperty(mimeType)) {
|
||||
function getType(mimeType: string | undefined): string {
|
||||
if (!!mimeType && mimeFileMapping.hasOwnProperty(mimeType)) {
|
||||
return mimeFileMapping[mimeType].type
|
||||
} else {
|
||||
return 'unknow type'
|
||||
}
|
||||
return 'unknown type'
|
||||
}
|
||||
|
||||
function getFormatDate(dateTime: string | undefined): string {
|
||||
if (dateTime === undefined) {
|
||||
return 'unknown date'
|
||||
}
|
||||
const date = new Date(dateTime)
|
||||
return date.toLocaleDateString('th-TH', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function getFileNameFormat(fileName: string | undefined): string {
|
||||
if (fileName === undefined) {
|
||||
return 'unknow name'
|
||||
}
|
||||
const dotIndex = fileName.lastIndexOf('.')
|
||||
const fileNameOnly = fileName.substring(0, dotIndex)
|
||||
|
||||
return fileNameOnly
|
||||
}
|
||||
|
||||
function getSize(size: string | undefined): string {
|
||||
if (size === undefined) {
|
||||
return 'unknow size'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let i = 0
|
||||
let sizeNumber = parseFloat(size)
|
||||
while (sizeNumber >= 1024 && i < units.length - 1) {
|
||||
sizeNumber /= 1024
|
||||
i++
|
||||
}
|
||||
return sizeNumber.toFixed(2) + ' ' + units[i]
|
||||
}
|
||||
|
||||
async function getFileInfo(data: EhrFile) {
|
||||
isFilePreview.value = true
|
||||
fileInfo.value = data
|
||||
}
|
||||
|
||||
return {
|
||||
mimeFileMapping,
|
||||
isPreview,
|
||||
isFilePreview,
|
||||
fileInfo,
|
||||
getFileInfo,
|
||||
getType,
|
||||
getFormatDate,
|
||||
getFileNameFormat,
|
||||
getSize,
|
||||
getFileInfo,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { EhrFile } from '@/stores/tree-data'
|
|||
|
||||
export const useSearchDataStore = defineStore('searched', () => {
|
||||
const foundFile = ref<EhrFile[]>([])
|
||||
const isAdvSearchCall = ref<boolean>(false)
|
||||
const isSearch = ref<Boolean>(false)
|
||||
|
||||
async function getFoundFile(data: EhrFile[]) {
|
||||
|
|
@ -11,8 +12,9 @@ export const useSearchDataStore = defineStore('searched', () => {
|
|||
}
|
||||
|
||||
return {
|
||||
isSearch,
|
||||
foundFile,
|
||||
isSearch,
|
||||
isAdvSearchCall,
|
||||
getFoundFile,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
import HttpService from '@/services/HttpService'
|
||||
import axiosClient from '@/services/HttpService'
|
||||
|
||||
const axiosClient = HttpService.getAxiosClient()
|
||||
const apiEndpoint: string = import.meta.env.VITE_API_ENDPOINT
|
||||
|
||||
export interface EhrFolder {
|
||||
|
|
@ -41,13 +40,16 @@ export interface TreeDataFolder {
|
|||
|
||||
export const useTreeDataStore = defineStore('changeCabinet', () => {
|
||||
const loader = useLoader()
|
||||
const indexToRemove = ref<number>()
|
||||
const fileNameRemove = ref<string>('')
|
||||
|
||||
const data = ref<TreeDataFolder[]>([])
|
||||
const currentFolder = ref<TreeDataFolder[]>([])
|
||||
const currentFile = ref<EhrFile[]>([])
|
||||
const currentPath = ref<string>('')
|
||||
const currentDept = ref<number>(0)
|
||||
|
||||
const listDataFolder = ref<TreeDataFolder[]>()
|
||||
const listDataFile = ref<EhrFile[]>()
|
||||
async function getCabinet() {
|
||||
const res = await axiosClient.get<EhrFolder[]>(`${apiEndpoint}cabinet`)
|
||||
|
||||
|
|
@ -56,6 +58,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
status: false,
|
||||
folder: [],
|
||||
}))
|
||||
|
||||
listDataFolder.value = data.value
|
||||
}
|
||||
|
||||
async function getFolder(pathname: string, updateStatus = true) {
|
||||
|
|
@ -129,6 +133,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
|
||||
await getFile(pathname)
|
||||
|
||||
listDataFolder.value = currentFolder.value
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +157,8 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
|
||||
currentFile.value = res.data
|
||||
|
||||
listDataFile.value = currentFile.value
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
|
|
@ -232,16 +240,145 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
await getFolder(pathname.join('/') + '/')
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
pathname: string,
|
||||
file: File,
|
||||
metadata: {
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
}
|
||||
) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathArray.length <= 2) return
|
||||
|
||||
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]}`
|
||||
requestPath += '/file'
|
||||
|
||||
const res = await axiosClient.post<EhrFile & { upload: string }>(
|
||||
`${apiEndpoint}${requestPath}`,
|
||||
{
|
||||
file: file.name,
|
||||
...metadata,
|
||||
}
|
||||
)
|
||||
|
||||
if (res && res.data.upload) {
|
||||
await fetch(res.data.upload, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
})
|
||||
|
||||
loader.hide()
|
||||
}
|
||||
|
||||
if (currentDept.value === 0) {
|
||||
await getCabinet()
|
||||
} else await getFolder(currentPath.value)
|
||||
|
||||
await getFile(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function updateFile(
|
||||
pathname: string,
|
||||
metadata: {
|
||||
title: string
|
||||
description: string
|
||||
keyword: string
|
||||
category: string
|
||||
},
|
||||
file?: File
|
||||
) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathArray.length < 4) return loader.hide()
|
||||
|
||||
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 4) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 5) requestPath += `/subfolder/${pathArray[3]}`
|
||||
requestPath += `/file/${pathArray.at(-1)}`
|
||||
|
||||
const res = await axiosClient.patch<{ upload: string }>(
|
||||
`${apiEndpoint}${requestPath}`,
|
||||
{ file: file?.name, ...metadata }
|
||||
)
|
||||
|
||||
if (res && res.data.upload) {
|
||||
await fetch(res.data.upload, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
})
|
||||
|
||||
loader.hide()
|
||||
}
|
||||
|
||||
if (currentDept.value === 0) {
|
||||
await getCabinet()
|
||||
} else await getFolder(currentPath.value)
|
||||
|
||||
await getFile(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function deleteFile(pathname: string) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathArray.length < 4) return loader.hide()
|
||||
|
||||
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 4) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 5) requestPath += `/subfolder/${pathArray[3]}`
|
||||
requestPath += `/file/${pathArray.at(-1)}`
|
||||
|
||||
await axiosClient.delete(`${apiEndpoint}${requestPath}`)
|
||||
|
||||
if (currentDept.value === 0) {
|
||||
await getCabinet()
|
||||
} else await getFolder(currentPath.value)
|
||||
|
||||
await getFile(currentPath.value)
|
||||
|
||||
currentFile.value = currentFile.value.filter((v) => v.pathname !== pathname)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
function checkFile(fileName: string) {
|
||||
return currentFile.value.some((element) => element.fileName === fileName)
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
currentFolder,
|
||||
currentFile,
|
||||
currentDept,
|
||||
currentPath,
|
||||
listDataFile,
|
||||
listDataFolder,
|
||||
getCabinet,
|
||||
getFolder,
|
||||
uploadFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
gotoParent,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
editFolder,
|
||||
checkFile,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ body
|
|||
-webkit-font-smoothing: antialiased
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
|
||||
$separator-color: #EDEDED !default
|
||||
|
||||
$shadow-color: #c1c1c7 !default
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ const { loader } = storeToRefs(loaderStore)
|
|||
</div>
|
||||
|
||||
<q-space></q-space>
|
||||
<profile v-if="$route.meta.statusAccount" />
|
||||
<profile />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<QPage>
|
||||
<q-page>
|
||||
<router-view :key="$route.fullPath" />
|
||||
</QPage>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
<full-loader :visibility="loader" />
|
||||
</q-layout>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
PUBLIC_KEY=
|
||||
|
||||
PORT=
|
||||
|
||||
MINIO_HOST=localhost
|
||||
MINIO_PORT=443
|
||||
MINIO_SSL=true
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_BUCKET=
|
||||
|
||||
ELASTICSEARCH_PROTOCOL=http
|
||||
ELASTICSEARCH_HOST=localhost
|
||||
ELASTICSEARCH_PORT=9200
|
||||
ELASTICSEARCH_INDEX=
|
||||
|
||||
AMQ_URL=amqp://admin:1234@localhost:9999
|
||||
AMQ_QUEUE=queue
|
||||
|
|
|
|||
66
Services/server/docker-compose.yaml
Normal file
66
Services/server/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
version: "3.8"
|
||||
|
||||
name: "ehr"
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
build:
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9200:9200
|
||||
volumes:
|
||||
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||
environment:
|
||||
- xpack.security.enabled=false
|
||||
- discovery.type=single-node
|
||||
|
||||
kibana:
|
||||
image: kibana:8.10.2
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
ports:
|
||||
- 5601:5601
|
||||
volumes:
|
||||
- kibana-data:/usr/share/kibana/data
|
||||
environment:
|
||||
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
command: server --console-address ":9001" /data
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 9001:9001
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
environment:
|
||||
- MINIO_ROOT_USER=ehr
|
||||
- MINIO_ROOT_PASSWORD=P@ssw0rd
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:22.0.3
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- start-dev
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- keycloak-data:/opt/keycloak/data
|
||||
environment:
|
||||
- KEYCLOAK_ADMIN=ehr
|
||||
- KEYCLOAK_ADMIN_PASSWORD=P@ssw0rd
|
||||
|
||||
volumes:
|
||||
elasticsearch-data:
|
||||
driver: local
|
||||
kibana-data:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
keycloak-data:
|
||||
driver: local
|
||||
3392
Services/server/package-lock.json
generated
Normal file
3392
Services/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -19,9 +19,6 @@
|
|||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.10.0",
|
||||
"@tsoa/runtime": "^5.0.0",
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/multer": "^1.4.10",
|
||||
"amqplib": "^0.10.3",
|
||||
"concurrently": "^8.2.2",
|
||||
"cors": "^2.8.5",
|
||||
|
|
@ -29,14 +26,16 @@
|
|||
"express": "^4.18.2",
|
||||
"fast-jwt": "^3.3.1",
|
||||
"minio": "^7.1.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"prettier": "^3.1.0",
|
||||
"promise.any": "^2.0.6",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"tsoa": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/amqplib": "^0.10.4",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/swagger-ui-express": "^4.1.6",
|
||||
"nodemon": "^3.0.1",
|
||||
|
|
|
|||
114
Services/server/pnpm-lock.yaml
generated
114
Services/server/pnpm-lock.yaml
generated
|
|
@ -11,15 +11,6 @@ dependencies:
|
|||
'@tsoa/runtime':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
'@types/cors':
|
||||
specifier: ^2.8.16
|
||||
version: 2.8.16
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.5
|
||||
version: 9.0.5
|
||||
'@types/multer':
|
||||
specifier: ^1.4.10
|
||||
version: 1.4.10
|
||||
amqplib:
|
||||
specifier: ^0.10.3
|
||||
version: 0.10.3
|
||||
|
|
@ -41,12 +32,12 @@ dependencies:
|
|||
minio:
|
||||
specifier: ^7.1.3
|
||||
version: 7.1.3
|
||||
multer:
|
||||
specifier: 1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
prettier:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
promise.any:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6
|
||||
swagger-ui-express:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(express@4.18.2)
|
||||
|
|
@ -58,9 +49,15 @@ devDependencies:
|
|||
'@types/amqplib':
|
||||
specifier: ^0.10.4
|
||||
version: 0.10.4
|
||||
'@types/cors':
|
||||
specifier: ^2.8.17
|
||||
version: 2.8.17
|
||||
'@types/express':
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.5
|
||||
version: 9.0.5
|
||||
'@types/node':
|
||||
specifier: ^20.9.0
|
||||
version: 20.9.0
|
||||
|
|
@ -218,11 +215,11 @@ packages:
|
|||
dependencies:
|
||||
'@types/node': 20.9.0
|
||||
|
||||
/@types/cors@2.8.16:
|
||||
resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==}
|
||||
/@types/cors@2.8.17:
|
||||
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
|
||||
dependencies:
|
||||
'@types/node': 20.9.0
|
||||
dev: false
|
||||
dev: true
|
||||
|
||||
/@types/express-serve-static-core@4.17.41:
|
||||
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
|
||||
|
|
@ -247,7 +244,7 @@ packages:
|
|||
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
||||
dependencies:
|
||||
'@types/node': 20.9.0
|
||||
dev: false
|
||||
dev: true
|
||||
|
||||
/@types/mime@1.3.5:
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
|
@ -357,10 +354,6 @@ packages:
|
|||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/append-field@1.0.0:
|
||||
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
||||
dev: false
|
||||
|
||||
/arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
dev: true
|
||||
|
|
@ -488,21 +481,10 @@ packages:
|
|||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-more-ints@1.0.0:
|
||||
resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==}
|
||||
dev: false
|
||||
|
||||
/busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
dev: false
|
||||
|
||||
/bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -562,16 +544,6 @@ packages:
|
|||
/concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
/concat-stream@1.6.2:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
inherits: 2.0.4
|
||||
readable-stream: 2.3.8
|
||||
typedarray: 0.0.6
|
||||
dev: false
|
||||
|
||||
/concurrently@8.2.2:
|
||||
resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==}
|
||||
engines: {node: ^14.13.0 || >=16.0.0}
|
||||
|
|
@ -1306,10 +1278,6 @@ packages:
|
|||
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||
dev: false
|
||||
|
||||
/isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: false
|
||||
|
||||
/isarray@2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
dev: false
|
||||
|
|
@ -1449,13 +1417,6 @@ packages:
|
|||
xml2js: 0.5.0
|
||||
dev: false
|
||||
|
||||
/mkdirp@0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
dev: false
|
||||
|
||||
/mnemonist@0.39.5:
|
||||
resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==}
|
||||
dependencies:
|
||||
|
|
@ -1473,19 +1434,6 @@ packages:
|
|||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
/multer@1.4.5-lts.1:
|
||||
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
dependencies:
|
||||
append-field: 1.0.0
|
||||
busboy: 1.6.0
|
||||
concat-stream: 1.6.2
|
||||
mkdirp: 0.5.6
|
||||
object-assign: 4.1.1
|
||||
type-is: 1.6.18
|
||||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/negotiator@0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -1590,10 +1538,6 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: false
|
||||
|
||||
/promise.any@2.0.6:
|
||||
resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -1664,18 +1608,6 @@ packages:
|
|||
string_decoder: 0.10.31
|
||||
dev: false
|
||||
|
||||
/readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
dev: false
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -1884,11 +1816,6 @@ packages:
|
|||
internal-slot: 1.0.6
|
||||
dev: false
|
||||
|
||||
/streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
dev: false
|
||||
|
||||
/strict-uri-encode@2.0.0:
|
||||
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -1932,12 +1859,6 @@ packages:
|
|||
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
|
||||
dev: false
|
||||
|
||||
/string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
dev: false
|
||||
|
||||
/string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
dependencies:
|
||||
|
|
@ -2110,10 +2031,6 @@ packages:
|
|||
is-typed-array: 1.1.12
|
||||
dev: false
|
||||
|
||||
/typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
dev: false
|
||||
|
||||
/typedoc@0.25.3(typescript@5.2.2):
|
||||
resolution: {integrity: sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
|
@ -2292,11 +2209,6 @@ packages:
|
|||
engines: {node: '>=4.0'}
|
||||
dev: false
|
||||
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: false
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ if (process.env.NODE_ENV !== "production") app.use(cors());
|
|||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static("static"));
|
||||
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { explorer: false }));
|
||||
|
||||
RegisterRoutes(router);
|
||||
|
|
@ -27,10 +26,9 @@ RegisterRoutes(router);
|
|||
app.use(swaggerSpecs.basePath, router);
|
||||
app.use(errorHandler);
|
||||
|
||||
app.use(express.static('static'))
|
||||
app.get('*',(req,res)=>{
|
||||
res.sendFile(`${process.cwd()}/static/index.html`)
|
||||
})
|
||||
app.use((_req, res, _next) => {
|
||||
res.sendFile(`${process.cwd()}/static/index.html`);
|
||||
});
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () =>
|
||||
console.log(`[APP] Application is running on http://localhost:${PORT}`),
|
||||
|
|
|
|||
|
|
@ -114,7 +114,8 @@ export class CabinetController extends Controller {
|
|||
list.map(async (current) => {
|
||||
if (!current.name) return;
|
||||
|
||||
const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`;
|
||||
const base = `${replaceIllegalChars(body.name)}/`;
|
||||
const destination = `${base}${current.name.slice(path.length)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${current.name}`;
|
||||
|
||||
return await minioClient
|
||||
|
|
@ -138,7 +139,11 @@ export class CabinetController extends Controller {
|
|||
await esClient.update({
|
||||
index: DEFAULT_INDEX!,
|
||||
id: data._id,
|
||||
doc: { pathname: destination },
|
||||
doc: {
|
||||
pathname: destination,
|
||||
path: destination.split("/").slice(0, -1).join("/") + "/",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
|
||||
|
|
|
|||
|
|
@ -131,9 +131,8 @@ export class DrawerController extends Controller {
|
|||
list.map(async (current) => {
|
||||
if (!current.name) return;
|
||||
|
||||
const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice(
|
||||
path.length,
|
||||
)}`;
|
||||
const base = `${cabinetName}/${replaceIllegalChars(body.name)}/`;
|
||||
const destination = `${base}${current.name.slice(path.length)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${current.name}`;
|
||||
|
||||
return await minioClient
|
||||
|
|
@ -157,7 +156,11 @@ export class DrawerController extends Controller {
|
|||
await esClient.update({
|
||||
index: DEFAULT_INDEX!,
|
||||
id: data._id,
|
||||
doc: { pathname: destination },
|
||||
doc: {
|
||||
pathname: destination,
|
||||
path: destination.split("/").slice(0, -1).join("/") + "/",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ export class FileController extends Controller {
|
|||
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
|
||||
index: DEFAULT_INDEX!,
|
||||
query: {
|
||||
prefix: {
|
||||
pathname: `${cabinetName}/${drawerName}/${folderName}/`,
|
||||
match: {
|
||||
path: `${cabinetName}/${drawerName}/${folderName}/`,
|
||||
},
|
||||
},
|
||||
size: 10000,
|
||||
|
|
@ -84,6 +84,10 @@ export class FileController extends Controller {
|
|||
@Path() drawerName: string,
|
||||
@Path() folderName: string,
|
||||
) {
|
||||
if (body.file.length > 85) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
|
||||
}
|
||||
|
||||
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
|
||||
const pathname = `${basePath}${body.file}`;
|
||||
|
||||
|
|
@ -111,10 +115,11 @@ export class FileController extends Controller {
|
|||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
const rec = result ? result.hits.hits[0]._source : false;
|
||||
const rec = result && result.hits.hits.length > 0 ? result.hits.hits[0]._source : false;
|
||||
|
||||
const metadata: Partial<StorageFile> = {
|
||||
pathname,
|
||||
path: basePath,
|
||||
fileName: body.file,
|
||||
fileSize: 0,
|
||||
fileType: "",
|
||||
|
|
@ -132,6 +137,7 @@ export class FileController extends Controller {
|
|||
await esClient.index({
|
||||
index: DEFAULT_INDEX!,
|
||||
document: metadata,
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -165,6 +171,10 @@ export class FileController extends Controller {
|
|||
keyword?: string;
|
||||
},
|
||||
): Promise<void | { upload: string }> {
|
||||
if (body.file && body.file.length > 85) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
|
||||
}
|
||||
|
||||
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
|
||||
const pathname = `${basePath}${fileName}`;
|
||||
|
||||
|
|
@ -201,9 +211,12 @@ export class FileController extends Controller {
|
|||
id,
|
||||
doc: {
|
||||
pathname: destination,
|
||||
path: basePath,
|
||||
fileName: body.file,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
})
|
||||
.then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname));
|
||||
} else {
|
||||
|
|
@ -230,6 +243,7 @@ export class FileController extends Controller {
|
|||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,16 +131,15 @@ export class FolderController extends Controller {
|
|||
@Path() drawerName: string,
|
||||
@Path() folderName: string,
|
||||
) {
|
||||
const path = `${cabinetName}/${drawerName}/${folderName}`;
|
||||
const path = `${cabinetName}/${drawerName}/${folderName}/`;
|
||||
const list = await listItem(DEFAULT_BUCKET!, path, true);
|
||||
|
||||
await Promise.all(
|
||||
list.map(async (current) => {
|
||||
if (!current.name) return;
|
||||
|
||||
const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars(
|
||||
body.name,
|
||||
)}/${current.name.slice(path.length)}`;
|
||||
const base = `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/`;
|
||||
const destination = `${base}${current.name.slice(path.length)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${current.name}`;
|
||||
|
||||
return await minioClient
|
||||
|
|
@ -164,7 +163,11 @@ export class FolderController extends Controller {
|
|||
await esClient.update({
|
||||
index: DEFAULT_INDEX!,
|
||||
id: data._id,
|
||||
doc: { pathname: destination },
|
||||
doc: {
|
||||
pathname: destination,
|
||||
path: destination.split("/").slice(0, -1).join("/") + "/",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
|
||||
|
|
|
|||
|
|
@ -139,9 +139,10 @@ export class SubFolderController extends Controller {
|
|||
list.map(async (current) => {
|
||||
if (!current.name) return;
|
||||
|
||||
const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
|
||||
const base = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
|
||||
body.name,
|
||||
)}/${current.name.slice(path.length)}`;
|
||||
)}/`;
|
||||
const destination = `${base}${current.name.slice(path.length)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${current.name}`;
|
||||
|
||||
return await minioClient
|
||||
|
|
@ -165,7 +166,11 @@ export class SubFolderController extends Controller {
|
|||
await esClient.update({
|
||||
index: DEFAULT_INDEX!,
|
||||
id: data._id,
|
||||
doc: { pathname: destination },
|
||||
doc: {
|
||||
pathname: destination,
|
||||
path: destination.split("/").slice(0, -1).join("/") + "/",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export class SubFolderFileController extends Controller {
|
|||
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
|
||||
index: DEFAULT_INDEX!,
|
||||
query: {
|
||||
prefix: {
|
||||
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
|
||||
match: {
|
||||
path: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`,
|
||||
},
|
||||
},
|
||||
size: 10000,
|
||||
|
|
@ -88,6 +88,10 @@ export class SubFolderFileController extends Controller {
|
|||
@Path() folderName: string,
|
||||
@Path() subFolderName: string,
|
||||
) {
|
||||
if (body.file && body.file.length > 85) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
|
||||
}
|
||||
|
||||
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
|
||||
const pathname = `${basePath}${body.file}`;
|
||||
|
||||
|
|
@ -115,10 +119,11 @@ export class SubFolderFileController extends Controller {
|
|||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
const rec = result ? result.hits.hits[0]._source : false;
|
||||
const rec = result && result.hits.hits.length > 0 ? result.hits.hits[0]._source : false;
|
||||
|
||||
const metadata: Partial<StorageFile> = {
|
||||
pathname,
|
||||
path: basePath,
|
||||
fileName: body.file,
|
||||
fileSize: 0,
|
||||
fileType: "",
|
||||
|
|
@ -136,6 +141,7 @@ export class SubFolderFileController extends Controller {
|
|||
await esClient.index({
|
||||
index: DEFAULT_INDEX!,
|
||||
document: metadata,
|
||||
refresh: "wait_for",
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -169,6 +175,10 @@ export class SubFolderFileController extends Controller {
|
|||
keyword?: string;
|
||||
},
|
||||
) {
|
||||
if (body.file && body.file.length > 85) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเิกนกำหนด");
|
||||
}
|
||||
|
||||
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
|
||||
const pathname = `${basePath}${fileName}`;
|
||||
|
||||
|
|
@ -205,9 +215,11 @@ export class SubFolderFileController extends Controller {
|
|||
id,
|
||||
doc: {
|
||||
pathname: destination,
|
||||
fileName: body.file,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
})
|
||||
.then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname));
|
||||
} else {
|
||||
|
|
@ -234,6 +246,7 @@ export class SubFolderFileController extends Controller {
|
|||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
refresh: "wait_for",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -261,7 +274,7 @@ export class SubFolderFileController extends Controller {
|
|||
) {
|
||||
await minioClient.removeObject(
|
||||
DEFAULT_BUCKET!,
|
||||
`${cabinetName}/${drawerName}/${folderName}/${fileName}/${subFolderName}/`,
|
||||
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
|
||||
);
|
||||
return this.setStatus(HttpStatusCode.NO_CONTENT);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ export interface StorageFile {
|
|||
category: string[];
|
||||
keyword: string[];
|
||||
|
||||
/**
|
||||
* @private For internal use only.
|
||||
*/
|
||||
path: string;
|
||||
upload: boolean;
|
||||
|
||||
updatedAt: string | Date;
|
||||
|
|
|
|||
|
|
@ -94,11 +94,13 @@ async function handleNotFoundRecord(
|
|||
buffer: Buffer,
|
||||
stat: { size: number; type: string },
|
||||
) {
|
||||
const path = pathname.split("/").slice(0, -1).join("/") + "/";
|
||||
const filename = pathname.split("/").at(-1);
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
|
||||
const metadata = {
|
||||
pathname,
|
||||
path,
|
||||
fileName: filename ?? "n/a", // should not possible to fallback, but just in case.
|
||||
fileSize: stat.size,
|
||||
fileType: stat.type,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const models: TsoaRoute.Models = {
|
|||
"description": {"dataType":"string","required":true},
|
||||
"category": {"dataType":"array","array":{"dataType":"string"},"required":true},
|
||||
"keyword": {"dataType":"array","array":{"dataType":"string"},"required":true},
|
||||
"path": {"dataType":"string","required":true},
|
||||
"upload": {"dataType":"boolean","required":true},
|
||||
"updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},
|
||||
"updatedBy": {"dataType":"string","required":true},
|
||||
|
|
@ -576,6 +577,83 @@ export function RegisterRoutes(app: Router) {
|
|||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/storage/d',
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController)),
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.getFolder)),
|
||||
|
||||
function StorageController_getFolder(request: any, response: any, next: any) {
|
||||
const args = {
|
||||
path: {"in":"query","name":"path","required":true,"dataType":"string"},
|
||||
bucket: {"in":"query","name":"bucket","dataType":"string"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = getValidatedArgs(args, request, response);
|
||||
|
||||
const controller = new StorageController();
|
||||
|
||||
|
||||
const promise = controller.getFolder.apply(controller, validatedArgs as any);
|
||||
promiseHandler(controller, promise, response, 200, next);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/storage/d',
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController)),
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.createFolder)),
|
||||
|
||||
function StorageController_createFolder(request: any, response: any, next: any) {
|
||||
const args = {
|
||||
path: {"in":"query","name":"path","required":true,"dataType":"string"},
|
||||
bucket: {"in":"query","name":"bucket","dataType":"string"},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = getValidatedArgs(args, request, response);
|
||||
|
||||
const controller = new StorageController();
|
||||
|
||||
|
||||
const promise = controller.createFolder.apply(controller, validatedArgs as any);
|
||||
promiseHandler(controller, promise, response, 201, next);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/storage/d',
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController)),
|
||||
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.updateFolder)),
|
||||
|
||||
function StorageController_updateFolder(request: any, response: any, next: any) {
|
||||
const args = {
|
||||
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"to":{"dataType":"nestedObjectLiteral","nestedProperties":{"path":{"dataType":"string","required":true},"bucket":{"dataType":"string","required":true}},"required":true},"from":{"dataType":"nestedObjectLiteral","nestedProperties":{"path":{"dataType":"string","required":true},"bucket":{"dataType":"string","required":true}},"required":true}}},
|
||||
};
|
||||
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
|
||||
let validatedArgs: any[] = [];
|
||||
try {
|
||||
validatedArgs = getValidatedArgs(args, request, response);
|
||||
|
||||
const controller = new StorageController();
|
||||
|
||||
|
||||
const promise = controller.updateFolder.apply(controller, validatedArgs as any);
|
||||
promiseHandler(controller, promise, response, 204, next);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
|
||||
authenticateMiddleware([{"bearerAuth":[]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@
|
|||
},
|
||||
"type": "array"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"upload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -120,6 +123,7 @@
|
|||
"description",
|
||||
"category",
|
||||
"keyword",
|
||||
"path",
|
||||
"upload",
|
||||
"updatedAt",
|
||||
"updatedBy",
|
||||
|
|
@ -974,6 +978,9 @@
|
|||
"upload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"keyword": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
|
@ -1015,6 +1022,7 @@
|
|||
"updatedBy",
|
||||
"updatedAt",
|
||||
"upload",
|
||||
"path",
|
||||
"keyword",
|
||||
"category",
|
||||
"description",
|
||||
|
|
@ -1364,6 +1372,140 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/storage/d": {
|
||||
"get": {
|
||||
"operationId": "GetFolder",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pathname": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"pathname"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "bucket",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "CreateFolder",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Success"
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "bucket",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "UpdateFolder",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Success"
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"to": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"bucket"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"from": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"bucket"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"to",
|
||||
"from"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": {
|
||||
"get": {
|
||||
"operationId": "ListFolder",
|
||||
|
|
@ -2070,6 +2212,9 @@
|
|||
"upload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"keyword": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
|
@ -2111,6 +2256,7 @@
|
|||
"updatedBy",
|
||||
"updatedAt",
|
||||
"upload",
|
||||
"path",
|
||||
"keyword",
|
||||
"category",
|
||||
"description",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ export async function expressAuthentication(
|
|||
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
|
||||
}
|
||||
|
||||
if (scopes && !scopes.some((v) => payload.resource_access[payload.azp].roles.includes(v))) {
|
||||
if (
|
||||
scopes &&
|
||||
scopes.length > 0 &&
|
||||
scopes.some((v) => !payload.resource_access[payload.azp].roles.includes(v))
|
||||
) {
|
||||
throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,14 @@ esClient.indices.putMapping({
|
|||
pathname: {
|
||||
type: "keyword",
|
||||
},
|
||||
path: {
|
||||
type: "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
minioClient.makeBucket(DEFAULT_BUCKET!, (e) => {
|
||||
if (!e) console.log("Configuration needed for Bucket Notification to AMQP");
|
||||
if (!e) console.log("Success. Configuration is needed for Bucket Notification to AMQP.");
|
||||
console.error(e);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@
|
|||
"experimentalDecorators": true,
|
||||
|
||||
"skipLibCheck": true
|
||||
}
|
||||
},
|
||||
"exclude": ["./tools/**/*"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue