Merge branch 'develop'

# Conflicts:
#	src/views/MainLayout.vue
This commit is contained in:
Kittapath 2023-03-20 15:13:35 +07:00
commit 1de1f5da73
66 changed files with 6739 additions and 120 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
**/node_modules
**/dist
Dockerfile
docker-compose.yaml

23
.github/workflows/deploy.yaml vendored Normal file
View file

@ -0,0 +1,23 @@
name: deploy-docker
run-name: deploy-docker ${{ github.actor }}
on:
workflow_dispatch:
jobs:
# https://github.com/appleboy/ssh-action
# act -W .github/workflows/deploy.yaml -j remote-deploy -s SSH_PASSWORD
remote-deploy:
runs-on: ubuntu-latest
steps:
- name: Remote Deployment
uses: appleboy/ssh-action@v0.1.8
with:
host: frappet.com
username: frappet
password: ${{ secrets.SSH_PASSWORD }}
port: 22
script: |
cd /home/frappet/docker/bma-ehr-recruit-qualifying-exam
docker-compose pull
docker-compose up -d
touch success

34
.github/workflows/local-build.yaml vendored Normal file
View file

@ -0,0 +1,34 @@
name: local-build
run-name: local-build ${{ github.actor }}
on:
# push:
# tags:
# - v1.**
# branches:
# - 'main'
# branches:
# - 'release-*'
# Allow run workflow manually from Action tab
workflow_dispatch:
env:
REGISTRY: docker.frappet.com
CMS_IMAGE_NAME: demo/qualifying-exam-cms
CMS_IMAGE_TAG: 0.1.1
jobs:
# act -W .github/workflows/local-build.yaml -j local-image
local-image:
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: cms
load: true
tags: ${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:${{env.CMS_IMAGE_TAG}},${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:latest

72
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,72 @@
name: release
run-name: release ${{ github.actor }}
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
tags-ignore:
- '2.*'
# Allow run workflow manually from Action tab
workflow_dispatch:
inputs:
IMAGE_VER:
description: "version for build image"
type: string
env:
REGISTRY: docker.frappet.com
IMAGE_NAME: demo/test-fe-exam
jobs:
# act workflow_dispatch -W .github/workflows/release.yaml --input IMAGE_VER=v0.2.1-dev -s DOCKER_USER=sorawit -s DOCKER_PASS=P@ssword -s SSH_PASSWORD=P@ssw0rd
release:
runs-on: ubuntu-latest
steps:
# https://thekevinwang.com/2022/06/13/github-actions-survival-skills/
- name: Check out code # checkout only cms is possible but I checkout all
uses: actions/checkout@v3
- name: Gen Version
id: gen_ver
run: |
if [[ $GITHUB_REF == 'refs/tags/'* ]]; then
IMAGE_VER='${GITHUB_REF/refs\/tags\//}'
else
IMAGE_VER=${{ github.event.inputs.IMAGE_VER }}
fi
if [[ $IMAGE_VER == '' ]]; then
IMAGE_VER='beta'
fi
# echo "{\"version\":\"$IMAGE_VER\", \"builddate\":\"$(date +"%Y-%m-%d_%T")\",\"ref_name\":\"$GITHUB_REF\" }" > ./cms/src/lib/ver.json
# cat ./cms/src/lib/ver.json
echo '::set-output name=image_ver::'$IMAGE_VER
- name: Test Version
run: |
echo $GITHUB_REF
echo ${{ steps.gen_ver.outputs.image_ver }}
# cat ./cms/src/lib/ver.json
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{env.REGISTRY}}
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASS}}
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
# context: cms
# platforms: linux/amd64,linux/arm64
push: true
tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest
# - name: Remote Deployment
# uses: appleboy/ssh-action@v0.1.8
# with:
# host: frappet.com
# username: frappet
# password: ${{ secrets.SSH_PASSWORD }}
# port: 22
# script: |
# cd /home/frappet/docker/bma-ehr-recruit-qualifying-exam
# docker-compose pull
# docker-compose up -d
# touch success

55
.github/workflows/remote-build.yaml vendored Normal file
View file

@ -0,0 +1,55 @@
name: remote-build
run-name: remote-build ${{ github.actor }}
on:
# push:
# tags:
# - 'v[0-9]+.[0-9]+.[0-9]+'
# tags-ignore:
# - '2.*'
# Allow run workflow manually from Action tab
workflow_dispatch:
env:
REGISTRY: docker.frappet.com
CMS_IMAGE_NAME: demo/qualifying-exam-cms
CMS_IMAGE_TAG: 0.2.1
jobs:
# act --workflows .github/workflows/build.yaml --job remote-image -s DOCKER_USER -s DOCKER_PASS -s SSH_PASSWORD
# act -W .github/workflows/remote-build.yaml -j remote-image -s DOCKER_USER -s DOCKER_PASS -s SSH_PASSWORD
remote-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# skip Set up QEMU because it fail on act and container
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{env.REGISTRY}}
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASS}}
# Node no need because use 2 state build
# - uses: actions/setup-node@v3
# with:
# node-version: '18'
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: cms
# platforms: linux/amd64,linux/arm64
push: true
tags: ${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:${{env.CMS_IMAGE_TAG}},${{env.REGISTRY}}/${{env.CMS_IMAGE_NAME}}:latest
- name: Remote Deployment
uses: appleboy/ssh-action@v0.1.8
with:
host: frappet.com
username: frappet
password: ${{ secrets.SSH_PASSWORD }}
port: 22
script: |
cd /home/frappet/docker/bma-ehr-recruit-qualifying-exam
docker-compose pull
docker-compose up -d
touch success

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
# docker build . -t docker.frappet.com/demo/fe:latest
FROM node:latest as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build
FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

30
nginx.conf Normal file
View file

@ -0,0 +1,30 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

103
package-lock.json generated
View file

@ -9,6 +9,9 @@
"version": "0.0.0",
"dependencies": {
"@quasar/extras": "^1.15.11",
"@vuepic/vue-datepicker": "^4.2.1",
"keycloak-js": "^21.0.1",
"moment": "^2.29.4",
"pinia": "^2.0.32",
"quasar": "^2.11.7",
"vue": "^3.2.47",
@ -2031,6 +2034,21 @@
}
}
},
"node_modules/@vuepic/vue-datepicker": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-4.2.1.tgz",
"integrity": "sha512-O8hy0o9jQkv/Et7G0mUFDR94NTcHHouy70ELfrTmaWOIBamS/8cRWAwwb/reOvKPX+eo1XKs/v2mj+i5WA14kw==",
"dependencies": {
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -2354,7 +2372,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@ -3054,6 +3071,26 @@
"node": ">=14"
}
},
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"peerDependencies": {
"date-fns": ">=2.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
@ -5189,6 +5226,11 @@
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5383,6 +5425,15 @@
"verror": "1.10.0"
}
},
"node_modules/keycloak-js": {
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-21.0.1.tgz",
"integrity": "sha512-ot0KW4qyDHl5AyDZNV0CkEkuvIZi+37y3BReNvqqfag7wqJeV13R/PcgECvbbd05+0NSOQjhBL8S+a4A++vNQw==",
"dependencies": {
"base64-js": "^1.5.1",
"js-sha256": "^0.9.0"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
@ -5801,6 +5852,14 @@
"ufo": "^1.1.1"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -9601,6 +9660,15 @@
"dev": true,
"requires": {}
},
"@vuepic/vue-datepicker": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-4.2.1.tgz",
"integrity": "sha512-O8hy0o9jQkv/Et7G0mUFDR94NTcHHouy70ELfrTmaWOIBamS/8cRWAwwb/reOvKPX+eo1XKs/v2mj+i5WA14kw==",
"requires": {
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7"
}
},
"abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -9842,8 +9910,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
@ -10348,6 +10415,17 @@
"whatwg-url": "^12.0.0"
}
},
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
},
"date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"requires": {}
},
"dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
@ -11909,6 +11987,11 @@
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
"dev": true
},
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -12065,6 +12148,15 @@
"verror": "1.10.0"
}
},
"keycloak-js": {
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-21.0.1.tgz",
"integrity": "sha512-ot0KW4qyDHl5AyDZNV0CkEkuvIZi+37y3BReNvqqfag7wqJeV13R/PcgECvbbd05+0NSOQjhBL8S+a4A++vNQw==",
"requires": {
"base64-js": "^1.5.1",
"js-sha256": "^0.9.0"
}
},
"lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
@ -12379,6 +12471,11 @@
"ufo": "^1.1.1"
}
},
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

View file

@ -16,6 +16,9 @@
},
"dependencies": {
"@quasar/extras": "^1.15.11",
"@vuepic/vue-datepicker": "^4.2.1",
"keycloak-js": "^21.0.1",
"moment": "^2.29.4",
"pinia": "^2.0.32",
"quasar": "^2.11.7",
"vue": "^3.2.47",

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

@ -0,0 +1,34 @@
/**config api */
import { ref } from "vue";
const env = ref<string>(process.env.NODE_ENV || "development");
// if (process.env.VUE_APP_TEST) {
// env = "test";
// }
const config = ref<any>({
development: {
API_URI: "https://localhost:7006/api/v1",
// API_URI: "https://wsh.frappet.com",
MEET_URI: "meet.frappet.com",
},
test: {
API_URI: "http://localhost:5010/api/v1",
MEET_URI: "meet.frappet.com",
},
production: {
// API_URI: "https://localhost:5010",
API_URI: `${window.location.protocol}//api-${window.location.host}/api/v1`,
MEET_URI: "meet.frappet.com",
},
});
const API_URI = ref<string>(config.value[env.value].API_URI);
const MEET_URI = ref<string>(config.value[env.value].MEET_URI);
export default {
env: env.value,
config: config.value,
API_URI: API_URI.value,
MEET_URI: MEET_URI.value,
};

View file

@ -0,0 +1,7 @@
import env from '../index'
const dashbord = `${env.API_URI}/dashbord/`
export default {
countDashbordSubHistory: (type: number) => `${dashbord}${type}`,
countDashbordHistory: `${dashbord}`
}

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

@ -0,0 +1,11 @@
/**ใช้รวมไฟล์ย่อยๆ ของ api แต่ละไฟล์ */
import manageOrganization from './api/manage/api.organization'
const API = {
...manageOrganization
}
export default {
API: API
}

View file

@ -0,0 +1,128 @@
<template>
<q-card-actions class="text-primary q-py-sm">
<q-btn
flat
round
icon="mdi-menu-left"
@click="clickPrevious"
v-if="modalEdit == true"
:disable="previous == false"
:color="!previous ? 'grey-7' : 'public'"
/>
<q-btn
flat
round
icon="mdi-menu-right"
@click="clickNext"
v-if="modalEdit == true"
:disable="next == false"
:color="!next ? 'grey-7' : 'public'"
/>
<q-space />
<div v-if="editBtn">
<q-btn
v-if="!editvisible"
flat
round
:disabled="editvisible"
:color="editvisible ? 'grey-7' : 'primary'"
@click="edit"
icon="mdi-pencil-outline"
>
<q-tooltip>แกไขขอม</q-tooltip>
</q-btn>
<div v-else>
<!-- <q-btn
flat
round
:disabled="!editvisible"
:outline="!editvisible"
:color="!editvisible ? 'grey-7' : 'red'"
@click="cancel()"
icon="mdi-undo"
v-if="modalEdit == true"
>
<q-tooltip>ยกเล</q-tooltip>
</q-btn> -->
<q-btn
flat
round
:disabled="!editvisible"
:color="!editvisible ? 'grey-7' : 'public'"
@click="checkSave"
icon="mdi-content-save-outline"
>
<q-tooltip>นท</q-tooltip>
</q-btn>
</div>
</div>
</q-card-actions>
</template>
<script setup lang="ts">
import { ref, useAttrs } from 'vue'
const props = defineProps({
editvisible: Boolean,
next: Boolean,
previous: Boolean,
modalEdit: Boolean,
editBtn: {
type: Boolean,
default: true
},
clickNext: {
type: Function,
default: () => console.log('not function')
},
clickPrevious: {
type: Function,
default: () => console.log('not function')
},
cancel: {
type: Function,
default: () => console.log('not function')
},
edit: {
type: Function,
default: () => console.log('not function')
},
save: {
type: Function,
default: () => console.log('not function')
},
validate: {
type: Function,
default: () => console.log('not function')
}
})
const emit = defineEmits(['update:editvisible', 'update:next', 'update:previous'])
const updateEdit = (value: Boolean) => {
emit('update:editvisible', value)
}
const cancel = async () => {
props.cancel()
}
const edit = async () => {
updateEdit(!props.editvisible)
props.edit()
}
const checkSave = () => {
props.validate()
props.save()
// if (myForm.value !== null) {
// myForm.value.validate().then((success) => {
// if (success) {
// }
// });
// }
}
const clickNext = async () => {
await props.clickNext()
}
const clickPrevious = async () => {
await props.clickPrevious()
}
</script>

View file

@ -0,0 +1,32 @@
<template>
<q-card-section class="row items-center col-12 q-py-sm">
<div class="row col-11">
<div class="text-bold">{{ tittle }}</div>
</div>
<q-space />
<!-- <div class="row col-1"> -->
<q-btn
icon="close"
unelevated
round
dense
@click="close"
style="color: #ff8080; background-color: #ffdede"
/>
<!-- </div> -->
</q-card-section>
</template>
<script setup lang="ts">
import { ref, useAttrs } from "vue";
const props = defineProps({
tittle: String,
close: {
type: Function,
default: () => console.log("not function"),
},
});
const close = async () => {
props.close();
};
</script>

View file

@ -0,0 +1,86 @@
<template>
<q-dialog :model-value="modal" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row items-center">
<div class="q-pr-md">
<q-avatar
icon="mdi-alert-circle-outline"
font-size="25px"
size="lg"
color="primary-1"
text-color="primary"
/>
</div>
<div class="col text-dark">
<span class="text-bold">{{ modalTittle }}</span>
<br />
<span>{{ modalDetail }}</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn label="ยกเลิก" color="primary" @click="cancel" />
<q-btn label="ตกลง" color="primary" @click="ok" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, useAttrs } from 'vue'
const props = defineProps({
modal: Boolean,
modalTittle: String,
modalDetail: String,
ok: {
type: Function,
default: () => console.log('not function')
},
cancel: {
type: Function,
default: () => console.log('not function')
}
})
const emit = defineEmits(['update:modal'])
const cancel = () => {
emit('update:modal', false)
props.cancel()
}
const ok = () => {
emit('update:modal', false)
props.ok()
}
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
max-height: 64vh;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f6f6f6ae;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

View file

@ -0,0 +1,92 @@
<template>
<q-dialog
:model-value="modalError"
persistent
@update:model-value="updateClose"
>
<q-card class="q-pa-sm">
<q-card-section class="row items-center">
<div class="q-pr-md">
<q-avatar
icon="mdi-alert-circle-outline"
font-size="25px"
size="lg"
color="red-1"
text-color="red"
/>
</div>
<div class="col text-dark">
<span class="text-bold">{{ modalErrorTittle }}</span>
<br />
<span>{{ modalErrorDetail }}</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn
label="ตกลง"
color="primary"
@click="updateClose"
v-close-popup
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, useAttrs } from "vue";
const props = defineProps({
modalError: Boolean,
modalErrorTittle: String,
modalErrorDetail: String,
close: {
type: Function,
default: () => console.log("not function"),
},
});
const emit = defineEmits([
"update:modalError",
"update:modalErrorTittle",
"update:modalErrorDetail",
]);
const updateClose = () => {
emit("update:modalError", false);
emit("update:modalErrorTittle", "");
emit("update:modalErrorDetail", "");
props.close();
};
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
max-height: 64vh;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f6f6f6ae;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

222
src/components/Table.vue Normal file
View file

@ -0,0 +1,222 @@
<template>
<div class="q-pb-sm row">
<!-- -->
<HeaderTop
v-model:edit="editBtn"
:header="name"
:icon="icon"
:add="clickAdd"
:editBtn="clickEdit"
:cancel="clickCancel"
:history="false"
:addData="false"
/>
<!-- v-if="nameHeader" -->
<!-- <div class="q-pl-sm">
<q-btn size="12px" flat round color="add" @click="add" icon="mdi-plus" v-if="addData">
<q-tooltip>เพมขอม</q-tooltip>
</q-btn>
</div> -->
<q-space />
<div class="items-center" style="display: flex">
<!-- นหาขอความใน table -->
<q-input
standout
dense
:model-value="inputfilter"
ref="filterRef"
@update:model-value="updateInput"
outlined
debounce="300"
placeholder="ค้นหา"
style="max-width: 200px"
class="q-ml-sm"
>
<template v-slot:append>
<q-icon v-if="inputfilter == ''" name="search" />
<q-icon
v-if="inputfilter !== ''"
name="clear"
class="cursor-pointer"
@click="resetFilter"
/>
</template>
</q-input>
<!-- แสดงคอลมนใน table -->
<q-select
:model-value="inputvisible"
@update:model-value="updateVisible"
:display-value="$q.lang.table.columns"
multiple
outlined
dense
:options="attrs.columns"
options-dense
option-value="name"
map-options
emit-value
style="min-width: 150px"
class="gt-xs q-ml-sm"
/>
</div>
</div>
<q-table
ref="table"
flat
bordered
class="custom-header-table"
v-bind="attrs"
virtual-scroll
:virtual-scroll-sticky-size-start="48"
dense
:pagination-label="paginationLabel"
:pagination="initialPagination"
:rows-per-page-options="[0]"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<span class="text-weight-medium">{{ col.label }}</span>
</q-th>
<q-th auto-width v-if="editBtn == true" />
</q-tr>
</template>
<template #body="props">
<slot v-bind="props" name="columns"></slot>
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue'
import HeaderTop from '@/components/top.vue'
import type { Pagination } from '@/modules/01_exam/interface/index/Main'
const attrs = ref<any>(useAttrs())
const table = ref<any>(null)
const filterRef = ref<any>(null)
const editBtn = ref<boolean>(false)
const initialPagination = ref<Pagination>({
// descending: false,
rowsPerPage: 0
})
const props = defineProps({
inputfilter: String,
name: String,
icon: String,
inputvisible: Array,
editvisible: Boolean,
nameHeader: Boolean,
addData: {
type: Boolean,
defualt: true
},
edit: {
type: Function,
default: () => console.log('not function')
},
add: {
type: Function,
default: () => console.log('not function')
},
cancel: {
type: Function,
default: () => console.log('not function')
},
validate: {
type: Function,
default: () => console.log('not function')
}
})
const emit = defineEmits(['update:inputfilter', 'update:inputvisible', 'update:editvisible'])
watch(props, (count: any, prevCount: any) => {
editBtn.value = props.editvisible
})
const updateEdit = (value: Boolean) => {
emit('update:editvisible', value)
}
const updateInput = (value: string | number | null) => {
emit('update:inputfilter', value)
}
const updateVisible = (value: []) => {
emit('update:inputvisible', value)
}
const paginationLabel = (start: string, end: string, total: string) => {
return start + '-' + end + ' ใน ' + total
}
const clickAdd = () => {
// props.validate();
props.add()
}
const clickEdit = () => {
// props.validate();
props.edit()
}
const clickCancel = () => {
// props.validate();
props.cancel()
}
const edit = async () => {
updateEdit(!props.editvisible)
props.edit()
}
const cancel = async () => {
updateEdit(!props.editvisible)
props.cancel()
}
const resetFilter = () => {
// reset X
emit('update:inputfilter', '')
filterRef.value.focus()
}
const add = () => {
props.add()
}
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
max-height: 64vh;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f6f6f6ae;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

View file

@ -0,0 +1,481 @@
<template>
<div class="q-px-md q-pb-md">
<!-- header บน table นหา แสดงคอลมน มแกไข เพ เผยแพรอม ยกเล (status nornmalData false) -->
<div class="col-12 row q-py-sm" v-if="nornmalData == false">
<q-btn
v-if="!editvisible == true && publicNoBtn == false"
flat
round
:disabled="editvisible == true"
:color="editvisible == true ? 'grey-7' : 'primary'"
@click="edit"
icon="mdi-pencil-outline"
>
<q-tooltip>แกไขขอม</q-tooltip>
</q-btn>
<!-- ยกเล แสดงเม กดปมแกไข -->
<q-btn
v-else
flat
round
:disabled="editvisible == false"
:outline="editvisible == false"
:color="editvisible == false ? 'grey-7' : 'red'"
@click="cancel()"
icon="mdi-undo"
>
<q-tooltip>ยกเล</q-tooltip>
</q-btn>
<!-- <q-separator vertical /> -->
<div class="q-px-sm">
<q-btn
flat
round
:disabled="editvisible == false"
:color="editvisible == false ? 'grey-7' : 'add'"
@click="add"
icon="mdi-plus"
>
<q-tooltip>เพมขอม</q-tooltip>
</q-btn>
<!-- นทกราง แสดงเม กดปมแกไข อมลมการเปนแปลงหร งไมเผยแพรอม -->
<q-btn
flat
round
:disabled="!(editvisible == true && updateData == true)"
:color="
!(editvisible == true && updateData == true) ? 'grey-7' : 'public'
"
@click="checkSave"
v-if="saveNoDraft == false"
icon="mdi-content-save-outline"
>
<q-tooltip>นทกราง</q-tooltip>
</q-btn>
<!-- ลบบนทกราง แสดงเม นทกรางแล -->
<q-btn
flat
round
:disabled="publicData == true"
:color="publicData == true ? 'grey-7' : 'deep-orange'"
@click="DeleteModal"
icon="mdi-file-remove-outline"
v-if="publicNoBtn == false"
>
<q-tooltip>ลบบนทกราง</q-tooltip>
</q-btn>
</div>
<!-- เผยแพร -->
<q-btn
flat
round
:disabled="!(publicData == false || updateData == true)"
:color="
!(publicData == false || updateData == true) ? 'grey-7' : 'public'
"
@click="publishModal"
icon="mdi-cloud-upload-outline"
v-if="publicNoBtn == false"
>
<q-tooltip>เผยแพร</q-tooltip>
</q-btn>
<div class="items-center" style="display: flex">
<div
class="row items-center"
style="display: flex"
v-if="publicData == false && publicNoBtn == false"
>
<div class="text-public text-body2 text-weight-medium q-px-sm">
อมลยงไมเผยแพร
</div>
</div>
</div>
<q-space />
<div class="items-center" style="display: flex">
<!-- อความสถานะเผยแพร โดยใช parameter publicData เปนตวกำหนดขอความ -->
<!-- <div class="row items-center" style="display: flex" v-if="publicData == true">
<q-icon cener size="20px" name="label_important" class="icon-color" />
<div class="text-size">อมลเผยแพรแล</div>
</div> -->
<div
class="row items-center"
style="display: flex"
v-if="publicData == false && publicNoBtn == false"
>
<!-- <q-icon cener size="20px" name="label_important" color="amber" />
<div class="text-grey-7 text-body2 text-weight-medium q-px-sm">
อมลยงไมเผยแพร
</div> -->
</div>
<!-- นหาขอความใน table -->
<q-input
standout
dense
:model-value="inputfilter"
ref="filterRef"
@update:model-value="updateInput"
outlined
debounce="300"
placeholder="ค้นหา"
style="max-width: 200px"
class="q-ml-sm"
>
<template v-slot:append>
<q-icon v-if="inputfilter == ''" name="search" />
<q-icon
v-if="inputfilter !== ''"
name="clear"
class="cursor-pointer"
@click="resetFilter"
/>
</template>
</q-input>
<!-- แสดงคอลมนใน table -->
<q-select
:model-value="inputvisible"
@update:model-value="updateVisible"
:display-value="$q.lang.table.columns"
multiple
outlined
dense
:options="attrs.columns"
options-dense
option-value="name"
map-options
emit-value
style="min-width: 150px"
class="gt-xs q-ml-sm"
/>
</div>
</div>
<!-- header บน table นหา แสดงคอลมน (status nornmalData true) -->
<div class="col-12 row q-py-sm" v-if="nornmalData == true">
<q-space />
<div class="items-center" style="display: flex">
<!-- นหาขอความใน table -->
<q-input
standout
dense
:model-value="inputfilter"
ref="filterRef"
@update:model-value="updateInput"
outlined
debounce="300"
placeholder="ค้นหา"
style="max-width: 200px"
class="q-ml-sm"
>
<template v-slot:append>
<q-icon v-if="inputfilter == ''" name="search" />
<q-icon
v-if="inputfilter !== ''"
name="clear"
class="cursor-pointer"
@click="resetFilter"
/>
</template>
</q-input>
<!-- แสดงคอลมนใน table -->
<q-select
:model-value="inputvisible"
@update:model-value="updateVisible"
:display-value="$q.lang.table.columns"
multiple
outlined
dense
:options="attrs.columns"
options-dense
option-value="name"
map-options
emit-value
style="min-width: 150px"
class="gt-xs q-ml-sm"
/>
</div>
</div>
<q-table
ref="table"
flat
bordered
class="custom-header-table"
v-bind="attrs"
virtual-scroll
:virtual-scroll-sticky-size-start="48"
dense
:pagination-label="paginationLabel"
:pagination="initialPagination"
:rows-per-page-options="paging == true ? [25, 50, 100, 500] : []"
>
<!-- :pagination="initialPagination" -->
<!-- :rows-per-page-options="[0]" -->
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width v-if="boss == true" />
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<span class="text-weight-medium">{{ col.label }}</span>
</q-th>
<q-th
auto-width
v-if="
editvisible == true || nextPageVisible == true || history == true
"
/>
</q-tr>
</template>
<!-- สำหรบเรยกใช template วขางนอก -->
<template #body="props">
<slot v-bind="props" name="columns"></slot>
</template>
</q-table>
</div>
<!-- อมลการเผยแพรอม -->
<q-dialog v-model="modalPublish" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
icon="public"
size="lg"
font-size="25px"
color="blue-1"
text-color="public"
/>
</div>
<div class="col text-dark">
<span class="text-bold">องการเผยแพรอมลนหรอไม?</span>
<br />
<span>อมลทกำลงถกเผยแพรจะมผลใชงานทนท</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn label="ยกเลิก" flat color="grey-8" v-close-popup />
<q-btn
label="เผยแพร่"
color="public"
@click="publish()"
v-close-popup
/>
</q-card-actions>
</q-card>
</q-dialog>
<!-- อมลการลบเผยแพรอม -->
<q-dialog v-model="modalDelete" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
icon="mdi-file-remove-outline"
size="lg"
font-size="25px"
color="red-1"
text-color="deep-orange"
/>
</div>
<div class="col text-dark">
<span class="text-bold">องการลบขอมลบนทกรางนหรอไม?</span>
<br />
<span>อมลบนทกรางทกำลงถกลบนจะมผลใชงานทนท</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn label="ยกเลิก" flat color="grey-8" v-close-popup />
<q-btn label="ลบบันทึก" color="red" @click="deleted()" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, useAttrs } from "vue";
const attrs = ref<any>(useAttrs());
const table = ref<any>(null);
const filterRef = ref<any>(null);
const modalPublish = ref<boolean>(false);
const modalDelete = ref<boolean>(false);
const props = defineProps({
inputfilter: String,
inputvisible: Array,
editvisible: Boolean,
boss: {
type: Boolean,
defualt: false,
},
saveNoDraft: {
type: Boolean,
defualt: false,
},
history: {
type: Boolean,
defualt: false,
},
paging: {
type: Boolean,
defualt: false,
},
nornmalData: {
type: Boolean,
defualt: false,
},
nextPageVisible: {
type: Boolean,
defualt: false,
},
publicData: {
type: Boolean,
defualt: true,
required: false,
},
updateData: {
type: Boolean,
defualt: true,
required: false,
},
publicNoBtn: {
type: Boolean,
defualt: false,
},
add: {
type: Function,
default: () => console.log("not function"),
},
edit: {
type: Function,
default: () => console.log("not function"),
},
save: {
type: Function,
default: () => console.log("not function"),
},
deleted: {
type: Function,
default: () => console.log("not function"),
},
cancel: {
type: Function,
default: () => console.log("not function"),
},
publish: {
type: Function,
default: () => console.log("not function"),
},
validate: {
type: Function,
default: () => console.log("not function"),
},
});
const initialPagination = ref<any>({
// descending: false,
rowsPerPage: props.paging == true ? 25 : 0,
});
const emit = defineEmits([
"update:inputfilter",
"update:inputvisible",
"update:editvisible",
]);
const updateEdit = (value: any) => {
emit("update:editvisible", value);
};
const updateInput = (value: any) => {
emit("update:inputfilter", value);
};
const updateVisible = (value: any) => {
emit("update:inputvisible", value);
};
const paginationLabel = (start: string, end: string, total: string) => {
if (props.paging == true)
return " " + start + " ใน " + end + " จากจำนวน " + total + " รายการ";
else return start + "-" + end + " ใน " + total;
};
const checkSave = () => {
props.validate();
props.save();
// if (myForm.value !== null) {
// myForm.value.validate().then((success) => {
// if (success) {
// }
// });
// }
};
const publishModal = () => {
props.validate();
const filter = attrs.value.rows.filter((r: any) => r.name == "");
if (filter.length == 0 || attrs.value.rows.length == 0) {
modalPublish.value = true;
}
};
const DeleteModal = () => {
modalDelete.value = true;
};
const edit = async () => {
updateEdit(!props.editvisible);
props.edit();
};
const add = async () => {
// if (myForm.value !== null) {
// myForm.value.validate();
// }
props.validate();
props.add();
await table.value.lastPage();
await table.value.scrollTo(attrs.value.rows.length - 1);
};
const deleted = async () => {
if (props.publicNoBtn === false) {
updateEdit(false);
}
props.deleted();
};
const resetFilter = () => {
// reset X
emit("update:inputfilter", "");
filterRef.value.focus();
};
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
max-height: 64vh;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f6f6f6ae;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

165
src/components/top.vue Normal file
View file

@ -0,0 +1,165 @@
<template>
<div class="flex items-center">
<div class="flex items-center" v-if="header != ''">
<q-icon :name="icon" size="1.5em" color="grey-5" class="q-mr-md" />
<div class="text-weight-medium text-dark col-12 row items-center text-header">
{{ header }}
</div>
</div>
<div class="q-gutter-sm q-mx-sm" v-if="addData == true">
<q-btn
size="12px"
v-if="!edit"
flat
round
:disabled="edit"
:color="edit ? 'grey-7' : 'primary'"
@click="ClickEdit"
icon="mdi-pencil-outline"
>
<q-tooltip>แกไขขอม</q-tooltip>
</q-btn>
<q-btn
size="12px"
flat
round
v-if="edit"
:disabled="!edit"
:color="!edit ? 'grey-7' : 'public'"
@click="save"
icon="mdi-content-save-outline"
>
<q-tooltip>นทกขอม</q-tooltip>
</q-btn>
<q-btn
size="12px"
flat
round
v-if="edit"
:disabled="!edit"
:color="!edit ? 'grey-7' : 'red'"
@click="ClickCancel"
icon="mdi-undo"
>
<q-tooltip>ยกเล</q-tooltip>
</q-btn>
</div>
<div class="q-pl-sm" v-else>
<q-btn
size="12px"
v-if="!edit"
flat
round
:disabled="edit"
:color="edit ? 'grey-7' : 'primary'"
@click="ClickEdit"
icon="mdi-pencil-outline"
>
<q-tooltip>แกไขขอม</q-tooltip>
</q-btn>
<q-btn
size="12px"
flat
round
v-if="edit"
:disabled="!edit"
:color="!edit ? 'grey-7' : 'add'"
@click="add"
icon="mdi-plus"
>
<q-tooltip>เพมขอม</q-tooltip>
</q-btn>
<q-btn
size="12px"
flat
round
v-if="edit"
:disabled="!edit"
:color="!edit ? 'grey-7' : 'red'"
@click="ClickCancel"
icon="mdi-undo"
>
<q-tooltip>ยกเล</q-tooltip>
</q-btn>
</div>
<q-space />
</div>
</template>
<script setup lang="ts">
const props = defineProps({
header: {
type: String,
default: '',
required: true
},
icon: {
type: String,
default: '',
required: true
},
edit: {
type: Boolean,
default: true,
required: true
},
addData: {
type: Boolean,
defualt: true
},
add: {
type: Function,
default: () => console.log('not function')
},
save: {
type: Function,
default: () => console.log('not function')
},
deleted: {
type: Function,
default: () => console.log('not function')
},
cancel: {
type: Function,
default: () => console.log('not function')
},
editBtn: {
type: Function,
default: () => console.log('not function')
},
changeBtn: {
type: Function,
default: () => console.log('not function')
}
})
const emit = defineEmits(['update:edit'])
const updateEdit = (value: any) => {
emit('update:edit', value)
}
const ClickEdit = () => {
updateEdit(!props.edit)
props.editBtn()
props.changeBtn()
}
const ClickCancel = () => {
updateEdit(!props.edit)
props.cancel()
props.changeBtn()
}
const save = () => {
props.save()
}
const add = () => {
props.add()
}
</script>
<style scoped>
/* .q-btn >>> .q-icon {
font-size: 20px;
} */
</style>

View file

@ -33,15 +33,8 @@ const menuList = readonly<menuType[]>([
key: 2,
icon: 'o_person',
activeIcon: 'person',
label: 'ข้อมูลหลัก01',
path: 'meta01'
},
{
key: 3,
icon: 'o_person',
activeIcon: 'person',
label: 'ข้อมูลหลัก02',
path: 'meta02'
label: 'รายการสอบทั้งหมด',
path: 'exam'
}
])
@ -70,7 +63,7 @@ const notiList = readonly<notiType[]>([
{
id: 1,
sender: 'ท',
body: 'ขอแก้ไขข้อมูลทะเบียนประวัติ',
body: 'ขอแก้ไขข้อมูลรายการสอบทั้งหมด',
timereceive: '13/12/2565'
}
])

View file

@ -1,21 +1,23 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { Dialog, Notify, Quasar } from 'quasar'
import { createApp, defineAsyncComponent } from 'vue'
import App from './App.vue'
import router from './router'
import { Dialog, Notify, Quasar } from 'quasar'
import quasarUserOptions from './quasar-user-options'
import 'quasar/src/css/index.sass'
import th from 'quasar/lang/th'
import '@vuepic/vue-datepicker/dist/main.css'
import http from './plugins/http'
import { createPinia } from 'pinia'
// import './assets/main.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(pinia)
app.use(
Quasar,
@ -30,8 +32,27 @@ app.use(
}
},
lang: th
},
quasarUserOptions
}
// quasarUserOptions
)
app.component(
'data-table',
defineAsyncComponent(() => import('./components/TableView.vue'))
)
app.component(
'notifyError',
defineAsyncComponent(() => import('./components/NotifyError.vue'))
)
app.component(
'datepicker',
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
)
app.component(
'full-loader',
defineAsyncComponent(() => import('./plugins/FullLoader.vue'))
)
app.config.globalProperties.$http = http
app.mount('#app')

View file

@ -0,0 +1,40 @@
<template>
<div class="q-pa-md shadow-4 rounded-borders cursor-pointer">
<label class="text-h6 row cursor-pointer">{{ items.title }}</label>
<label class="text-grey-7 row text-size cursor-pointer"
>ประกาศวนท {{ date2Thai(items.announcementDate) }}</label
>
<label class="text-grey-7 row q-my-md text-size cursor-pointer"
>ครงท {{ items.registerRound }}</label
>
<div class="row justify-between items-end cursor-pointer">
<label class="text-grey-7 text-size row cursor-pointer"
>เปดรบสมคร {{ date2Thai(items.registerDateStart) }} -
{{ date2Thai(items.registerDateEnd) }}</label
>
<div class="rounded-borders q-pa-sm bg-teal cursor-pointer shadow-2">
<label class="text-semi text-white text-size cursor-pointer">สมครสอบ</label>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import type { ExamCard } from '../interface/index/Main'
import { useCounterMixin } from '@/stores/mixin'
const mixin = useCounterMixin()
const { date2Thai } = mixin
const props = defineProps({
items: {
type: Object as PropType<ExamCard>,
required: true
}
})
</script>
<style lang="scss" scope>
.text-size {
font-size: 1.2em;
}
</style>

View file

@ -0,0 +1,102 @@
<template>
<div class="q-px-md">
<q-card flat bordered class="col-12 q-px-lg q-py-md q-mt-md">
<q-card-section>
<div class="text-h6 q-py-sm">{{ exam.displayText }} ({{ exam.description }})</div>
<div class="text-subtitle2 q-py-sm">ประกาศวนท {{ date2Thai(exam.createdUtc) }}</div>
<div class="text-subtitle2 q-py-sm">ประจำปงบประมาณ {{ exam.budgetYear }}</div>
<div class="text-subtitle2 q-py-sm">
เปดรบสมครวนท {{ date2Thai(exam.startDate) }} - {{ date2Thai(exam.endDate) }}
</div>
<div class="text-subtitle2 q-py-sm">าธรรมเนยมการสมคร {{ exam.fee }} บาท</div>
<!-- <div v-html="exam.htmlBody.html"></div> -->
</q-card-section>
<div class="text-subtitle1 text-center text-primary">เอกสารเกยวกบการสอบน</div>
<div class="row justify-start q-pa-md q-col-gutter-md">
<div
v-for="file in exam.document"
:key="file.contentItemId"
class="col-12"
:href="`https://localhost:5001${file.file}`"
>
<p>
<a :href="`https://localhost:5001${file.file}`" target="_blank">{{
file.displayText
}}</a
><br />
</p>
</div>
</div>
<!-- <template v-slot:body-cell-name="props">
<q-td :props="props">
<div>
<a href="https://quasar.dev/vue-components/table#QTable-API" />
</div>
</q-td>
</template> -->
</q-card>
<div class="col-12">
<div class="text-h6 q-py-sm">อควรระว</div>
<div class="text-warning q-pb-md">1. สมครสามารถสมครไดเพยงครงเดยว</div>
<div class="text-grey-7 q-pb-md">
2. และนามสก านกรอกลงใน "ใบสมัครออนไลน์"
จะตองเปนชอทตรงกบบตรประจำตวประชาชน
</div>
<div class="text-grey-7 q-pb-md">
3. กรณาตรวจสอบเลขประจำตวประชาชนของทานใหกตอง
เนองจากเลขประจำตวประชาชนของทานจะใชางองตลอดการสอบ
</div>
<div class="text-grey-7 q-pb-md">
4. และนามสก านกรอกลงใน "ใบสมัครออนไลน์"
จะตองเปนชอทตรงกบบตรประจำตวประชาชน
</div>
</div>
<div class="row justify-center q-pa-md">
<q-btn color="primary" class="q-ml-md" label="ต่อไป" @click="applyCandidate" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
const props = defineProps({
fetchStep: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const mixin = useCounterMixin()
const { date2Thai } = mixin
const exam = ref<any>({
contentItemId: null,
displayText: 'การสอบภาค ข.พิเศษ สำหรับผู้สอบผ่านของ่วนราชการแล้ว',
description: 'ครั้งที่2',
createdUtc: new Date(),
budgetYear: 2566,
startDate: new Date(),
endDate: new Date(),
fee: 200,
document: [
{
contentItemId: 111,
file: '111',
displayText: 'เอกสารเพิ่มเติมเกี่ยวกับการสอบ'
}
]
})
const applyCandidate = () => {
props.fetchStep()
}
</script>
<style scoped></style>

View file

@ -0,0 +1,103 @@
<template>
<div>
<div class="q-pa-md row items-center justify-center">
<q-card class="my-card" style="max-width: 600px; width: 100%" bordered>
<q-card-actions class="q-pa-md" :class="getClass(status)">
<div v-if="status == false" class="text-black text-bold">
เจาหนาทกำลงตรวจสถานทสอบ
</div>
<div v-else class="text-black text-bold">สมครสอบสำเร</div>
<q-space />
<q-btn size="12px" flat round color="primary" @click="download" icon="mdi-download">
<q-tooltip>มพตรประจำตวผสอบ</q-tooltip>
</q-btn>
</q-card-actions>
<q-separator />
<q-card-section horizontal>
<q-card-section class="col-8 q-pt-xs">
<div class="text-bold">ตรประจำตวผสอบ</div>
<div class="q-pt-xs row">
<div class=""> :</div>
<div class="text-black text-bold">{{ fullName }}</div>
</div>
<div class="q-pt-xs row">
<div class="">เลขประจำตวสอบ :</div>
<div class="text-black text-bold">{{ examNumber }}</div>
</div>
<div class="q-pt-xs row">
<div class="">เลขประจำตวประชาชน :</div>
<div class="">{{ citizenId }}</div>
</div>
<div class="q-pt-xs row">
<div class="">เวลาสอบ :</div>
<div class="">{{ examTime }}</div>
</div>
<div class="q-pt-xs row">
<div class="">สถานทสอบ :</div>
<div class="">{{ examLocation }}</div>
</div>
<div class="q-pt-xs row">
<div class="">นท :</div>
<div class="">{{ floor }}</div>
</div>
<div class="q-pt-xs row">
<div class="">องสอบ :</div>
<div class="">{{ examRoom }}</div>
</div>
<div class="q-pt-xs row">
<div class="">เลขท :</div>
<div class="">{{ seatNumber }}</div>
</div>
</q-card-section>
<q-card-section class="col-4 flex flex-center">
<q-img
class="rounded-borders"
src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cGVyc29ufGVufDB8fDB8fA%3D%3D&w=1000&q=80"
:ratio="1"
style="max-width: 300px; max-height: 300px"
/>
</q-card-section>
</q-card-section>
</q-card>
</div>
<!-- <q-btn color="positive" @click="testChangeStatus">จัดที่นั่ง</q-btn> -->
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const props = defineProps({
fetchStep: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const fullName = ref<string>('นางพิมพ์ภา วงศ์สวัสดิ์')
const examNumber = ref<string>('CDE-004')
const citizenId = ref<string>('1100700954521')
const examTime = ref<string>('09:00-12:00')
const examLocation = ref<string>('ศูนย์สอบ 01 กรุงเทพฯและนนทบุรี')
const floor = ref<string>('4')
const examRoom = ref<string>('CB-706')
const seatNumber = ref<string>('75')
const status = ref<boolean>(true)
const download = () => {}
const testChangeStatus = () => {
status.value = true
}
const getClass = (val: boolean) => {
return val == true ? 'bg-green-4' : 'bg-yellow-3'
}
</script>
<style lang="scss" scoped></style>

View file

@ -0,0 +1,194 @@
<!-- step กรอกขอม -->
<template>
<div>
<q-tabs
v-model="tab"
dense
class=""
active-color="primary"
indicator-color="primary"
align="justify"
@update:model-value="changeTab"
>
<q-tab name="profile" label="ข้อมูลส่วนบุคคล" />
<q-tab name="education" label="ประวัติการศีกษา" />
<q-tab name="career" label="ประวัติการทำงาน/ฝึกงาน" />
<q-tab name="document" label="เอกสาร" />
</q-tabs>
<q-tab-panels v-model="tab" animated class="bg-white">
<!-- อมลสวนบคคล -->
<q-tab-panel name="profile">
<Profile :loader="loader" v-model:statusEdit="statusEdit" :step="step" />
</q-tab-panel>
<!-- ประวการศกษา -->
<q-tab-panel name="education"
><Education :loader="loader" v-model:statusEdit="statusEdit" :step="step"
/></q-tab-panel>
<!-- ประวการทำงาน/กงาน -->
<q-tab-panel name="career"
><Career :loader="loader" v-model:statusEdit="statusEdit" :step="step"
/></q-tab-panel>
<!-- ไฟล -->
<q-tab-panel name="document">
<Document :loader="loader" v-model:statusEdit="statusEdit" :step="step" />
</q-tab-panel>
</q-tab-panels>
<div>
<q-checkbox
v-model="acceptTermOfUse"
:disable="step !== 2"
v-if="step !== 2 || tab == 'document'"
label="ข้าพเจ้ารับรองว่า ข้าพเจ้ามีคุณสมบัติครบถ้วนตามประกาศรับสมัคร ข้อความข้างต้นตรงตามความจริงทุกประการ"
/>
</div>
<div class="row justify-center q-pa-md">
<q-btn
color="primary"
class="q-ml-md"
label="กลับ"
@click="clickPreview(tab)"
v-if="step === 2 && (tab == 'education' || tab == 'career' || tab == 'document')"
icon="mdi-chevron-left"
/>
<q-btn
color="primary"
class="q-ml-md"
label="ต่อไป"
@click="clickNext(tab)"
v-if="step === 2 && (tab == 'profile' || tab == 'education' || tab == 'career')"
icon-right="mdi-chevron-right"
/>
<q-btn
color="primary"
class="q-ml-md"
label="สมัครสอบ"
@click="okModalComfirm"
v-if="step === 2 && tab == 'document'"
:disable="!acceptTermOfUse"
/>
</div>
</div>
<NotifyConfirm
:modal="modalComfirm"
:modalTittle="modalComfirmTittle"
:modalDetail="modalComfirmDetail"
:ok="applyCandidate"
:cancel="cancelModalComfirm"
/>
<NotifyError
:modalError="modalNoEdit"
:modalErrorTittle="modalNoEditTittle"
:modalErrorDetail="modalNoEditDetail"
:close="closeModalError"
/>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import Profile from '@/modules/01_exam/components/Form/Profile.vue'
import Education from '@/modules/01_exam/components/Form/Education.vue'
import Career from '@/modules/01_exam/components/Form/Career.vue'
import Document from '@/modules/01_exam/components/Form/Document.vue'
import NotifyError from '@/components/NotifyError.vue'
import NotifyConfirm from '@/components/NotifyConfirm.vue'
const props = defineProps({
fetchStep: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const tab = ref<string>('profile')
const tabRaw = ref<string>('profile')
const loader = ref<boolean>(false)
const statusEdit = ref<boolean>(false)
const acceptTermOfUse = ref<boolean>(false)
const modalComfirm = ref<boolean>(false)
const modalComfirmTittle = ref<string>('ยืนยันการสมัครสอบ?')
const modalComfirmDetail = ref<string>('เมื่อยืนยันการสมัครสอบแล้วจะไม่สามารถแก้ไขข้อมูลได้')
const modalNoEdit = ref<boolean>(false)
const modalNoEditTittle = ref<string>('ไม่สามารถเปลี่ยนแท็ปได้?')
const modalNoEditDetail = ref<string>('มีข้อมูลที่ยังไม่ถูกบันทึกข้อมูล')
onMounted(async () => {
if (props.step > 2) {
acceptTermOfUse.value = true
}
})
const okModalComfirm = () => {
if (statusEdit.value == true) {
modalNoEditTittle.value = 'ไม่สามารถสมัครสอบได้?'
modalNoEditDetail.value = 'มีข้อมูลที่ยังไม่ถูกบันทึกข้อมูล'
modalNoEdit.value = true
} else {
modalComfirm.value = true
}
}
const cancelModalComfirm = () => {
modalComfirm.value = false
}
const closeModalError = () => {
modalNoEdit.value = false
}
const applyCandidate = () => {
props.fetchStep()
}
const clickPreview = (val: string) => {
console.log(val)
switch (val) {
case 'profile':
return (tab.value = 'profile')
case 'education':
return (tab.value = 'profile')
case 'career':
return (tab.value = 'education')
case 'document':
return (tab.value = 'career')
default:
return (tab.value = 'profile')
}
}
const clickNext = (val: string) => {
switch (val) {
case 'profile':
return (tab.value = 'education')
case 'education':
return (tab.value = 'career')
case 'career':
return (tab.value = 'document')
case 'document':
return (tab.value = 'profile')
default:
return (tab.value = 'profile')
}
}
const changeTab = () => {
if (statusEdit.value == true) {
modalNoEditTittle.value = 'ไม่สามารถเปลี่ยนแท็ปได้?'
modalNoEditDetail.value = 'มีข้อมูลที่ยังไม่ถูกบันทึกข้อมูล'
tab.value = tabRaw.value
modalNoEdit.value = true
} else {
tabRaw.value = tab.value
}
}
</script>
<style lang="scss" scoped></style>

View file

@ -0,0 +1,131 @@
<template>
<q-card class="my-card q-mb-sm" bordered>
<q-card-actions class="q-pa-md text-left row" :class="getClass(status)">
<div class="text-black text-bold col-12" style="font-size: 16px">
{{ message(status) }}
</div>
<div class="text-black col-12" v-if="status === 'rejected'">
<li>{{ rejectMessage }}</li>
</div>
<!-- <q-space /> -->
</q-card-actions>
</q-card>
<div class="d-flex justify-content-center align-items-center row col-12">
<!-- <div class="text-center q-pb-lg">
<q-badge
:outline="badgeOutline"
:color="badgeColor"
:label="badgeLabel"
class="q-pa-nond text-center"
style="font-size: 16px"
/>
</div> -->
<!-- <div class="row q-pa-nond text-center col-6"> -->
<!-- <q-img :src="img" fit="contain" style="max-height: 300px" class="col-6">
<div class="absolute-bottom text-center">หลกฐานชำระเง</div>
</q-img> -->
<q-file
borderless
v-model="fileData"
stack-label
@update:model-value="uploadImg"
class="col-12"
>
<!-- <q-img src="@/assets/avatar_user.jpg" class="col-12">
<div class="overlay" v-if="edit">
<q-icon name="mdi-camera" />
<br />ปเดต
</div>
</q-img> -->
<q-img :src="img" fit="contain" style="max-height: 300px" class="col-12">
<div class="absolute-bottom text-center">หลกฐานชำระเง</div>
</q-img>
</q-file>
<!-- <q-uploader
ref="uploader"
type="file"
:factory="uploadImg"
class="col-6"
label="กรุณาอัปโหลดหลักฐานการชำระเงิน"
flat
color="blue"
bordered
/> -->
<!-- </div> -->
<div class="q-pa-md text-center col-12">
<q-btn color="primary" @click="setStatus('processing')" label="ส่งหลักฐานการชำระเงิน" />
</div>
<div class="text-black text-center q-pb-lg col-12">
***าตองการเเกไขหลกฐานการโอนเงนกรณาอพโหลดซ***
</div>
<q-btn color="negative" @click="setStatus('rejected')">Re</q-btn>
<q-btn color="positive" @click="setStatus('success')">Su</q-btn>
<q-btn color="positive" @click="setStatus('next')">Ne</q-btn>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps({
fetchStep: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const status = ref<string>('')
const rejectMessage = ref<string>('กรุณาจ่ายเงินให้ครบตามจำนวน')
const img = ref<string>('https://cdn-icons-png.flaticon.com/512/2496/2496846.png')
const fileData = ref<any>()
const uploadImg = (file: any) => {
fileData.value = null
// img.value =
// 'https://s359.kapook.com/r/600/auto/pagebuilder/ba154685-db18-4ac7-b318-a4a2b15b9d4c.jpg'
img.value =
'https://www.bangkokbank.com/-/media/feature/page-content/bbl-corporate/image-carousel-slides/digital-banking/bualuang-mbanking/how-to-use/payment/others/7_en.png'
}
const setStatus = (val: string) => {
status.value = val
if (val == 'next') props.fetchStep()
}
const getClass = (val: string) => {
switch (val) {
case 'processing':
return 'bg-lime-12'
case 'rejected':
return 'bg-deep-orange-11'
case 'success':
return 'bg-green-12'
default:
return 'bg-blue-4'
}
}
const message = (val: string) => {
switch (val) {
case 'processing':
return 'รอการตรวจสอบ'
case 'rejected':
return 'หลักฐานการชำระเงินผิดพลาด'
case 'success':
return 'ตรวจสอบเเล้ว'
default:
return 'รออัปโหลดหลักฐานชำระเงิน'
}
}
</script>
<style lang="scss" scoped>
.q-img {
margin: 0 auto;
}
</style>

View file

@ -0,0 +1,632 @@
<!-- tab ประวการทำงาน/กงาน -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md">
<q-form ref="myForm">
<Table
:rows="rows"
:columns="columns"
:filter="filter"
:visible-columns="visibleColumns"
v-model:inputfilter="filter"
v-model:inputvisible="visibleColumns"
v-model:editvisible="edit"
:add="clickAdd"
:edit="clickEdit"
:cancel="clickCancel"
:nameHeader="false"
:addData="addData"
>
<template #columns="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
@click="selectData(props)"
class="cursor-pointer"
>
<div v-if="col.name == 'salary'" class="">
{{ col.value.toLocaleString('en-US') }}
</div>
<div v-else-if="col.name == 'duration'" class="">
{{ dateThaiRange(col.value) }}
</div>
<div v-else class="">
{{ col.value }}
</div>
</q-td>
<q-td auto-width v-if="edit === true">
<q-btn
color="red"
flat
dense
round
size="14px"
icon="mdi-trash-can-outline"
@click="clickDeleteRow(props.row)"
/>
</q-td>
</q-tr>
</template>
</Table>
</q-form>
</q-card>
<!-- popup Edit window-->
<q-dialog v-model="modal" persistent>
<q-card style="width: 600px">
<q-form ref="myForm">
<DialogHeader tittle="ประวัติการทำงาน/ฝึกงาน" :close="clickClose" />
<q-separator />
<q-card-section class="q-p-sm">
<div class="row col-12 items-center q-col-gutter-x-xs q-col-gutter-y-xs">
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="location"
:rules="[(val) => !!val || `${'กรุณากรอกสถานที่ทำงาน/ฝึกงาน'}`]"
:label="`${'สถานที่ทำงาน/ฝึกงาน'}`"
@update:modelValue="clickEditRow"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="position"
:rules="[(val) => !!val || `${'กรุณากรอกตำแหน่ง/ลักษณะงาน'}`]"
:label="`${'ตำแหน่ง/ลักษณะงาน'}`"
@update:modelValue="clickEditRow"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="salary"
:rules="[(val) => !!val || `${'กรุณากรอกเงินเดือนสุดท้ายก่อนออก'}`]"
:label="`${'เงินเดือนสุดท้ายก่อนออก'}`"
@update:modelValue="clickEditRow"
type="number"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<datepicker
:readonly="!edit"
v-model="duration"
:locale="'th'"
autoApply
range
:enableTimePicker="false"
week-start="0"
>
<template #year="{ year }">
{{ year + 543 }}
</template>
<template #year-overlay-value="{ value }">
{{ parseInt(value + 543) }}
</template>
<template #trigger>
<q-input
:class="getClass(edit)"
class="datepicker"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:borderless="!edit"
:model-value="dateThaiRange(duration)"
>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer" style="color: var(--q-primary)">
</q-icon>
</template>
</q-input>
</template>
</datepicker>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="reason"
:rules="[(val) => !!val || `${'กรุณากรอกเหตุผลที่ออก'}`]"
:label="`${'เหตุผลที่ออก'}`"
@update:modelValue="clickEditRow"
type="textarea"
/>
</div>
</div>
</q-card-section>
<q-separator />
<DialogFooter
:cancel="clickCancel"
:edit="clickEdit"
:save="clickSave"
:validate="validateData"
:clickNext="clickNext"
:clickPrevious="clickPrevious"
:editBtn="addData"
v-model:editvisible="edit"
v-model:next="next"
v-model:previous="previous"
v-model:modalEdit="modalEdit"
/>
</q-form>
</q-card>
</q-dialog>
<notifyError
:modalError="modalError"
:modalErrorTittle="modalErrorTittle"
:modalErrorDetail="modalErrorDetail"
:close="closeModalError"
></notifyError>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useExamDataStore } from '@/modules/01_exam/store'
import Table from '@/components/Table.vue'
import DialogHeader from '@/components/DialogHeader.vue'
import DialogFooter from '@/components/DialogFooter.vue'
import { useQuasar } from 'quasar'
import { useCounterMixin } from '@/stores/mixin'
import type {
RequestItemsObject,
Columns,
DataProps
} from '@/modules/01_exam/interface/request/Career'
import type { ResponseObject } from '@/modules/01_exam/interface/response/Career'
import http from '@/plugins/http'
import config from '@/app.config'
const props = defineProps({
loader: {
// main refresh data
type: Boolean,
required: true
},
step: {
type: Number,
required: true
},
statusEdit: {
type: Boolean,
required: true
}
})
const $q = useQuasar()
const mixin = useCounterMixin() //
const { dateThaiRange } = mixin
const store = useExamDataStore()
const { examData, changeExamColumns } = store
const loader = ref<boolean>(false)
const id = ref<string>()
const location = ref<string>()
const position = ref<string>()
const salary = ref<number | null>()
const duration = ref<[Date, Date]>([new Date(), new Date()])
const reason = ref<string>()
const myForm = ref<any>() //form data input
const edit = ref<boolean>(false) // dialog
const modal = ref<boolean>(false) //modal add detail
const modalEdit = ref<boolean>(false) //modal
const rawItem = ref<RequestItemsObject>() // row
const rowIndex = ref<number>(0) //index row
const previous = ref<boolean>() //
const next = ref<boolean>() //
const editRow = ref<boolean>(false) //
const addData = ref<boolean>(true)
const rawHistory = ref<RequestItemsObject[]>([]) //raw data history
const modalError = ref<boolean>(false) // modal error
const modalErrorTittle = ref<string>('') // tittle modal error
const modalErrorDetail = ref<string>('') // detail modal error
const statusCode = ref<number>()
const checkValidate = ref<boolean>(false) //validate data
const closeModalError = () => {
modalError.value = false
if (statusCode.value != 404) {
// fetchData();
}
}
const emit = defineEmits(['update:loader', 'update:statusEdit'])
const rows = ref<RequestItemsObject[]>([
{
id: '1',
location: 'แอดวานซ์ อินโฟร์ เซอร์วิส',
position: 'Direct Sales Staff',
salary: 30000,
duration: [new Date('1995-04-15'), new Date('1999-02-25')],
reason: 'ไล่ออก'
},
{
id: '2',
location: 'บริษัท ทรู คอร์ปอเรชั่น จำกัด (มหาชน)',
position: 'Direct Sales Staff',
salary: 40000,
duration: [new Date('2020-06-30'), new Date('2023-10-14')],
reason: '-'
}
])
const filter = ref<string>('') //search data table
const visibleColumns = ref<String[]>([])
examData.career.columns.length == 0
? (visibleColumns.value = ['location', 'position', 'salary', 'duration', 'reason'])
: (visibleColumns.value = examData.career.columns)
const columns = ref<Columns>([
{
name: 'location',
align: 'left',
label: 'สถานที่ทำงาน/ฝึกงาน',
sortable: true,
field: 'location',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'position',
align: 'left',
label: 'ตำแหน่ง/ลักษณะงาน',
sortable: true,
field: 'position',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'salary',
align: 'left',
label: 'เงินเดือนสุดท้ายก่อนออก',
sortable: true,
field: 'salary',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'duration',
align: 'left',
label: 'ระยะเวลา',
sortable: true,
field: 'duration',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'reason',
align: 'left',
label: 'เหตุผลที่ออก',
sortable: true,
field: 'reason',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
}
])
watch(loader, (count: boolean, prevCount: boolean) => {
emit('update:loader', count)
})
watch(visibleColumns, async (count: String[], prevCount: String[]) => {
await changeExamColumns('career', count)
})
watch(edit, (count: boolean, prevCount: boolean) => {
emit('update:statusEdit', count)
})
onMounted(async () => {
// await fetchData()
rawHistory.value = rows.value
if (props.step !== 2) {
addData.value = false
}
})
const fetchData = async () => {}
/**
* กดดอมลกอนหน
*/
const clickPrevious = () => {
// edit.value = false
rowIndex.value -= 1
const row = rows.value[rowIndex.value]
location.value = row.location
position.value = row.position
salary.value = row.salary
duration.value = row.duration
reason.value = row.reason
id.value = row.id
checkRowPage()
}
/**
* กดดอมลตอไป
*/
const clickNext = () => {
// edit.value = false
rowIndex.value += 1
const row = rows.value[rowIndex.value]
location.value = row.location
position.value = row.position
salary.value = row.salary
duration.value = row.duration
reason.value = row.reason
id.value = row.id
checkRowPage()
}
/**
* กดดอมลตอไป
*/
const getData = () => {
const row = rows.value[rowIndex.value]
location.value = row.location
position.value = row.position
salary.value = row.salary
duration.value = row.duration
reason.value = row.reason
id.value = row.id
}
/**
* เชคปมดอม อน อไป าตองแสดงไหม
*/
const checkRowPage = () => {
editRow.value = false
next.value = true
previous.value = true
if (rowIndex.value + 1 >= rows.value.length) {
next.value = false
}
if (rowIndex.value - 1 < 0) {
previous.value = false
}
}
/**
* กดปมแกไขใน dialog
*/
const clickEdit = () => {
edit.value = true
next.value = false
previous.value = false
}
/**
* กดปมเพมดานบน table
*/
const clickAdd = () => {
addRow()
}
/**
* ลบขอมลใน table
*/
const clickDeleteRow = (row: RequestItemsObject) => {
$q.dialog({
title: 'ยืนยันการลบข้อมูล',
message: 'หากต้องการลบกดให้กดตกลง',
cancel: true,
persistent: true
})
.onOk(() => {
rows.value = rows.value.filter((val: any) => val.id != row.id)
})
.onCancel(() => {})
.onDismiss(() => {})
}
/**
* กดบนทกใน dialog
*/
const clickSave = async () => {
myForm.value.validate().then(async (result: boolean) => {
if (result) {
if (modalEdit.value) {
await editData()
} else {
await saveData()
}
}
})
}
/**
* นทกเพมขอม
*/
const saveData = async () => {
modal.value = false
// loader.value = true;
// await http
// .post(config.API.xxxxxxxxxxxxxxx, {
// xxx: "xxx",
// })
// .then((res) => {
// success($q, "");
// })
// .catch((e) => {
// modalError.value = true;
// modalErrorTittle.value = "";
// modalErrorDetail.value = e.response.data.message;
// statusCode.value = e.response.data.status;
// })
// .finally(async () => {
// modal.value = false;
// await fetchData();
// });
}
/**
* นทกแกไขขอม
*/
const editData = async () => {
// edit.value = false
editRow.value = false
// loader.value = true;
// await http
// .post(config.API.xxxxxxxxxxxxxxx(id.value), {
// xxx: "xxx",
// })
// .then((res) => {
// success($q, "");
// })
// .catch((e) => {
// modalError.value = true;
// modalErrorTittle.value = "";
// modalErrorDetail.value = e.response.data.message;
// statusCode.value = e.response.data.status;
// })
// .finally(async () => {
// edit.value = false;
// await fetchData();
// });
}
/**
* กดป dialog
*/
const clickClose = async () => {
if (editRow.value == true) {
$q.dialog({
title: `ข้อมูลมีการแก้ไข`,
message: `ยืนยันการบันทึกข้อมูลใช่หรือไม่?`,
cancel: 'ยกเลิก',
ok: 'ยืนยัน',
persistent: true
}).onOk(async () => {
modal.value = false
next.value = false
previous.value = false
await getData()
})
} else {
modal.value = false
next.value = false
previous.value = false
}
}
/**
* กดเลอกขอมลทจะแกไข
* @param props props ใน row เลอก
*/
const selectData = (props: DataProps) => {
modalEdit.value = true
modal.value = true
// edit.value = false
rawItem.value = props.row
rowIndex.value = props.rowIndex
location.value = props.row.location
position.value = props.row.position
salary.value = props.row.salary
duration.value = props.row.duration
reason.value = props.row.reason
id.value = props.row.id
if (edit.value == true) {
next.value = false
previous.value = false
} else {
checkRowPage()
}
}
/**
* กดปมเพมบน table
*/
const addRow = () => {
modalEdit.value = false
modal.value = true
// edit.value = true
location.value = ''
position.value = ''
salary.value = null
duration.value = [new Date(), new Date()]
reason.value = ''
}
/**
* งกนปมยกเลกการแกไขขอม
*/
const clickCancel = async () => {
// if (editRow.value == true) {
// $q.dialog({
// title: ``,
// message: `?`,
// cancel: '',
// ok: '',
// persistent: true
// }).onOk(async () => {
// // edit.value = false
// checkRowPage()
// await getData()
// })
// } else {
// // edit.value = false
// checkRowPage()
// }
edit.value = false
}
/**
* เชความการแกไขขอม
*/
const clickEditRow = () => {
editRow.value = true
}
/**
* validate input ใน dialog
*/
const validateData = async () => {
checkValidate.value = true
await myForm.value.validate().then((result: boolean) => {
if (result == false) {
checkValidate.value = false
}
})
}
/**
* class ดรปแบบแสดงระหวางขอมลทแกไขหรอแสดงเฉยๆ
* @param val อม input สำหรบแกไขหรอไม
*/
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>
<style lang="scss">
.modalfix {
position: fixed !important;
}
</style>

View file

@ -0,0 +1,134 @@
<!-- card เอกสารหลกฐาน -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md">
<HeaderTop
v-model:edit="edit"
header="เอกสารหลักฐาน(เช่น สำเนาบัตรประชาชน ทะเบียนบ้าน วุฒิการศึกษา)"
icon="mdi-file-document"
:history="false"
:addData="addData"
:cancel="cancelData"
/>
<div class="row col-12 q-gutter-sm q-pt-sm">
<q-card bordered flat class="full-width">
<q-list separator>
<q-item v-for="file in files" :key="file.key" class="q-my-xs">
<q-item-section>
<q-item-label class="full-width ellipsis">
{{ file.name }}
</q-item-label>
<q-item-label caption> สถานะ: {{ file.status }} / {{ file.sizeLabel }} </q-item-label>
</q-item-section>
<q-item-section top side>
<div class="q-gutter-sm">
<q-btn size="12px" flat dense round color="blue" icon="mdi-download-outline">
<q-tooltip>ดาวนโหลด</q-tooltip>
</q-btn>
<q-btn
size="12px"
flat
dense
round
color="red"
icon="mdi-delete-outline"
v-if="edit"
>
<q-tooltip>ลบไฟล</q-tooltip>
</q-btn>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card>
<div v-if="edit" class="col-12 q-ma-none">
<q-separator class="q-mt-sm" />
<q-input
class="q-my-sm"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="name"
:rules="[(val) => !!val || `${'กรุณากรอกชื่อเอกสาร'}`]"
:label="`${'ชื่อเอกสาร'}`"
/>
<q-uploader
color="blue"
type="file"
flat
auto-upload
:factory="fileUpload"
class="full-width"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
bordered
label="[ไฟล์ jpg,png,pdf,csv,doc]"
/>
</div>
</div>
</q-card>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import HeaderTop from '@/components/top.vue'
const props = defineProps({
loader: {
// main refresh data
type: Boolean,
required: true
},
statusEdit: {
type: Boolean,
required: true
},
notiNoEdit: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const edit = ref<boolean>(false)
const addData = ref<boolean>(true)
const name = ref<string>('')
const files = ref<any>([
{
key: 1,
name: 'เอกสารข้อมูลการเปลี่ยนชื่อ',
status: 'อัปโหลดเสร็จสิ้น',
sizeLabel: '176MB'
},
{
key: 2,
name: 'เอกสารข้อมูลการสมรส',
status: 'อัปโหลดเสร็จสิ้น',
sizeLabel: '89MB'
}
])
const emit = defineEmits(['update:loader', 'update:statusEdit'])
watch(edit, (count: boolean, prevCount: boolean) => {
emit('update:statusEdit', count)
})
onMounted(async () => {
if (props.step !== 2) {
addData.value = false
}
})
const fileUpload = async (file: any) => {
return {
url: 'http://localhost:4444/upload',
method: 'POST'
}
}
const cancelData = () => {}
</script>

View file

@ -0,0 +1,661 @@
<!-- tab ประวการศกษา -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md">
<q-form ref="myForm">
<Table
:rows="rows"
:columns="columns"
:filter="filter"
:visible-columns="visibleColumns"
v-model:inputfilter="filter"
v-model:inputvisible="visibleColumns"
v-model:editvisible="edit"
:add="clickAdd"
:edit="clickEdit"
:cancel="clickCancel"
:nameHeader="false"
:addData="addData"
>
<template #columns="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
@click="selectData(props)"
class="cursor-pointer"
>
<div v-if="col.name == 'duration'" class="">
{{ dateThaiRange(col.value) }}
</div>
<div v-else class="">
{{ col.value }}
</div>
</q-td>
<q-td auto-width v-if="edit === true">
<q-btn
color="red"
flat
dense
round
size="14px"
icon="mdi-trash-can-outline"
@click="clickDeleteRow(props.row)"
/>
</q-td>
</q-tr>
</template>
</Table>
</q-form>
</q-card>
<!-- popup Edit window-->
<q-dialog v-model="modal" persistent>
<q-card style="width: 600px">
<q-form ref="myForm">
<DialogHeader tittle="ประวัติการศึกษา" :close="clickClose" />
<q-separator />
<q-card-section class="q-p-sm">
<div class="row col-12 items-center q-col-gutter-x-xs q-col-gutter-y-xs">
<div class="col-xs-6 col-sm-6 col-md-6">
<q-select
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="qualificationId"
:rules="[(val) => !!val || `${'กรุณาเลือกวุฒิที่ได้รับ'}`]"
:label="`${'วุฒิที่ได้รับ'}`"
@update:modelValue="clickEditRow"
emit-value
map-options
option-label="name"
:options="qualificationOptions"
option-value="id"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="major"
:rules="[(val) => !!val || `${'กรุณากรอกสาขาวิชา/วิชาเอก'}`]"
:label="`${'สาขาวิชา/วิชาเอก'}`"
@update:modelValue="clickEditRow"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
type="number"
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="scores"
:rules="[(val) => !!val || `${'กรุณากรอกคะแนนเฉลี่ยตลอดหลักสูตร'}`]"
:label="`${'คะแนนเฉลี่ยตลอดหลักสูตร'}`"
@update:modelValue="clickEditRow"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="name"
:rules="[(val) => !!val || `${'กรุณากรอกชื่อสถานศึกษา'}`]"
:label="`${'ชื่อสถานศึกษา'}`"
@update:modelValue="clickEditRow"
/>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<datepicker
:readonly="!edit"
v-model="duration"
:locale="'th'"
autoApply
range
:enableTimePicker="false"
week-start="0"
>
<template #year="{ year }">
{{ year + 543 }}
</template>
<template #year-overlay-value="{ value }">
{{ parseInt(value + 543) }}
</template>
<template #trigger>
<q-input
:class="getClass(edit)"
class="datepicker"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:borderless="!edit"
:model-value="dateThaiRange(duration)"
>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer" style="color: var(--q-primary)">
</q-icon>
</template>
</q-input>
</template>
</datepicker>
</div>
</div>
</q-card-section>
<q-separator />
<DialogFooter
:cancel="clickCancel"
:edit="clickEdit"
:save="clickSave"
:validate="validateData"
:clickNext="clickNext"
:clickPrevious="clickPrevious"
:editBtn="addData"
v-model:editvisible="edit"
v-model:next="next"
v-model:previous="previous"
v-model:modalEdit="modalEdit"
/>
</q-form>
</q-card>
</q-dialog>
<notifyError
:modalError="modalError"
:modalErrorTittle="modalErrorTittle"
:modalErrorDetail="modalErrorDetail"
:close="closeModalError"
></notifyError>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useExamDataStore } from '@/modules/01_exam/store'
import Table from '@/components/Table.vue'
import DialogHeader from '@/components/DialogHeader.vue'
import DialogFooter from '@/components/DialogFooter.vue'
import { useQuasar } from 'quasar'
import { useCounterMixin } from '@/stores/mixin'
import type {
RequestItemsObject,
Columns,
DataProps
} from '@/modules/01_exam/interface/request/Education'
import type { ResponseObject } from '@/modules/01_exam/interface/response/Education'
import type { DataOption } from '@/modules/01_exam/interface/index/Main'
import http from '@/plugins/http'
import config from '@/app.config'
const props = defineProps({
loader: {
// main refresh data
type: Boolean,
required: true
},
step: {
type: Number,
required: true
},
statusEdit: {
type: Boolean,
required: true
}
})
const $q = useQuasar()
const mixin = useCounterMixin() //
const { dateThaiRange } = mixin
const store = useExamDataStore()
const { examData, changeExamColumns } = store
const loader = ref<boolean>(false)
const id = ref<string>()
const qualification = ref<string>()
const qualificationId = ref<string>()
const qualificationOptions = ref<DataOption[]>([])
const major = ref<string>()
const scores = ref<number | null>()
const name = ref<string>()
const duration = ref<[Date, Date]>([new Date(), new Date()])
const myForm = ref<any>() //form data input
const edit = ref<boolean>(false) // dialog
const modal = ref<boolean>(false) //modal add detail
const modalEdit = ref<boolean>(false) //modal
const rawItem = ref<RequestItemsObject>() // row
const rowIndex = ref<number>(0) //index row
const previous = ref<boolean>() //
const next = ref<boolean>() //
const editRow = ref<boolean>(false) //
const addData = ref<boolean>(true)
const rawHistory = ref<RequestItemsObject[]>([]) //raw data history
const modalError = ref<boolean>(false) // modal error
const modalErrorTittle = ref<string>('') // tittle modal error
const modalErrorDetail = ref<string>('') // detail modal error
const statusCode = ref<number>()
const checkValidate = ref<boolean>(false) //validate data
const closeModalError = () => {
modalError.value = false
if (statusCode.value != 404) {
// fetchData();
}
}
const emit = defineEmits(['update:loader', 'update:statusEdit'])
const rows = ref<RequestItemsObject[]>([
{
id: '1',
qualificationId: 'ปริญญาตรี',
qualification: 'ปริญญาตรี',
major: 'คอมพิวเตอร์',
scores: 3.99,
name: 'มหาลัยเอ',
duration: [new Date('1995-04-15'), new Date('1999-02-25')]
},
{
id: '2',
qualificationId: 'ปริญญาเอก',
qualification: 'ปริญญาเอก',
major: 'คอมพิวเตอร์',
scores: 3.54,
name: 'มหาลัยบี',
duration: [new Date('2020-06-30'), new Date('2023-10-14')]
}
])
const filter = ref<string>('') //search data table
const visibleColumns = ref<String[]>([])
examData.education.columns.length == 0
? (visibleColumns.value = ['qualification', 'major', 'scores', 'name', 'duration'])
: (visibleColumns.value = examData.education.columns)
const columns = ref<Columns>([
{
name: 'qualification',
align: 'left',
label: 'วุฒิที่ได้รับ',
sortable: true,
field: 'qualification',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'major',
align: 'left',
label: 'สาขาวิชา/วิชาเอก',
sortable: true,
field: 'major',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'scores',
align: 'left',
label: 'คะแนนเฉลี่ยตลอดหลักสูตร',
sortable: true,
field: 'scores',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'name',
align: 'left',
label: 'ชื่อสถานศึกษา',
sortable: true,
field: 'name',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'duration',
align: 'left',
label: 'ระยะเวลา',
sortable: true,
field: 'duration',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
}
])
watch(loader, (count: boolean, prevCount: boolean) => {
emit('update:loader', count)
})
watch(visibleColumns, async (count: String[], prevCount: String[]) => {
await changeExamColumns('education', count)
})
watch(edit, (count: boolean, prevCount: boolean) => {
emit('update:statusEdit', count)
})
onMounted(async () => {
// await fetchData()
// await fetchQualification()
rawHistory.value = rows.value
if (props.step !== 2) {
addData.value = false
}
})
const fetchQualification = async () => {
// loader.value = true;
// await http
// .get(config.API.educationQualification)
// .then((res) => {
// const data = res.data.result
// let option: DataOption[] = []
// data.map((r: any) => {
// option.push({ id: r.id.toString(), name: r.name.toString() })
// })
// qualificationOptions.value = option
// })
// .catch((e: any) => {})
// .finally(() => {
// // loader.value = false;
// })
}
const fetchData = async () => {}
/**
* กดดอมลกอนหน
*/
const clickPrevious = () => {
// edit.value = false
rowIndex.value -= 1
const row = rows.value[rowIndex.value]
qualification.value = row.qualification
qualificationId.value = row.qualificationId
major.value = row.major
scores.value = row.scores
name.value = row.name
duration.value = row.duration
id.value = row.id
checkRowPage()
}
/**
* กดดอมลตอไป
*/
const clickNext = () => {
// edit.value = false
rowIndex.value += 1
const row = rows.value[rowIndex.value]
qualification.value = row.qualification
qualificationId.value = row.qualificationId
major.value = row.major
scores.value = row.scores
name.value = row.name
duration.value = row.duration
id.value = row.id
checkRowPage()
}
/**
* กดดอมลตอไป
*/
const getData = () => {
const row = rows.value[rowIndex.value]
qualification.value = row.qualification
qualificationId.value = row.qualificationId
major.value = row.major
scores.value = row.scores
name.value = row.name
duration.value = row.duration
id.value = row.id
}
/**
* เชคปมดอม อน อไป าตองแสดงไหม
*/
const checkRowPage = () => {
editRow.value = false
next.value = true
previous.value = true
if (rowIndex.value + 1 >= rows.value.length) {
next.value = false
}
if (rowIndex.value - 1 < 0) {
previous.value = false
}
}
/**
* กดปมแกไขใน dialog
*/
const clickEdit = () => {
edit.value = true
next.value = false
previous.value = false
}
/**
* กดปมเพมดานบน table
*/
const clickAdd = () => {
addRow()
}
/**
* ลบขอมลใน table
*/
const clickDeleteRow = (row: RequestItemsObject) => {
$q.dialog({
title: 'ยืนยันการลบข้อมูล',
message: 'หากต้องการลบกดให้กดตกลง',
cancel: true,
persistent: true
})
.onOk(() => {
rows.value = rows.value.filter((val: any) => val.id != row.id)
})
.onCancel(() => {})
.onDismiss(() => {})
}
/**
* กดบนทกใน dialog
*/
const clickSave = async () => {
myForm.value.validate().then(async (result: boolean) => {
if (result) {
if (modalEdit.value) {
await editData()
} else {
await saveData()
}
}
})
}
/**
* นทกเพมขอม
*/
const saveData = async () => {
modal.value = false
// loader.value = true;
// await http
// .post(config.API.xxxxxxxxxxxxxxx, {
// xxx: "xxx",
// })
// .then((res) => {
// success($q, "");
// })
// .catch((e) => {
// modalError.value = true;
// modalErrorTittle.value = "";
// modalErrorDetail.value = e.response.data.message;
// statusCode.value = e.response.data.status;
// })
// .finally(async () => {
// modal.value = false;
// await fetchData();
// });
}
/**
* นทกแกไขขอม
*/
const editData = async () => {
// edit.value = false
editRow.value = false
// loader.value = true;
// await http
// .post(config.API.xxxxxxxxxxxxxxx(id.value), {
// xxx: "xxx",
// })
// .then((res) => {
// success($q, "");
// })
// .catch((e) => {
// modalError.value = true;
// modalErrorTittle.value = "";
// modalErrorDetail.value = e.response.data.message;
// statusCode.value = e.response.data.status;
// })
// .finally(async () => {
// edit.value = false;
// await fetchData();
// });
}
/**
* กดป dialog
*/
const clickClose = async () => {
if (editRow.value == true) {
$q.dialog({
title: `ข้อมูลมีการแก้ไข`,
message: `ยืนยันการบันทึกข้อมูลใช่หรือไม่?`,
cancel: 'ยกเลิก',
ok: 'ยืนยัน',
persistent: true
}).onOk(async () => {
modal.value = false
next.value = false
previous.value = false
await getData()
})
} else {
modal.value = false
next.value = false
previous.value = false
}
}
/**
* กดเลอกขอมลทจะแกไข
* @param props props ใน row เลอก
*/
const selectData = (props: DataProps) => {
modalEdit.value = true
modal.value = true
// edit.value = false
rawItem.value = props.row
rowIndex.value = props.rowIndex
qualification.value = props.row.qualification
qualificationId.value = props.row.qualificationId
major.value = props.row.major
scores.value = props.row.scores
name.value = props.row.name
duration.value = props.row.duration
id.value = props.row.id
if (edit.value == true) {
next.value = false
previous.value = false
} else {
checkRowPage()
}
}
/**
* กดปมเพมบน table
*/
const addRow = () => {
modalEdit.value = false
modal.value = true
// edit.value = true
qualification.value = ''
qualificationId.value = ''
major.value = ''
scores.value = null
name.value = ''
duration.value = [new Date(), new Date()]
}
/**
* งกนปมยกเลกการแกไขขอม
*/
const clickCancel = async () => {
// if (editRow.value == true) {
// $q.dialog({
// title: ``,
// message: `?`,
// cancel: '',
// ok: '',
// persistent: true
// }).onOk(async () => {
// edit.value = false
// checkRowPage()
// await getData()
// })
// } else {
// edit.value = false
// checkRowPage()
// }
edit.value = false
}
/**
* เชความการแกไขขอม
*/
const clickEditRow = () => {
editRow.value = true
}
/**
* validate input ใน dialog
*/
const validateData = async () => {
checkValidate.value = true
await myForm.value.validate().then((result: boolean) => {
if (result == false) {
checkValidate.value = false
}
})
}
/**
* class ดรปแบบแสดงระหวางขอมลทแกไขหรอแสดงเฉยๆ
* @param val อม input สำหรบแกไขหรอไม
*/
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>
<style lang="scss">
.modalfix {
position: fixed !important;
}
</style>

View file

@ -0,0 +1,86 @@
<!-- tab อมลสวนบคคล -->
<template>
<Information
:prefixOptions="prefixOptions"
:genderOptions="genderOptions"
:bloodOptions="bloodOptions"
:statusOptions="statusOptions"
:religionOptions="religionOptions"
:provinceOptions="provinceOptions"
v-model:statusEdit="statusEdit"
:notiNoEdit="notiNoEdit"
:step="step"
/>
<Address v-model:statusEdit="statusEdit" :notiNoEdit="notiNoEdit" :step="step" />
<Family
:prefixOptions="prefixOptions"
v-model:statusEdit="statusEdit"
:notiNoEdit="notiNoEdit"
:step="step"
/>
<Occupation v-model:statusEdit="statusEdit" :notiNoEdit="notiNoEdit" :step="step" />
<NotifyError
:modalError="modalNoEdit"
:modalErrorTittle="modalNoEditTittle"
:modalErrorDetail="modalNoEditDetail"
:close="closeModalError"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import type { DataOption } from '@/modules/01_exam/interface/index/Main'
import Information from '@/modules/01_exam/components/Form/Profile/Information.vue'
import Address from '@/modules/01_exam/components/Form/Profile/Address.vue'
import Family from '@/modules/01_exam/components/Form/Profile/Family.vue'
import Occupation from '@/modules/01_exam/components/Form/Profile/Occupation.vue'
import NotifyError from '@/components/NotifyError.vue'
import http from '@/plugins/http'
import config from '@/app.config'
const props = defineProps({
loader: {
// main refresh data
type: Boolean,
required: true
},
statusEdit: {
type: Boolean,
required: true
},
step: {
type: Number,
required: true
}
})
const loader = ref<boolean>(true)
const statusEdit = ref<boolean>(false)
const modalNoEdit = ref<boolean>(false)
// const modalNoEditTittle = ref<string>('?')
// const modalNoEditDetail = ref<string>(
// ' '
// )
const modalNoEditTittle = ref<string>('ไม่สามารถไม่สามารถแก้ไขข้อมูลได้?')
const modalNoEditDetail = ref<string>('มีข้อมูลที่ยังไม่ถูกบันทึกข้อมูล')
const prefixOptions = ref<DataOption[]>([])
const bloodOptions = ref<DataOption[]>([])
const genderOptions = ref<DataOption[]>([])
const statusOptions = ref<DataOption[]>([])
const religionOptions = ref<DataOption[]>([])
const provinceOptions = ref<DataOption[]>([])
const emit = defineEmits(['update:loader', 'update:statusEdit'])
watch(statusEdit, (count: boolean, prevCount: boolean) => {
emit('update:statusEdit', count)
})
const closeModalError = () => {
modalNoEdit.value = false
}
const notiNoEdit = () => {
modalNoEdit.value = true
}
</script>

View file

@ -0,0 +1,421 @@
<!-- card อมลทอย -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md q-mt-md">
<!-- <HeaderTop
v-model:edit="edit"
header="ข้อมูลที่อยู่"
icon="mdi-map-marker"
:save="saveData"
/> -->
<HeaderTop
v-model:edit="edit"
header="ข้อมูลที่อยู่"
icon="mdi-map-marker"
:save="saveData"
:history="true"
:addData="addData"
:cancel="cancelData"
:changeBtn="changeBtn"
/>
<q-form ref="myform">
<div class="row col-12 items-center q-col-gutter-x-xs q-col-gutter-y-xs">
<div class="col-xs-12">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="addressData.address"
:rules="[(val) => !!val || `${'กรุณากรอก ที่อยู่ตามทะเบียนบ้าน'}`]"
:label="`${'ที่อยู่ตามทะเบียนบ้าน'}`"
/>
<!-- :filled="edit" -->
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก จังหวัด'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.provinceId"
emit-value
map-options
option-label="name"
:options="provinceOptions"
option-value="id"
:label="`${'จังหวัด'}`"
@update:model-value="(value) => selectProvince(value, '1')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก เขต / อำเภอ'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.districtId"
emit-value
map-options
option-label="name"
:options="districtOptions"
option-value="id"
:label="`${'เขต / อำเภอ'}`"
@update:model-value="(value) => selectDistrict(value, '1')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก ตำบล / แขวง'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.subdistrictId"
emit-value
map-options
option-label="name"
:options="subdistrictOptions"
option-value="id"
:label="`${'ตำบล / แขวง'}`"
@update:model-value="(value) => selectSubDistrict(value, '1')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
readonly
borderless
v-model="addressData.code"
:style="!edit ? '' : 'padding:0 12px;'"
:label="`${'รหัสไปรษณีย์'}`"
/>
</div>
<div class="col-12 q-pt-lg"><q-separator /></div>
<div class="col-xs-12 q-gutter-sm items-center flex q-my-sm">
<label class="text-bold">อยจจนตรงกบทอยตามทะเบยนบาน</label>
<q-radio
v-model="addressData.same"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="1"
label="ใช่"
dense
:disable="!edit"
/>
<q-radio
v-model="addressData.same"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="0"
label="ไม่"
dense
:disable="!edit"
/>
</div>
<div class="col-xs-12" v-if="addressData.same == '0'">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="addressData.addressC"
:rules="[(val) => !!val || `${'กรุณากรอก ที่อยู่ปัจจุบัน'}`]"
:label="`${'ที่อยู่ปัจจุบัน'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="addressData.same == '0'">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก จังหวัด'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.provinceIdC"
emit-value
map-options
option-label="name"
:options="provinceOptions"
option-value="id"
:label="`${'จังหวัด'}`"
@update:model-value="(value) => selectProvince(value, '2')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="addressData.same == '0'">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก เขต / อำเภอ'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.districtIdC"
emit-value
map-options
option-label="name"
:options="districtCOptions"
option-value="id"
:label="`${'เขต / อำเภอ'}`"
@update:model-value="(value) => selectDistrict(value, '2')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="addressData.same == '0'">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก ตำบล / แขวง'}`]"
:outlined="edit"
dense
lazy-rules
v-model="addressData.subdistrictIdC"
emit-value
map-options
option-label="name"
:options="subdistrictCOptions"
option-value="id"
:label="`${'ตำบล / แขวง'}`"
@update:model-value="(value) => selectSubDistrict(value, '2')"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="addressData.same == '0'">
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
readonly
borderless
v-model="addressData.codeC"
:style="!edit ? '' : 'padding:0 12px;'"
:label="`${'รหัสไปรษณีย์'}`"
/>
</div>
</div>
</q-form>
</q-card>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
import http from '@/plugins/http'
import config from '@/app.config'
import type { Address, DataOption, zipCodeOption } from '@/modules/01_exam/interface/index/Main'
import { defaultAddress } from '@/modules/01_exam/interface/index/Main'
import HeaderTop from '@/components/top.vue'
const mixin = useCounterMixin()
const { date2Thai, calAge } = mixin
const edit = ref<boolean>(false)
const addData = ref<boolean>(true)
const addressData = ref<Address>(defaultAddress)
const myform = ref<any>()
const codep = ref<string>('')
const codec = ref<string>('')
const provinceOptions = ref<DataOption[]>([])
const districtOptions = ref<DataOption[]>([])
const districtCOptions = ref<DataOption[]>([])
const subdistrictOptions = ref<zipCodeOption[]>([])
const subdistrictCOptions = ref<zipCodeOption[]>([])
const props = defineProps({
statusEdit: {
type: Boolean,
required: true
},
notiNoEdit: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const emit = defineEmits(['update:statusEdit'])
onMounted(() => {
if (props.step !== 2) {
addData.value = false
}
// fetchProvince()
// fetchDistrict(addressData.value.provinceId, '1')
// fetchDistrict(addressData.value.provinceIdC, '2')
})
const saveData = async () => {
await myform.value.validate().then(async (success: boolean) => {
if (success) {
} else {
}
})
}
const changeBtn = async () => {
if (edit.value == true) {
if (props.statusEdit === true) {
edit.value = false
props.notiNoEdit()
} else {
emit('update:statusEdit', true)
}
} else {
emit('update:statusEdit', false)
}
}
const selectProvince = (e: string, name: string) => {
if (name == '1') {
addressData.value.districtId = ''
addressData.value.subdistrictId = ''
codep.value = ''
} else {
addressData.value.districtIdC = ''
addressData.value.subdistrictIdC = ''
codec.value = ''
}
myform.value.resetValidation()
fetchDistrict(e, name)
}
const selectDistrict = (e: string, name: string) => {
if (name == '1') {
addressData.value.subdistrictId = ''
codep.value = ''
} else {
addressData.value.subdistrictIdC = ''
codec.value = ''
}
myform.value.resetValidation()
fetchSubDistrict(e, name)
}
const selectSubDistrict = (e: string, name: string) => {
if (name == '1') {
const findcode = subdistrictOptions.value.filter((r) => r.id == e)
const namecode = findcode.length > 0 ? findcode[0].zipCode : ''
codep.value = namecode
} else {
const findcode = subdistrictCOptions.value.filter((r) => r.id == e)
const namecode = findcode.length > 0 ? findcode[0].zipCode : ''
codec.value = namecode
}
// myform.value.resetValidation();
// fetchSubDistrict(e, name);
}
const fetchProvince = async () => {
// // loader.value = true;
// await http
// .get(config.API.province)
// .then((res) => {
// const data = res.data.result
// let option: DataOption[] = []
// // console.log(data);
// data.map((r: any) => {
// option.push({ id: r.id.toString(), name: r.name.toString() })
// })
// provinceOptions.value = option
// })
// .catch((e: any) => {})
// .finally(() => {
// // loader.value = false;
// })
}
const fetchDistrict = async (id: string, position: string) => {
// // loader.value = true;
// await http
// .get(config.API.listDistrict(id))
// .then((res) => {
// const data = res.data.result
// let option: DataOption[] = []
// // console.log(data);
// data.map((r: any) => {
// option.push({ id: r.id.toString(), name: r.name.toString() })
// })
// if (position == '1') {
// districtOptions.value = option
// } else {
// districtCOptions.value = option
// }
// })
// .catch((e: any) => {})
// .finally(() => {
// // loader.value = false;
// })
}
const fetchSubDistrict = async (id: string, position: string) => {
// // loader.value = true;
// await http
// .get(config.API.listSubDistrict(id))
// .then((res) => {
// const data = res.data.result
// let option: zipCodeOption[] = []
// // console.log(res);
// data.map((r: any) => {
// option.push({
// id: r.id.toString(),
// name: r.name.toString(),
// zipCode: r.zipCode.toString()
// })
// })
// if (position == '1') {
// subdistrictOptions.value = option
// } else {
// subdistrictCOptions.value = option
// }
// })
// .catch((e: any) => {})
// .finally(() => {
// // loader.value = false;
// })
}
const cancelData = () => {}
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>

View file

@ -0,0 +1,361 @@
<!-- card อมลครอบคร -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md q-mt-md text-dark">
<HeaderTop
v-model:edit="edit"
header="ข้อมูลครอบครัว"
icon="mdi-account-group"
:save="saveData"
:addData="addData"
:cancel="cancelData"
:changeBtn="changeBtn"
/>
<q-form ref="myform" class="col-12">
<div class="row col-12 items-center q-col-gutter-x-xs q-col-gutter-y-xs">
<div class="col-xs-12 q-gutter-sm items-center flex q-my-sm">
<label class="text-weight-bold"> สมรส</label>
<q-radio
v-model="familyData.same"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="1"
label="มี"
dense
:disable="!edit"
@update:model-value="selectRadio"
/>
<q-radio
v-model="familyData.same"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="0"
label="ไม่มี"
dense
:disable="!edit"
@update:model-value="selectRadio"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2" v-if="familyData.same == '1'">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก คำนำหน้า'}`]"
:outlined="edit"
dense
lazy-rules
v-model="familyData.prefixIdC"
emit-value
map-options
option-label="name"
:options="prefixOptions"
option-value="id"
:label="`${'คำนำหน้า'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="familyData.same == '1'">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.firstnameC"
:rules="[(val) => !!val || `${'กรุณากรอก ชื่อ'}`]"
:label="`${'ชื่อ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3" v-if="familyData.same == '1'">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.lastnameC"
:rules="[(val) => !!val || `${'กรุณากรอก นามสกุล'}`]"
:label="`${'นามสกุล'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2" v-if="familyData.same == '1'">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.occupationC"
:rules="[(val) => !!val || `${'กรุณากรอก อาชีพ'}`]"
:label="`${'อาชีพ'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2" v-if="familyData.same == '1'">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.nationalityC"
:rules="[(val) => !!val || `${'กรุณากรอก สัญชาติ'}`]"
:label="`${'สัญชาติ'}`"
/>
</div>
<div class="col-xs-12 text-weight-bold"> ดา</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก คำนำหน้า'}`]"
:outlined="edit"
dense
lazy-rules
v-model="familyData.prefixIdM"
emit-value
map-options
option-label="name"
:options="prefixOptions"
option-value="id"
:label="`${'คำนำหน้า'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.firstnameM"
:rules="[(val) => !!val || `${'กรุณากรอก ชื่อ'}`]"
:label="`${'ชื่อ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.lastnameM"
:rules="[(val) => !!val || `${'กรุณากรอก นามสกุล'}`]"
:label="`${'นามสกุล'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.occupationM"
:rules="[(val) => !!val || `${'กรุณากรอก อาชีพ'}`]"
:label="`${'อาชีพ'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.nationalityM"
:rules="[(val) => !!val || `${'กรุณากรอก สัญชาติ'}`]"
:label="`${'สัญชาติ'}`"
/>
</div>
<div class="col-xs-12 text-weight-bold"> มารดา</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก คำนำหน้า'}`]"
:outlined="edit"
dense
lazy-rules
v-model="familyData.prefixIdF"
emit-value
map-options
option-label="name"
:options="prefixOptions"
option-value="id"
:label="`${'คำนำหน้า'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.firstnameF"
:rules="[(val) => !!val || `${'กรุณากรอก ชื่อ'}`]"
:label="`${'ชื่อ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.lastnameF"
:rules="[(val) => !!val || `${'กรุณากรอก นามสกุล'}`]"
:label="`${'นามสกุล'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.occupationF"
:rules="[(val) => !!val || `${'กรุณากรอก อาชีพ'}`]"
:label="`${'อาชีพ'}`"
/>
</div>
<div class="col-xs-12 col-sm-2 col-md-2">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="familyData.nationalityF"
:rules="[(val) => !!val || `${'กรุณากรอก สัญชาติ'}`]"
:label="`${'สัญชาติ'}`"
/>
</div>
</div>
</q-form>
</q-card>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import type { PropType } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
import type { Family, DataOption } from '@/modules/01_exam/interface/index/Main'
import { defaultFamily } from '@/modules/01_exam/interface/index/Main'
import HeaderTop from '@/components/top.vue'
const mixin = useCounterMixin()
const { date2Thai, calAge } = mixin
const edit = ref<boolean>(false)
const addData = ref<boolean>(true)
const myform = ref<any>()
const familyData = ref<Family>(defaultFamily)
const statusOptions = ref<DataOption[]>([])
// const prefixOptions = ref<any>([
// { id: "1", name: "" },
// { id: "2", name: "" },
// { id: "3", name: "" },
// ]);
const props = defineProps({
prefixOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
statusEdit: {
type: Boolean,
required: true
},
notiNoEdit: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const emit = defineEmits(['update:statusEdit'])
onMounted(() => {
if (props.step !== 2) {
addData.value = false
}
})
const saveData = async () => {
await myform.value.validate().then(async (success: boolean) => {
if (success) {
} else {
}
})
}
const changeBtn = async () => {
if (edit.value == true) {
if (props.statusEdit === true) {
edit.value = false
props.notiNoEdit()
} else {
emit('update:statusEdit', true)
}
} else {
emit('update:statusEdit', false)
}
}
const selectRadio = (e: boolean, i: any) => {
if (!e) {
familyData.value.prefixIdC = ''
familyData.value.firstnameC = ''
familyData.value.lastnameC = ''
familyData.value.occupationC = ''
}
}
const cancelData = () => {}
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>

View file

@ -0,0 +1,494 @@
<!-- card อมลสวนต -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md">
<HeaderTop
v-model:edit="edit"
header="ข้อมูลส่วนตัว"
icon="mdi-account"
:save="saveData"
:addData="addData"
:cancel="cancelData"
:changeBtn="changeBtn"
/>
<q-form ref="myform" class="col-12 row">
<div class="row col-10 items-center q-col-gutter-x-sm q-col-gutter-y-sm">
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก คำนำหน้า'}`]"
:outlined="edit"
dense
lazy-rules
v-model="informaData.prefixId"
emit-value
map-options
option-label="name"
:options="prefixOptions"
option-value="id"
:label="`${'คำนำหน้า'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.firstname"
:rules="[(val) => !!val || `${'กรุณากรอก ชื่อ'}`]"
:label="`${'ชื่อ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.lastname"
:rules="[(val) => !!val || `${'กรุณากรอก นามสกุล'}`]"
:label="`${'นามสกุล'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.nationality"
:rules="[(val) => !!val || `${'กรุณากรอก สัญชาติ'}`]"
:label="`${'สัญชาติ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<datepicker
v-model="informaData.birthDate"
:locale="'th'"
autoApply
:enableTimePicker="false"
week-start="0"
:max-date="new Date()"
>
<template #year="{ year }">
{{ year + 543 }}
</template>
<template #year-overlay-value="{ value }">
{{ parseInt(value + 543) }}
</template>
<template #trigger>
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
:model-value="date2Thai(informaData.birthDate)"
:rules="[(val) => !!val || `${'กรุณาเลือก วัน/เดือน/ปี เกิด'}`]"
:label="`${'วัน/เดือน/ปี เกิด'}`"
>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer" style="color: var(--q-primary)">
</q-icon>
</template>
</q-input>
</template>
</datepicker>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
readonly
borderless
:style="!edit ? '' : 'padding:0 12px;'"
:model-value="calAge(informaData.birthDate)"
:label="`${'อายุ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก สถานภาพ'}`]"
:outlined="edit"
dense
lazy-rules
v-model="informaData.statusId"
emit-value
map-options
option-label="name"
:options="statusOptions"
option-value="id"
:label="`${'สถานภาพ'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.email"
:rules="[(val) => !!val || `${'กรุณากรอก E-mail address'}`]"
label="E-mail address"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.cardid"
maxlength="13"
:rules="[
(val) => !!val || `${'กรุณากรอก เลขบัตรประจำตัวประชาชน'}`,
(val) => val.length >= 13 || `${'กรุณากรอกเลขบัตรประจำตัวประชาชนให้ครบ'}`
]"
label="เลขบัตรประจำตัวประชาชน"
mask="#############"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.cardid"
:rules="[(val) => !!val || `${'กรุณากรอก ออกให้ ณ อำเภอ'}`]"
label="ออกให้ ณ อำเภอ"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-select
hide-bottom-space
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
:rules="[(val) => !!val || `${'กรุณาเลือก จังหวัด'}`]"
:outlined="edit"
dense
lazy-rules
v-model="informaData.provinceId"
emit-value
map-options
option-label="name"
:options="provinceOptions"
option-value="id"
:label="`${'จังหวัด'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<datepicker
v-model="informaData.cardIdDate"
:locale="'th'"
autoApply
:enableTimePicker="false"
week-start="0"
:max-date="new Date()"
>
<template #year="{ year }">
{{ year + 543 }}
</template>
<template #year-overlay-value="{ value }">
{{ parseInt(value + 543) }}
</template>
<template #trigger>
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
:model-value="date2Thai(informaData.cardIdDate)"
:rules="[(val) => !!val || `${'กรุณาเลือก วัน/เดือน/ปี'}`]"
:label="`${'วัน/เดือน/ปี'}`"
>
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer" style="color: var(--q-primary)">
</q-icon>
</template>
</q-input>
</template>
</datepicker>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="tel"
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
v-model="informaData.tel"
:rules="[
(val) => !!val || `${'กรุณากรอก โทรศัพท์'}`,
(val) => val.length >= 9 || `${'กรุณากรอกข้อมูลโทรศัพท์ให้ครบ'}`
]"
:label="`${'โทรศัพท์'}`"
mask="#########"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="tel"
:class="getClass(edit)"
:readonly="!edit"
:borderless="!edit"
v-model="informaData.phone"
:rules="[
(val) => !!val || `${'กรุณากรอก โทรศัพท์มือถือ'}`,
(val) => val.length >= 10 || `${'กรุณากรอกข้อมูลโทรศัพท์มือถือให้ครบ'}`
]"
:label="`${'โทรศัพท์มือถือ'}`"
mask="##########"
/>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
:readonly="!edit"
:borderless="!edit"
v-model="informaData.knowledge"
:rules="[(val) => !!val || `${'กรุณากรอก ความรู้ความสามารถพิเศษ'}`]"
label="ความรู้ความสามารถพิเศษ"
type="textarea"
/>
</div>
</div>
<div class="row col-2 justify-center q-pt-md">
<div class="containerimage row justify-center">
<q-file
borderless
v-model="fileData"
stack-label
:readonly="!edit"
@update:model-value="pickFile"
>
<q-img src="@/assets/avatar_user.jpg" class="col-12">
<div class="overlay" v-if="edit">
<q-icon name="mdi-camera" />
<br />ปเดต
</div>
</q-img>
</q-file>
</div>
<div class="col-12 text-center" v-if="disabledPic">
<q-btn outline dense color="black" icon="mdi-content-save-outline" @click="savePic">
<q-tooltip content-class="bg-grey-2 text-black">นทกร</q-tooltip>
</q-btn>
</div>
</div>
</q-form>
</q-card>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useCounterMixin } from '@/stores/mixin'
import type { PropType } from 'vue'
import type { Information, DataOption } from '@/modules/01_exam/interface/index/Main'
import { defaultInformation } from '@/modules/01_exam/interface/index/Main'
import HeaderTop from '@/components/top.vue'
import http from '@/plugins/http'
import config from '@/app.config'
import type { file } from '@babel/types'
const mixin = useCounterMixin()
const { date2Thai, calAge } = mixin
const edit = ref<boolean>(false)
const addData = ref<boolean>(true)
const informaData = ref<Information>(defaultInformation)
const provinceOptions = ref<DataOption[]>([])
const myform = ref<any>()
const imageUrl = ref<string | null>(null)
const disabledPic = ref<boolean>(false)
const fileData = ref<File | null>()
const props = defineProps({
prefixOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
genderOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
bloodOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
statusOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
religionOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
provinceOptions: {
type: Array as PropType<DataOption[]>,
required: true
},
statusEdit: {
type: Boolean,
required: true
},
notiNoEdit: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const emit = defineEmits(['update:statusEdit'])
onMounted(() => {
if (props.step !== 2) {
addData.value = false
}
// fetchProvince()
})
const saveData = async () => {
await myform.value.validate().then(async (success: boolean) => {
if (success) {
} else {
}
})
}
const changeBtn = async () => {
if (edit.value == true) {
if (props.statusEdit === true) {
edit.value = false
props.notiNoEdit()
} else {
emit('update:statusEdit', true)
}
} else {
emit('update:statusEdit', false)
}
}
const fetchProvince = async () => {
// // loader.value = true;
// await http
// .get(config.API.province)
// .then((res) => {
// const data = res.data.result
// let option: DataOption[] = []
// // console.log(data);
// data.map((r: any) => {
// option.push({ id: r.id.toString(), name: r.name.toString() })
// })
// provinceOptions.value = option
// })
// .catch((e: any) => {})
// .finally(() => {
// // loader.value = false;
// })
}
const savePic = () => {
disabledPic.value = false
}
const onFilePicked = () => {}
const pickFile = () => {
disabledPic.value = true
}
const cancelData = () => {
fileData.value = null
disabledPic.value = false
}
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>
<style>
.containerimage {
position: relative;
width: 100%;
}
.q-img {
display: block;
width: 160px;
height: 170px;
padding: 1%;
border-radius: 3px;
border: solid 2px rgba(168, 168, 168, 0.055) !important;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.048), 0 3px 6px 0 rgba(0, 0, 0, 0.19);
}
.overlay {
position: absolute;
bottom: 0;
background: rgb(0, 0, 0);
background: rgba(0, 0, 0, 0.7);
color: #f1f1f1;
transition: 0.5s ease;
width: 160px;
height: 70px;
opacity: 0;
color: white;
text-align: center;
cursor: pointer;
padding: 5% 0 5% 0;
}
.containerimage:hover .overlay {
opacity: 1;
}
</style>

View file

@ -0,0 +1,297 @@
<!-- card อาช -->
<template>
<q-card flat bordered class="col-12 q-px-lg q-py-md q-mt-md">
<HeaderTop
v-model:edit="edit"
header="อาชีพ"
icon="mdi-briefcase"
:save="saveData"
:addData="addData"
:cancel="cancelData"
:changeBtn="changeBtn"
/>
<q-form ref="myform">
<div class="row col-12 items-center q-col-gutter-x-xs q-col-gutter-y-xs">
<div class="col-12 row">
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.official"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:disable="occupationData.status !== 'official' || !edit"
>
<template v-slot:before>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="official"
label="ข้าราชการกรุงเทพมหานคร ตำแหน่ง"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
</template>
</q-input>
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.personnel"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:disable="occupationData.status !== 'personnel' || !edit"
>
<template v-slot:before>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="personnel"
label="บุคลากรกรุงเทพมหานคร ตำแหน่ง"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
</template>
</q-input>
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.officialsOther"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:disable="occupationData.status !== 'officialsOther' || !edit"
>
<template v-slot:before>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="officialsOther"
label="ข้าราชการประเภทอื่น ตำแหน่ง"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
</template>
</q-input>
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.employee"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:disable="occupationData.status !== 'employee' || !edit"
>
<template v-slot:before>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="employee"
label="ลูกจ้าง/พนักงานราชการของส่วนราชการอื่น ตำแหน่ง"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
</template>
</q-input>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="studying"
label="กำลังศึกษาต่อ"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
<q-input
:class="getClass(edit)"
hide-bottom-space
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.other"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:disable="occupationData.status !== 'other' || !edit"
>
<template v-slot:before>
<q-radio
v-model="occupationData.status"
checked-icon="task_alt"
unchecked-icon="panorama_fish_eye"
val="other"
label="อื่นๆ"
dense
:disable="!edit"
size="md"
style="font-size: 14px; color: black"
/>
</template>
</q-input>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.company"
:rules="[(val) => !!val || `${'กรุณากรอก สำนัก/บริษัท'}`]"
:label="`${'สำนัก/บริษัท'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.department"
:rules="[(val) => !!val || `${'กรุณากรอก กอง/ฝ่าย'}`]"
:label="`${'กอง/ฝ่าย'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.email"
:rules="[(val) => !!val || `${'กรุณากรอก E-mail address'}`]"
:label="`${'E-mail address'}`"
/>
</div>
<div class="col-xs-6 col-sm-3 col-md-3">
<q-input
:class="getClass(edit)"
hide-bottom-space
:outlined="edit"
dense
lazy-rules
type="textarea"
autogrow
:readonly="!edit"
:borderless="!edit"
v-model="occupationData.tel"
:rules="[(val) => !!val || `${'กรุณากรอก โทรศัพท์'}`]"
:label="`${'โทรศัพท์'}`"
/>
</div>
</div>
</q-form>
</q-card>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import http from '@/plugins/http'
import config from '@/app.config'
import type { Occupation } from '@/modules/01_exam/interface/index/Main'
import { defaultOccupation } from '@/modules/01_exam/interface/index/Main'
import HeaderTop from '@/components/top.vue'
const edit = ref<boolean>(false)
const addData = ref<boolean>(true)
const occupationData = ref<Occupation>(defaultOccupation)
const myform = ref<any>()
const props = defineProps({
statusEdit: {
type: Boolean,
required: true
},
notiNoEdit: {
type: Function,
default: () => console.log('not function')
},
step: {
type: Number,
required: true
}
})
const emit = defineEmits(['update:statusEdit'])
onMounted(() => {
if (props.step !== 2) {
addData.value = false
}
})
const saveData = async () => {
await myform.value.validate().then(async (success: boolean) => {
if (success) {
} else {
}
})
}
const changeBtn = async () => {
if (edit.value == true) {
if (props.statusEdit === true) {
edit.value = false
props.notiNoEdit()
} else {
emit('update:statusEdit', true)
}
} else {
emit('update:statusEdit', false)
}
}
const cancelData = () => {}
const getClass = (val: boolean) => {
return {
'full-width inputgreen cursor-pointer': val,
'full-width cursor-pointer': !val
}
}
</script>

View file

@ -0,0 +1,283 @@
interface Pagination {
rowsPerPage: number
}
interface DataDateMonthObject {
month: number
year: number
}
interface ChangeActive {
name: string
id: number
}
//ข้อมูลส่วนตัว
interface Information {
cardid: string
prefix: string
prefixId: string
firstname: string
lastname: string
birthDate: Date
genderId: string
bloodId: string
nationality: string
ethnicity: string
religionId: string
tel: string
phone: string
email: string
provinceId: string
cardIdDate: Date
statusId: string
knowledge: string
}
interface Family {
prefixC: string // couple
prefixIdC: string
firstnameC: string
lastnameC: string
occupationC: string
nationalityC: string
prefixM: string // male
prefixIdM: string
firstnameM: string
lastnameM: string
occupationM: string
nationalityM: string
prefixF: string // female
prefixIdF: string
firstnameF: string
lastnameF: string
occupationF: string
nationalityF: string
same: string
}
interface Occupation {
status: string
company: string
department: string
email: string
tel: string
official: string | null
personnel: string | null
officialsOther: string | null
employee: string | null
other: string | null
}
interface Address {
address: string
provinceId: string
districtId: string
subdistrictId: string
code: number
addressC: string
provinceIdC: string
districtIdC: string
subdistrictIdC: string
codeC: number
same: string
}
interface DataOption {
id: string
name: string
zipCode?: string
}
interface zipCodeOption {
id: string
name: string
zipCode: string
}
interface ExamCard {
id: string
title: string
announcementDate: Date
registerRound: Number
registerDateStart: Date
registerDateEnd: Date
yearly: Date
}
const defaultCard: ExamCard[] = [
{
id: '1',
title: 'การสอบภาค ข. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 2,
announcementDate: new Date('2022-11-23'),
registerDateStart: new Date('2022-11-09'),
registerDateEnd: new Date('2022-11-10'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
},
{
id: '2',
title: 'การสอบภาค ก. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว',
registerRound: 1,
announcementDate: new Date('2022-10-14'),
registerDateStart: new Date('2022-10-09'),
registerDateEnd: new Date('2022-11-30'),
yearly: new Date('2022-01-01')
}
]
const defaultAddress: Address = {
address: 'บ้านเลขที่ 1/2 ซอย 56 ถนนตัดใหม่',
provinceId: 'กรุงเทพ',
districtId: 'หนองแขม',
subdistrictId: 'หนองค้างพลู',
code: 10160,
addressC: 'บ้านเลขที่ 1/2 ซอย 56 ถนนตัดใหม่',
provinceIdC: 'กรุงเทพ',
districtIdC: 'หนองแขม',
subdistrictIdC: 'หนองค้างพลู',
codeC: 10160,
same: '0'
}
const defaultInformation: Information = {
cardid: '10238002345325',
prefix: 'นางสาว',
prefixId: 'นางสาว',
firstname: 'ณัฐกา',
lastname: 'ชมสิน',
birthDate: new Date('2002-01-01'),
genderId: 'หญิง',
bloodId: 'O',
nationality: 'ไทย',
ethnicity: 'ไทย',
religionId: 'พุทธ',
tel: '0914569982',
phone: '0914569982',
email: 'kittapath@frappet.com',
provinceId: 'กรุงเทพ',
cardIdDate: new Date('2000-01-10'),
statusId: 'โสด',
knowledge: 'excel, word, photoshop'
}
const defaultFamily: Family = {
prefixC: 'นาย',
prefixIdC: 'นาย',
firstnameC: 'ธนาคาร',
lastnameC: 'กสิกร',
occupationC: 'ว่าง',
nationalityC: 'ไทย',
prefixM: 'นาย',
prefixIdM: 'นาย',
firstnameM: 'ธนายุทธ',
lastnameM: 'ชมสิน',
occupationM: 'ว่าง',
nationalityM: 'ไทย',
prefixF: 'นางสาว',
prefixIdF: 'นางสาว',
firstnameF: 'ณัฐกาล',
lastnameF: 'ชมสิน',
occupationF: 'ว่าง',
nationalityF: 'ไทย',
same: '1'
}
const defaultOccupation: Occupation = {
status: 'official',
company: 'บริษัท ทรู คอร์ปอเรชั่น จำกัด (มหาชน)',
department: '-',
email: 'kittapath@frappet.com',
tel: '0846464646',
official: 'Direct Sales Staff',
personnel: null,
officialsOther: null,
employee: null,
other: null
}
export { defaultInformation, defaultFamily, defaultAddress, defaultOccupation, defaultCard }
export type {
Pagination,
DataOption,
DataDateMonthObject,
ChangeActive,
Information,
Family,
Address,
zipCodeOption,
Occupation,
ExamCard
}

View file

@ -0,0 +1,29 @@
interface DataProps {
row: RequestItemsObject
rowIndex: number
}
//ข้อมูล
interface RequestItemsObject {
id: string
location: string
position: string
salary: number | null
duration: [Date, Date]
reason: string
}
//columns
interface Columns {
[index: number]: {
name: String
align: String
label: String
sortable: Boolean
field: String
headerStyle: String
style: String
}
}
export type { RequestItemsObject, Columns, DataProps }

View file

@ -0,0 +1,30 @@
interface DataProps {
row: RequestItemsObject
rowIndex: number
}
//ข้อมูล
interface RequestItemsObject {
id: string
qualificationId: string
qualification: string
major: string
scores: number | null
name: string
duration: [Date, Date]
}
//columns
interface Columns {
[index: number]: {
name: String
align: String
label: String
sortable: Boolean
field: String
headerStyle: String
style: String
}
}
export type { RequestItemsObject, Columns, DataProps }

View file

@ -0,0 +1,30 @@
//ข้อมูล
interface RequestItemsObject {
id: number;
fullname: String;
avatar: String;
citizenId: String;
position: String;
line: String;
linePosition: String;
level: String;
positionFormalManage: String;
positionManage: String;
numberPosition: String;
government: String;
}
//columns
interface Columns {
[index: number]: {
name: String;
align: String;
label: String;
sortable: Boolean;
field: String;
headerStyle: String;
style: String;
};
}
export type { RequestItemsObject, Columns };

View file

@ -0,0 +1,11 @@
//ข้อมูล
interface ResponseObject {
id: string
location: string
position: string
salary: number | null
duration: [Date, Date]
reason: string
}
export type { ResponseObject }

View file

@ -0,0 +1,12 @@
//ข้อมูล
interface ResponseObject {
id: string
qualificationId: string
qualification: string
major: string
scores: number | null
name: string
duration: [Date, Date]
}
export type { ResponseObject }

View file

@ -0,0 +1,17 @@
//ข้อมูล
interface ResponseObject {
id: number;
fullname: String;
avatar: String;
citizenId: String;
position: String;
line: String;
linePosition: String;
level: String;
positionFormalManage: String;
positionManage: String;
numberPosition: String;
government: String;
}
export type { ResponseObject };

View file

@ -0,0 +1,23 @@
const Main = () => import('@/modules/01_exam/views/ExamMain.vue')
const Detail = () => import('@/modules/01_exam/views/ExamDetail.vue')
export default [
// {
// path: '/exam',
// name: 'exam',
// component: Main,
// meta: {
// Auth: true
// // Key: [7]
// }
// },
{
path: '/exam/:id',
name: 'examDetail',
component: Detail,
meta: {
Auth: true,
Key: [7]
}
}
]

View file

@ -0,0 +1,32 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useExamDataStore = defineStore('exam', () => {
interface exam {
main: { columns: String[] }
education: { columns: String[] }
career: { columns: String[] }
}
const examData = ref<exam>({
main: { columns: [] },
education: { columns: [] },
career: { columns: [] }
})
const changeExamColumns = (system: String, val: String[]) => {
if (system == 'main') examData.value.main.columns = val
if (system == 'education') examData.value.education.columns = val
if (system == 'career') examData.value.career.columns = val
localStorage.setItem('exam', JSON.stringify(examData.value))
}
if (localStorage.getItem('exam') !== null) {
examData.value = JSON.parse(localStorage.getItem('exam') || '{}')
}
return {
examData,
changeExamColumns
}
})

View file

@ -0,0 +1,152 @@
<!-- page:detail page รายการสอบทงหมด -->
<template>
<q-toolbar class="q-py-sm q-px-md bg-grey-2">
<q-toolbar-title class="toptitle text-dark col-12 row items-center">
{{ tittle }}
<q-space />
<q-btn color="primary" label="ออกจากระบบ" push size="sm" v-close-popup @click="doLogout" />
</q-toolbar-title>
</q-toolbar>
<q-card flat class="">
<div class="justify-center">
<!-- done-color="deep-orange"
active-color="purple"
inactive-color="secondary" -->
<q-stepper
v-model="step"
ref="stepper"
alternative-labels
header-nav
animated
done-color="positive"
active-color="blue-7"
inactive-color="positive"
done-icon="check"
:active-icon="stepRaw === step && stepRaw != 4 ? 'mdi-pencil-outline' : 'mdi-eye-outline'"
>
<q-step
:done="step > 1"
:disable="stepRaw < 1"
:name="1"
title="อ่านคำชี้แจง"
:icon="
stepRaw >= 1 ? (stepRaw == 1 ? 'mdi-pencil-outline' : 'check') : 'mdi-pencil-outline'
"
>
<ExamDetail :fetchStep="fetchStep" :step="stepRaw" />
</q-step>
<q-step
:done="step > 2"
:disable="stepRaw < 2"
:name="2"
title="ข้อมูลสมัครสอบ"
:icon="
stepRaw >= 2 ? (stepRaw == 2 ? 'mdi-pencil-outline' : 'check') : 'mdi-pencil-outline'
"
>
<ExamForm :fetchStep="fetchStep" :step="stepRaw" />
</q-step>
<q-step
:done="step > 3"
:disable="stepRaw < 3"
:name="3"
title="ชำระค่าธรรมเนียมการสอบ"
:icon="
stepRaw >= 3 ? (stepRaw == 3 ? 'mdi-pencil-outline' : 'check') : 'mdi-pencil-outline'
"
>
<ExamPayment :fetchStep="fetchStep" :step="stepRaw" />
</q-step>
<q-step
:done="step > 4"
:disable="stepRaw < 4"
:name="4"
title="สำเร็จ"
:icon="
stepRaw >= 4 ? (stepRaw == 4 ? 'mdi-pencil-outline' : 'check') : 'mdi-pencil-outline'
"
>
<ExamFinished :fetchStep="fetchStep" :step="stepRaw" />
</q-step>
</q-stepper>
</div>
</q-card>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import ExamDetail from '@/modules/01_exam/components/ExamDetail.vue'
import ExamForm from '@/modules/01_exam/components/ExamForm.vue'
import ExamPayment from '@/modules/01_exam/components/ExamPayment.vue'
import ExamFinished from '@/modules/01_exam/components/ExamFinished.vue'
import keycloak from '@/plugins/keycloak'
import { useQuasar } from 'quasar'
const $q = useQuasar()
const examId = ref<string>('zxc')
const step = ref<number>(1)
const stepRaw = ref<number>(1)
const examPost = ref<any>()
const test = ref<any>()
const loading = ref<boolean>(false)
const tittle = ref<string>('การสอบภาค ข. พิเศษ สำหรับผู้สอบผ่านของส่วนราชการแล้ว')
onMounted(() => {
loadCandidate()
fetchExams()
})
const loadCandidate = () => {
// this.loader = true
// this.$http
// .get(this.$config.API.getCandidateCurrentStep(this.examId, this.$keycloak.tokenParsed.sub))
// .then((res) => {
// this.step = res.data.items
// this.stepRaw = res.data.items
// })
// .catch((e) => {
// this.step = 1
// })
// .finally(() => {
// this.loader = false
// })
}
const fetchExams = () => {
// const headers = { 'Content-Type': 'application/graphql' }
// this.loading = true
// this.$http
// .post(
// this.$config.API.getOrchardPath(),
// this.$config.API.getExamPostsForExamDetailQuery(this.$route.params.examId),
// { headers }
// )
// .then((response) => {
// this.examPost = response.data.data.examPost[0]
// this.test = this.examPost.bag.contentItems
// })
// .catch((err) => console.log(err))
// .finally(() => (this.loading = false))
}
const fetchStep = () => {
stepRaw.value += 1
step.value += 1
}
/**
* logout keycloak
* confirm อนออกจากระบบ
*/
const doLogout = () => {
$q.dialog({
title: 'ยืนยันการออกจากระบบ',
message: `ต้องการออกจากระบบใช้หรือไม่?`,
cancel: 'ยกเลิก',
ok: 'ยืนยัน',
persistent: true
}).onOk(() => {
keycloak.logout()
})
}
</script>

View file

@ -0,0 +1,235 @@
<!-- page:main page รายการสอบทงหมด -->
<template>
<q-toolbar class="q-py-sm q-px-md bg-grey-2">
<q-toolbar-title class="toptitle text-dark col-12 row items-center"
>รายการสอบทงหมด</q-toolbar-title
>
</q-toolbar>
<div class="q-pa-md q-gutter-md">
<CardExam v-for="row in ExamData" :key="row.id" :items="row" @click="next(row.id)" />
<!-- <data-table
style="height: 80vh"
:rows="rows"
:columns="columns"
:filter="filter"
:visible-columns="visibleColumns"
v-model:inputfilter="filter"
v-model:inputvisible="visibleColumns"
:pagination="initialPagination"
:nornmalData="true"
:paging="true"
>
<template #columns="props">
<q-tr :props="props" @click="next(props.row.id)" class="cursor-pointer">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.name == 'no'" class="table_ellipsis">
{{ props.rowIndex + 1 }}
</div>
<div v-else-if="col.name == 'fullname'">
<div class="row col-12 items-center">
<img
:src="props.row.avatar"
class="q-mr-sm col-4"
style="width: 28px; height: 28px; border-radius: 50%"
/>
<div class="col-4">
<div class="text-weight-medium">
{{ props.row.fullname }}
</div>
<div class="text-weight-light">
{{ props.row.citizenId }}
</div>
</div>
</div>
</div>
<div v-else class="table_ellipsis">
{{ col.value }}
</div>
</q-td>
</q-tr>
</template>
</data-table> -->
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useExamDataStore } from '@/modules/01_exam/store'
import type { RequestItemsObject, Columns } from '@/modules/01_exam/interface/request/Main'
import type { ResponseObject } from '@/modules/01_exam/interface/response/Main'
import type { Pagination, ExamCard } from '@/modules/01_exam/interface/index/Main'
import { defaultCard } from '@/modules/01_exam/interface/index/Main'
import CardExam from '../components/ExamCrad.vue'
const router = useRouter()
const store = useExamDataStore()
const { examData, changeExamColumns } = store
const filter = ref<string>('') //search data table
const initialPagination = ref<Pagination>({
rowsPerPage: 0
})
const ExamData = ref<ExamCard[]>(defaultCard)
const visibleColumns = ref<String[]>([])
examData.main.columns.length == 0
? (visibleColumns.value = [
'no',
'fullname',
'position',
'line',
'linePosition',
'level',
'positionFormalManage',
'positionManage',
'numberPosition',
'government'
])
: (visibleColumns.value = examData.main.columns)
const columns = ref<Columns>([
{
name: 'no',
align: 'left',
label: 'ลำดับ',
sortable: true,
field: 'no',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'fullname',
align: 'left',
label: 'ชื่อ-สกุล',
sortable: true,
field: 'fullname',
headerStyle: 'font-size: 14px; min-width: 200px',
style: 'font-size: 14px; '
},
{
name: 'position',
align: 'left',
label: 'ตำแหน่ง',
sortable: true,
field: 'position',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'line',
align: 'left',
label: 'สายงาน',
sortable: true,
field: 'line',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'linePosition',
align: 'left',
label: 'ตำแหน่งในสายงาน',
sortable: true,
field: 'linePosition',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'level',
align: 'left',
label: 'ระดับ',
sortable: true,
field: 'level',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'positionFormalManage',
align: 'left',
label: 'ตำแหน่งทางการบริหาร',
sortable: true,
field: 'positionFormalManage',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'positionManage',
align: 'left',
label: 'ตำแหน่งการบริหาร',
sortable: true,
field: 'positionManage',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'numberPosition',
align: 'left',
label: 'เลขที่ตำแหน่ง',
sortable: true,
field: 'numberPosition',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
},
{
name: 'government',
align: 'left',
label: 'หน่วยงาน/ส่วนราชการ',
sortable: true,
field: 'government',
headerStyle: 'font-size: 14px',
style: 'font-size: 14px'
}
])
const rows = ref<RequestItemsObject[]>([
{
id: 1,
fullname: 'นางสาวณัฐกา ชมสิน',
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
citizenId: '4016500103241',
position: 'นักจัดการงานทั่วไป',
line: 'จัดการทั่วไป',
linePosition: 'ทั่วไป ',
level: 'ต้น',
positionFormalManage: 'นักจัดการทั่วไป',
positionManage: 'นักจัดการทั่วไป',
numberPosition: 'กทข.1',
government: 'กองบริหารทั่วไป'
},
{
id: 2,
fullname: 'นางสาวรัชภรณ์ ภักดี',
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
citizenId: '4016500092355',
position: 'นักจัดการงานทั่วไป',
line: 'จัดการทั่วไป',
linePosition: 'ทั่วไป ',
level: 'ต้น',
positionFormalManage: 'นักจัดการทั่วไป',
positionManage: 'นักจัดการทั่วไป',
numberPosition: 'กทข.1',
government: 'กองบริหารทั่วไป'
},
{
id: 3,
fullname: 'นางสาวภาพรรณ ลออ',
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
citizenId: '4016500086436',
position: 'นักจัดการงานทั่วไป',
line: 'จัดการทั่วไป',
linePosition: 'ทั่วไป ',
level: 'ต้น',
positionFormalManage: 'นักจัดการทั่วไป',
positionManage: 'นักจัดการทั่วไป',
numberPosition: 'กทข.1',
government: 'กองบริหารทั่วไป'
}
])
watch(visibleColumns, async (count: String[], prevCount: String[]) => {
await changeExamColumns('main', count)
})
const next = (id: string) => {
router.push(`/exam/${id}`)
}
</script>
<style></style>

View file

@ -1,13 +0,0 @@
const Meta = () => import('@/modules/01_meta/views/Meta01View.vue')
export default [
{
path: '/meta01',
name: 'meta01',
component: Meta,
meta: {
Auth: true
// Key: [7]
}
}
]

View file

@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about META01</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View file

@ -0,0 +1,10 @@
interface Pagination {
rowsPerPage: number
}
interface DataOption {
id: string
name: string
}
export type { Pagination, DataOption }

View file

@ -0,0 +1,28 @@
interface DataProps {
row: RequestItemsObject
rowIndex: number
}
//ข้อมูล
interface RequestItemsObject {
id: string
name: string
certiNumber: string
start: Date
end: Date
}
//columns
interface Columns {
[index: number]: {
name: String
align: String
label: String
sortable: Boolean
field: String
headerStyle: String
style: String
}
}
export type { RequestItemsObject, Columns, DataProps }

View file

@ -0,0 +1,11 @@
//ข้อมูล
interface ResponseObject {
id: string
date: Date
status: string
level: string
refNo: string
refDate: Date
}
export type { ResponseObject }

View file

@ -0,0 +1,10 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useMetaStore = defineStore('meta', () => {
const meta = ref<string>('')
return {
meta
}
})

View file

@ -0,0 +1,21 @@
<!-- แสดง ui การโหลด -->
<template>
<div>
<q-inner-loading :showing="loaderVisibility">
<q-spinner-cube size="80px" color="primary" />
</q-inner-loading>
</div>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
const props = defineProps({
visibility: Boolean,
});
const loaderVisibility = ref<boolean>(props.visibility);
watch(props, (count, prevCount) => {
loaderVisibility.value = props.visibility;
});
</script>

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

@ -0,0 +1,25 @@
import axios from "axios"
import config from "process"
// import { dotnetPath } from "../path/axiosPath";
// import { getToken } from "@baloise/vue-keycloak";
import keycloak from "../plugins/keycloak"
const axiosInstance = axios.create({
withCredentials: false,
})
// axiosInstance.defaults.baseURL = dotnetPath;
axiosInstance.interceptors.request.use(
async (config) => {
const token = await keycloak.token
config.headers = {
Authorization: `Bearer ${token}`,
}
return config
},
(error) => {
Promise.reject(error)
}
)
export default axiosInstance

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

@ -0,0 +1,45 @@
import Axios, { type AxiosRequestConfig, type AxiosResponse } from "axios";
import keycloak from "./keycloak";
const http = Axios.create({
timeout: 1000000000, // เพิ่มค่า timeout
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
http.interceptors.request.use(
async function (config: AxiosRequestConfig<any>) {
await keycloak.updateToken(1);
config.headers = config.headers ?? {};
const token = keycloak.token;
// const token = localStorage.getItem("access_token")
// const token =
// "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxU2VKV2dVRFVlNXZwNS13Q1ZHaG9lT2l4bDJTTkdKemthLU5ZN211NXZJIn0.eyJleHAiOjE2NzI0MTI1NDksImlhdCI6MTY3MjM3NjU0OSwiYXV0aF90aW1lIjoxNjcyMzc2NTQ5LCJqdGkiOiI1MTVhY2IwNC1jODQ3LTQzM2YtYjUxOC03ODUzMzJhY2ZjNWYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLmZyYXBwZXQuc3lub2xvZ3kubWUvYXV0aC9yZWFsbXMvYm1hLWVociIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlZmM5YjRlMC1mZGU2LTQ1NDQtYmU1OS1lMTA0MjEwMjUzZjAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJibWEtZWhyIiwibm9uY2UiOiI3NjMyMGI3ZS0xZTMxLTQ5ODYtYWIzOC1iOTUyYjFlODY3OGYiLCJzZXNzaW9uX3N0YXRlIjoiMDZlNTBkZjktNzAyNi00ZGIwLTkxMjgtMWY3Y2FiYTRkNDEyIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2xvY2FsaG9zdDo3MDA2Il0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWJtYS1laHIiLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiMDZlNTBkZjktNzAyNi00ZGIwLTkxMjgtMWY3Y2FiYTRkNDEyIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiZGVmYXVsdC1yb2xlcy1ibWEtZWhyIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl0sIm5hbWUiOiJTeXN0ZW0gQWRtaW5pc3RyYXRvciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6IlN5c3RlbSIsImZhbWlseV9uYW1lIjoiQWRtaW5pc3RyYXRvciIsImVtYWlsIjoiYWRtaW5AbG9jYWxob3N0In0.xmfJ3pzI-jLYsaiFXyjTW7gfAEpvUmMVsp9BsB1CfRCVOKiGBbuZhnQY8W-1SWVAx1NjJ55L-zMHPK6hk1dRPLbEse3DlIBZw04W9j8m-Wz3eqdHf_UCjmrXb8qAwkeq0Iaxq9mVfJJeQWeKhFBi-Ff8ek4hCXTYDICXS8ny_BaC5WkyrefHQ2xBqQjwRyoxsg4IoVMjXYNb8L9A-4BNlRfs928SqgFYCRlF5h6zw_rC0XoLrGTmqeacBdpey-r3j2g_lTqWy8mQg2T9s65IDqW3kFPOsr0SVO88sjlFbN9Et0L57RmiqORk_RwzbWg-_Yb6dOuolXsnjBOhOoTzkA";
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (error: any) {
return Promise.reject(error);
}
);
http.interceptors.response.use(
function (response: AxiosResponse<any, any>) {
return response;
},
function (error: any) {
if (typeof error !== undefined) {
// eslint-disable-next-line no-prototype-builtins
if (error.hasOwnProperty("response")) {
if (error.response.status === 401 || error.response.status === 403) {
// Store.commit("SET_ERROR_MESSAGE", error.response.data.message);
// Store.commit("REMOVE_ACCESS_TOKEN")
}
}
}
return Promise.reject(error);
}
);
export default http;

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

@ -0,0 +1,23 @@
/**
* front connect to keycloak
*/
import Keycloak from 'keycloak-js'
// import config from "../app.config";
// import http from "../shared/http";
// import router from "../router";
const initOptions = {
// url: "https://keycloak.frappet.synology.me/auth/",
// realm: "bma-ehr",
// clientId: "bma-ehr-vue3",
url: 'https://identity.frappet.com/',
realm: 'exam_test',
clientId: 'exam_vue3'
} //option keycloak ที่จะ connect
const keycloak = Keycloak(initOptions)
keycloak.onAuthSuccess = () => {} //เพิ่มlogin สำเร็จจะมาทำฟังก์ชันนี้
await keycloak.init({ checkLoginIframe: false }) //ทำการ connect keycloak
export default keycloak

View file

@ -1,31 +1,34 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Meta01 from '@/modules/01_meta/router'
import Exam from '@/modules/01_exam/router'
import Meta02 from '@/modules/02_meta/router'
import keycloak from '@/plugins/keycloak'
const MainLayout = () => import('@/views/MainLayout.vue')
const Error404NotFound = () => import('@/views/Error404NotFound.vue')
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
path: '/exam/:id',
// name: 'home',
component: MainLayout,
children: [
{
path: '/',
name: 'dashboard',
component: HomeView,
meta: {
Auth: true,
Key: [7]
}
},
...Meta01,
...Meta02
// {
// path: '/',
// name: 'dashboard',
// component: HomeView,
// meta: {
// Auth: true,
// Key: [7]
// }
// },
...Exam
// ...Meta02
]
}
},
/**
* 404 Not Found
* ref: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
@ -35,6 +38,11 @@ const router = createRouter({
// path: "/:pathMatch(.*)*",
// component: Error404NotFound,
// },
{
// path: "/:catchAll(.*)*", // TODO: ใช้ pathMatch แทนตามในเอกสารแนะนำ คงไว้เผื่อจำเป็น
path: '/:pathMatch(.*)*',
component: Error404NotFound
}
]
})
@ -56,5 +64,21 @@ const router = createRouter({
// }
// ]
// })
router.beforeEach((to, from, next) => {
// if (to.meta.Auth) {
// if (!keycloak.authenticated) {
// keycloak.login({
// redirectUri: `${window.location.protocol}//${window.location.host}${to.path}`,
// locale: 'th'
// })
// } else {
// // keycloak.updateToken(60);
// next()
// }
// } else {
// next()
// }
next()
})
export default router

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

@ -0,0 +1,348 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import moment from 'moment'
import type { DataDateMonthObject } from '@/modules/01_exam/interface/index/Main'
export const useCounterMixin = defineStore('mixin', () => {
/**
*
*/
const calAge = (srcDate: Date, birthCal: Date = new Date(), eng: boolean = false) => {
const toDay = birthCal
const birth = new Date(srcDate)
const yearNow = toDay.getFullYear()
const monthNow = toDay.getMonth()
const dateNow = toDay.getDate()
const yearDob = birth.getFullYear()
const monthDob = birth.getMonth()
const dateDob = birth.getDate()
const lastYear = 12
const subtractDate: Object = moment().subtract(1, 'months').endOf('month')
const lastMonths = new Date(subtractDate.toString()).getDate()
let yearAge = yearNow - yearDob
let monthAge = 0
let dateAge = 0
if (monthNow >= monthDob) {
monthAge = monthNow - monthDob
} else {
yearAge--
monthAge = lastYear + monthNow - monthDob
}
if (dateNow >= dateDob) {
dateAge = dateNow - dateDob
} else {
monthAge--
dateAge = lastMonths + dateNow - dateDob
if (monthAge < 0) {
monthAge = 11
yearAge--
}
}
const age = {
years: yearAge,
months: monthAge,
days: dateAge
}
const year = eng ? 'years' : 'ปี'
const month = eng ? 'months' : 'เดือน'
const day = eng ? 'days' : 'วัน'
return `${yearAge} ${year} ${monthAge} ${month} ${dateAge} ${day}`
}
function date2Thai(srcDate: Date, isFullMonth: boolean = false, isTime: boolean = false) {
const date = new Date(srcDate)
const isValidDate = Boolean(+date)
if (!isValidDate) return srcDate.toString()
if (isValidDate && date.getFullYear() < 1000) return srcDate.toString()
const fullMonthThai = [
'มกราคม',
'กุมภาพันธ์',
'มีนาคม',
'เมษายน',
'พฤษภาคม',
'มิถุนายน',
'กรกฎาคม',
'สิงหาคม',
'กันยายน',
'ตุลาคม',
'พฤศจิกายน',
'ธันวาคม'
]
const abbrMonthThai = [
'ม.ค.',
'ก.พ.',
'มี.ค.',
'เม.ย.',
'พ.ค.',
'มิ.ย.',
'ก.ค.',
'ส.ค.',
'ก.ย.',
'ต.ค.',
'พ.ย.',
'ธ.ค.'
]
let dstYear = 0
if (date.getFullYear() > 2500) {
dstYear = date.getFullYear()
} else {
dstYear = date.getFullYear() + 543
}
let dstMonth = ''
if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()]
} else {
dstMonth = abbrMonthThai[date.getMonth()]
}
let dstTime = ''
if (isTime) {
const H = date.getHours().toString().padStart(2, '0')
const M = date.getMinutes().toString().padStart(2, '0')
// const S = date.getSeconds().toString().padStart(2, "0")
// dstTime = " " + H + ":" + M + ":" + S + " น."
dstTime = ' ' + H + ':' + M + ' น.'
}
return date.getDate().toString().padStart(2, '0') + ' ' + dstMonth + ' ' + dstYear + dstTime
}
function monthYear2Thai(month: number, year: number, isFullMonth = false) {
const date = new Date(`${year}-${month + 1}-1`)
const fullMonthThai = [
'มกราคม',
'กุมภาพันธ์',
'มีนาคม',
'เมษายน',
'พฤษภาคม',
'มิถุนายน',
'กรกฎาคม',
'สิงหาคม',
'กันยายน',
'ตุลาคม',
'พฤศจิกายน',
'ธันวาคม'
]
const abbrMonthThai = [
'ม.ค.',
'ก.พ.',
'มี.ค.',
'เม.ย.',
'พ.ค.',
'มิ.ย.',
'ก.ค.',
'ส.ค.',
'ก.ย.',
'ต.ค.',
'พ.ย.',
'ธ.ค.'
]
let dstYear = 0
if (date.getFullYear() > 2500) {
dstYear = date.getFullYear()
} else {
dstYear = date.getFullYear() + 543
}
let dstMonth = ''
if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()]
} else {
dstMonth = abbrMonthThai[date.getMonth()]
}
return dstMonth + ' ' + dstYear
}
function dateToISO(date: Date) {
return (
date.getFullYear() +
'-' +
appendLeadingZeroes(date.getMonth() + 1) +
'-' +
appendLeadingZeroes(date.getDate())
)
}
function appendLeadingZeroes(n: Number) {
if (n <= 9) return '0' + n
return n
}
function textToPhone(n: string) {
const p = n.substr(0, 3) + '-' + n.substr(3, 3) + '-' + n.substr(6, 4)
return p
}
function textToFax(n: string) {
const p = n.substr(0, 2) + '-' + n.substr(2, 3) + '-' + n.substr(5, 4)
return p
}
const success = (q: any, val: string) => {
// useQuasar ไม่สามารถใช้นอกไฟล์ .vue
if (val !== '') {
return q.notify({
message: val,
color: 'primary',
icon: 'mdi-information',
position: 'bottom-right',
multiLine: true,
timeout: 1000,
badgeColor: 'positive',
classes: 'my-notif-class'
})
}
}
function notify(q: any, val: string) {
if (val !== '') {
q.notify({
color: 'teal-10',
message: val,
icon: 'mdi-information',
position: 'bottom-right',
multiLine: true,
timeout: 7000,
actions: [{ label: 'ปิด', color: 'white', handler: () => {} }]
})
}
}
function notifyError(q: any, val: string) {
if (val !== '') {
q.notify({
color: 'negative',
message: val,
icon: 'mdi-alert-circle',
position: 'top',
multiLine: true,
timeout: 12000,
actions: [{ label: 'ปิด', color: 'white', handler: () => {} }]
})
}
}
const dateText = (val: Date) => {
if (val != null) {
return date2Thai(val)
} else {
return '-'
}
}
const weekThai = (val: Number) => {
switch (val) {
case 0:
return 'วันอาทิตย์'
case 1:
return 'วันจันทร์'
case 2:
return 'วันอังคาร'
case 3:
return 'วันพุธ'
case 4:
return 'วันพฤหัสบดี'
case 5:
return 'วันศุกร์'
case 6:
return 'วันเสาร์'
default:
return '-'
}
}
const genColor15 = (val: number) => {
val = val % 15
switch (val) {
case 1:
return 'pink'
case 2:
return 'purple'
case 3:
return 'deep-purple'
case 4:
return 'indigo'
case 5:
return 'blue'
case 6:
return 'light-blue'
case 7:
return 'cyan'
case 8:
return 'teal'
case 9:
return 'green'
case 10:
return 'light-green'
case 11:
return 'amber'
case 12:
return 'orange'
case 13:
return 'deep-orange'
case 14:
return 'brown'
case 0:
return 'blue-grey'
default:
return ''
}
}
/**
* 2
* @param val
*/
const dateThaiRange = (val: [Date, Date]) => {
if (val === null) {
return ''
} else if (date2Thai(val[0]) === date2Thai(val[1])) {
return `${date2Thai(val[0])}`
} else {
return `${date2Thai(val[0])} - ${date2Thai(val[1])}`
}
}
/**
* 2
* @param val
*/
const monthThaiRange = (val: DataDateMonthObject[]) => {
if (val === null || val[0] === null || val[1] === null) {
return ''
} else if (
monthYear2Thai(val[0].month, val[0].year) === monthYear2Thai(val[1].month, val[1].year)
) {
return `${monthYear2Thai(val[0].month, val[0].year)}`
} else {
return `${monthYear2Thai(val[0].month, val[0].year)} - ${monthYear2Thai(
val[1].month,
val[1].year
)}`
}
}
return {
calAge,
date2Thai,
dateToISO,
notify,
notifyError,
dateText,
monthYear2Thai,
success,
weekThai,
genColor15,
textToPhone,
textToFax,
dateThaiRange,
monthThaiRange
}
})

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Error404NotFound'
})
</script>
<template>
<div class="fullscreen bg-blue-10 text-white text-center q-pa-md flex flex-center">
<div>
<div class="text-h1">ไมพบหนาทองการ</div>
<div class="text-h2">(404 Not Found)</div>
<!-- <q-btn class="q-mt-xl" color="white" text-color="blue" unelevated to="/" label="กลับไปหน้าหลัก" no-caps /> -->
</div>
</div>
</template>

View file

@ -1,11 +1,14 @@
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { ref } from 'vue'
// import keycloak from '@/plugins/keycloak'
import { useRoute } from 'vue-router'
import { menuList, options, notiList } from '../interface/main/index'
import keycloak from '@/plugins/keycloak'
const route = useRoute()
const $q = useQuasar()
const miniState = ref<boolean>(false)
const active = ref<number>(0)
const drawerL = ref<boolean>(false)
@ -37,19 +40,15 @@ const activeMenu = (path: string) => {
* confirm อนออกจากระบบ
*/
const doLogout = () => {
// this.$q
// .dialog({
// title: "",
// message: `?`,
// cancel: "",
// ok: "",
// persistent: true,
// })
// .onOk(() => {
// // this.$router.push("/logout")
// keycloak.logout()
// });
$q.dialog({
title: 'ยืนยันการออกจากระบบ',
message: `ต้องการออกจากระบบใช้หรือไม่?`,
cancel: 'ยกเลิก',
ok: 'ยืนยัน',
persistent: true
}).onOk(() => {
keycloak.logout()
})
}
/**
@ -64,12 +63,12 @@ const doLogout = () => {
<template>
<!-- แบบเก design แรก -->
<!-- <q-layout view="lHh Lpr lff"> -->
<!-- ปรบใหบหน รายละเอยดทะเบยนประว -->
<q-layout view="lHh LpR lff" @scroll="onScroll">
<!-- ปรบใหบหน รายละเอยดรายการสอบทงหมด -->
<q-layout view="lHh LpR lff">
<!-- header -->
<q-header flat class="bg-grey-2 text-dark" height-hint="7">
<q-toolbar style="padding: 0 2%">
<q-btn
<!-- <q-btn
size="13px"
class="bg-grey-3"
flat
@ -83,7 +82,7 @@ const doLogout = () => {
size="20px"
color="grey-7"
/>
</q-btn>
</q-btn> -->
<q-space />
@ -93,12 +92,12 @@ const doLogout = () => {
<q-icon name="mdi-magnify" size="20px" color="grey-7" />
</template>
</q-input> -->
<q-btn class="bg-grey-3" flat dense round>
<!-- <q-btn class="bg-grey-3" flat dense round>
<q-icon name="mdi-magnify" size="20px" color="grey-7" />
</q-btn>
</q-btn> -->
<!-- Notification -->
<q-btn
<!-- <q-btn
round
dense
flat
@ -146,7 +145,6 @@ const doLogout = () => {
<q-item v-close-popup class="q-pa-none">
<q-item-section avatar>
<q-avatar color="grey-3">
<!-- <img src="https://cdn.quasar.dev/img/avatar.png" /> -->
<q-icon name="mdi-account" size="22px" color="grey-7" />
</q-avatar>
</q-item-section>
@ -165,7 +163,6 @@ const doLogout = () => {
<div class="column items-center col-12 q-py-md" color="grey-3">
<q-avatar size="72px" color="grey-4">
<q-icon name="mdi-account" color="grey-7" />
<!-- <img :src="require('@/assets/logo.png')" /> -->
</q-avatar>
<div class="text-subtitle2 q-mt-md q-mb-xs text-center">
{{ fullname }}
@ -177,14 +174,13 @@ const doLogout = () => {
size="sm"
v-close-popup
@click="doLogout"
/><!-- -->
/>
</div>
<div class="column col-12">
<q-separator />
<div class="column q-pb-md justify-center">
<div class="text-overline text-grey q-px-md q-pt-sm">เลอกโหมด</div>
<!-- <q-option-group v-model="group" :options="options" color="primary"/> -->
<q-list dense v-for="op in options" :key="op.label">
<q-item clickable>
<q-item-section avatar>
@ -203,13 +199,13 @@ const doLogout = () => {
</div>
</div>
</q-btn-dropdown>
</div>
</div> -->
</q-toolbar>
</q-header>
<!-- end header -->
<!-- drawer -->
<q-drawer
<!-- <q-drawer
side="left"
bordered
class="text-white"
@ -249,33 +245,6 @@ const doLogout = () => {
<q-separator inset color="grey-9" />
<q-list padding>
<div v-for="(menuItem, index) in menuList" :key="index">
<!-- <q-expansion-item
group="somegroup"
class="menuSub"
expand-icon="mdi-menu-right"
expanded-icon="mdi-menu-down"
v-if="menuItem.key == 3 || menuItem.key == 5"
>
<template v-slot:header>
<q-item-section avatar>
<q-avatar :icon="menuItem.icon" size="md" font-size="20px" />
</q-item-section>
<q-item-section>{{ menuItem.label }}</q-item-section>
</template>
<q-item
dense
class="menuSubHover"
active-class="text-primary active-item text-weight-bold menuSubAct"
clickable
v-for="subMenu in menuItem.children"
:key="subMenu.key"
:to="{ name: `${subMenu.path}` }"
>
<q-item-section>
<q-item-label>{{ subMenu.label }}</q-item-label>
</q-item-section>
</q-item>
</q-expansion-item> -->
<q-item
class="text-weight-medium menu"
active-class="text-primary active-item text-weight-bold menuActive"
@ -286,7 +255,6 @@ const doLogout = () => {
dense
exact
>
<!-- v-else -->
<q-item-section avatar>
<q-avatar size="md" font-size="20px">
<q-icon :name="menuItem.key === active ? menuItem.activeIcon : menuItem.icon" />
@ -300,7 +268,7 @@ const doLogout = () => {
</div>
</q-list>
</q-scroll-area>
</q-drawer>
</q-drawer> -->
<q-page-container class="bg-grey-2">
<q-page style="padding: 0 2%">
@ -366,7 +334,6 @@ const doLogout = () => {
.toptitle {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1%;
}
.q-field--outlined .q-field__control {

View file

@ -8,7 +8,9 @@ import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/style/quasar-variables.sass'
}),
@ -18,5 +20,8 @@ export default defineConfig({
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
target: 'esnext'
}
})