diff --git a/src/api/org/api.org.ts b/src/api/org/api.org.ts index 4eae586..8a7edfd 100644 --- a/src/api/org/api.org.ts +++ b/src/api/org/api.org.ts @@ -76,4 +76,9 @@ export default { profileNewProvince: `${metadata}province`, profileNewDistrictByPId: (id: string) => `${metadata}province/${id}`, profileNewSubDistrictByDId: (id: string) => `${metadata}district/${id}`, + + /** + * รายการคำร้องขอแก้ไขข้อมูลทะเบียนประวัติ + */ + requestEdit: `${profileOrg}/edit/`, }; diff --git a/src/assets/keycloak-bg.png b/src/assets/keycloak-bg.png new file mode 100644 index 0000000..f6b0837 Binary files /dev/null and b/src/assets/keycloak-bg.png differ diff --git a/src/assets/keycloak-logo-poc.png b/src/assets/keycloak-logo-poc.png new file mode 100644 index 0000000..f16aa38 Binary files /dev/null and b/src/assets/keycloak-logo-poc.png differ diff --git a/src/main.ts b/src/main.ts index 98cedae..b2b22cb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,8 @@ import "@vuepic/vue-datepicker/dist/main.css"; import "quasar/src/css/index.sass"; import http from "./plugins/http"; +import keycloak, { getToken } from "@/plugins/keycloak"; + // import OpenLayersMap from "vue3-openlayers"; // import './assets/main.css' @@ -40,4 +42,26 @@ app.component( app.config.globalProperties.$http = http; +// authen with keycloak client +const auth = await getToken(); + +if (auth.token && auth.refresh_token) { + keycloak.init({ + checkLoginIframe: false, + token: auth.token, + refreshToken: auth.refresh_token, + }); + // .then((authenticated) => { + // console.log("authenticated", authenticated); + // if (!authenticated) { + // window.location.reload(); + // } else { + // console.log("Authenticated"); + // } + // }) + // .catch((err) => { + // console.error("Keycloak initialization failed:", err); + // }); +} + app.mount("#app"); diff --git a/src/modules/03_retire/views/addRetire.vue b/src/modules/03_retire/views/addRetire.vue index 8c97ac2..f8f3546 100644 --- a/src/modules/03_retire/views/addRetire.vue +++ b/src/modules/03_retire/views/addRetire.vue @@ -128,6 +128,7 @@ const cancelResing = () => { modal.value = true; }; +const checkCancleLeave = ref(false); /** * ฟังก์ชั่นเรียกข้อมูลจาก Api * @param id ไอดีของข้อมูล @@ -141,6 +142,12 @@ const fectDataresign = async (id: string) => { tranferOrg.value = data.location; dateCommand.value = data.sendDate; dateLeave.value = data.activeDate; + + const currentDate = new Date(); + let dueDateMinusOne = new Date(data.activeDate); + dueDateMinusOne.setDate(dueDateMinusOne.getDate() - 1); + checkCancleLeave.value = currentDate < dueDateMinusOne; + noteReason.value = data.reason; files.value = data.docs; dataDetail.value = data; @@ -561,6 +568,7 @@ function downloadFile(data: string) { + +import { reactive, ref, watch } from "vue"; +import { useQuasar } from "quasar"; +import config from "@/app.config"; +import http from "@/plugins/http"; +import axios from "axios"; +import keycloak from "@/plugins/keycloak"; + +import DialogHeader from "@/components/DialogHeader.vue"; + +import { useRequestEditStore } from "@/modules/10_registry/store/RequestEdit"; +import { useCounterMixin } from "@/stores/mixin"; + +/** + * use + */ +const $q = useQuasar(); +const store = useRequestEditStore(); +const { dialogConfirm, showLoader, hideLoader, messageError, success } = + useCounterMixin(); + +/** + * props + */ +const modal = defineModel("modal", { required: true }); +const props = defineProps({ + fetchData: { type: Function, requied: true }, +}); + +const isReadOnly = ref(false); +const profileId = ref(""); +const formData = reactive({ + topic: "", + detail: "", + document: null, +}); +const topicOption = ref(store.optionTopic); + +function closeDialog() { + modal.value = false; + formData.topic = ""; + formData.detail = ""; + formData.document = null; +} + +/** + * function ยืนยันการยื่นคำร้องขอแก้ไขข้อมูล + */ +function onSubmit() { + dialogConfirm( + $q, + () => { + showLoader(); + http + .post(config.API.requestEdit, { + topic: formData.topic, + detail: formData.detail, + profileId: profileId.value, + }) + .then((res) => { + createURLUpload(res.data.result, formData.document); + }) + .catch((err) => { + messageError($q, err); + hideLoader(); + }); + }, + "ยืนยันการยื่นคำร้อง", + "ต้องการยืนยันการยื่นคำร้องนี้ใช่หรือไม่?" + ); +} + +/** + * function สร้าง URL อัปโหลไฟล์เอกสารหลักฐาน + */ +function createURLUpload(id: string, file: any) { + const fileName = { fileName: file.name }; + http + .post( + config.API.file( + "ระบบทะเบียนประวัติ", + "เอกสารหลักฐานคำร้องขอแก้ไขข้อมูล", + id + ), + { + replace: false, + fileList: fileName, + } + ) + .then(async (res) => { + const foundKey: string | undefined = Object.keys(res.data).find( + (key) => + res.data[key]?.fileName !== undefined && + res.data[key]?.fileName !== "" + ); + foundKey && + uploadFileDoc(res.data[foundKey]?.uploadUrl, formData.document); + }) + .catch((err) => { + messageError($q, err); + }); +} + +/** + * function สำหรับอัพโหลดไฟล์เอกสารหลักฐาน + */ +function uploadFileDoc(uploadUrl: string, file: any) { + showLoader(); + axios + .put(uploadUrl, file, { + headers: { + "Content-Type": file.type, + }, + }) + .then(async (res) => { + await props.fetchData?.(); + await success($q, "บันทึกข้อมูลสำเร็จ"); + closeDialog(); + }) + .catch((e) => { + messageError($q, e); + }) + .finally(() => { + hideLoader(); + }); +} + +/** + * class input + * @param val + */ +function classInput(val: boolean) { + return { + "full-width cursor-pointer ": val, + "full-width cursor-pointer inputgreen": !val, + }; +} + +/** + * function ค้นหาข้อมูลใน select + * @param val คำค้นหา + * @param update Function + */ +function filterOption(val: string, update: Function) { + update(() => { + topicOption.value = store.optionTopic.filter( + (v: any) => v.indexOf(val) > -1 + ); + }); +} + +/** + * function fetch profileId + */ +function fetchProfile() { + showLoader(); + http + .get(config.API.profilePosition()) + .then((res) => { + const data = res.data.result; + profileId.value = data.profileId; + }) + .catch((err) => { + messageError($q, err); + }) + .finally(() => { + hideLoader(); + }); +} +watch( + () => modal.value, + () => { + modal.value && fetchProfile(); + } +); + + + + + diff --git a/src/modules/10_registry/interface/index/Main.ts b/src/modules/10_registry/interface/index/Main.ts new file mode 100644 index 0000000..8a07723 --- /dev/null +++ b/src/modules/10_registry/interface/index/Main.ts @@ -0,0 +1,13 @@ +interface DataOption { + id: string; + name: string; +} + +interface NewPagination { + descending: boolean; + page: number; + rowsPerPage: number; + sortBy: string; +} + +export type { DataOption,NewPagination }; diff --git a/src/modules/10_registry/interface/response/Main.ts b/src/modules/10_registry/interface/response/Main.ts new file mode 100644 index 0000000..136aded --- /dev/null +++ b/src/modules/10_registry/interface/response/Main.ts @@ -0,0 +1,14 @@ +interface DataRequest { + createdAt: string; + createdFullName: string; + detail: string; + fullname: string; + id: string; + lastUpdateFullName: string; + lastUpdatedAt: string; + remark: string; + status: string; + topic: string; +} + +export type { DataRequest }; diff --git a/src/modules/10_registry/router.ts b/src/modules/10_registry/router.ts index 8797400..2dbae2c 100644 --- a/src/modules/10_registry/router.ts +++ b/src/modules/10_registry/router.ts @@ -1,12 +1,20 @@ // registry const registryPage = () => import("@/modules/10_registry/views/main.vue"); -const registryInformation = () => import("@/modules/10_registry/tabs/01_information.vue"); -const registryGovernment = () => import("@/modules/10_registry/tabs/02_government.vue"); +const registryInformation = () => + import("@/modules/10_registry/tabs/01_information.vue"); +const registryGovernment = () => + import("@/modules/10_registry/tabs/02_government.vue"); const registrySalary = () => import("@/modules/10_registry/tabs/03_salary.vue"); -const registryAchievement = () => import("@/modules/10_registry/tabs/04_Achievement.vue"); +const registryAchievement = () => + import("@/modules/10_registry/tabs/04_Achievement.vue"); const registryOther = () => import("@/modules/10_registry/tabs/05_other.vue"); +/** + * คำร้องแก้ไข + */ +const requestEditMain = () => import("@/modules/10_registry/views/requestEditMain.vue"); + export default [ { path: "/registry", @@ -63,4 +71,14 @@ export default [ Key: [10], }, }, + + { + path: "/registry/request-edit", + name: "request-edit", + component: requestEditMain, + meta: { + Auth: true, + Key: [10], + }, + }, ]; diff --git a/src/modules/10_registry/store/RequestEdit.ts b/src/modules/10_registry/store/RequestEdit.ts new file mode 100644 index 0000000..eb8e82c --- /dev/null +++ b/src/modules/10_registry/store/RequestEdit.ts @@ -0,0 +1,36 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import type { DataOption } from "@/modules/10_registry/interface/index/Main"; + +export const useRequestEditStore = defineStore("requestEditStore", () => { + const optionTopic = ref([ + "ขอแก้ไขคำนำหน้านาม ชื่อ นามสกุล", + "ขอแก้ไขรูปภาพประจำตัว", + "ขอแก้ไขชื่อ - นามสกุล คู่สมรส", + "ขอแก้ไขชื่อ - นามสกุล บิดา", + "ขอแก้ไขชื่อ - นามสกุล มารดา", + "ขอแก้ไขข้อมูลการได้รับพระราชทานเครื่องราชอิสริยาภรณ์/เหรียญจักรพรรดิมาลา", + "ขอแก้ไขประกาศเกียรติคุณ", + "ขอแก้ไขข้อมูลประวัติการศึกษา", + ]); + const optionStatus = ref([ + { id: "", name: "ทั้งหมด" }, + { id: "PENDING", name: "รอดำเนินการ" }, + { id: "COMPLETE", name: "ดำเนินการแก้ไขแล้ว" }, + { id: "REJECT", name: "ไม่อนุมัตการแก้ไข" }, + ]); + + function convertStatus(val: string) { + switch (val) { + case "PENDING": + return "รอดำเนินการ"; + case "COMPLETE": + return "ดำเนินการแก้ไขแล้ว"; + case "REJECT": + return "ไม่อนุมัตการแก้ไข"; + default: + return "-"; + } + } + return { convertStatus, optionTopic, optionStatus }; +}); diff --git a/src/modules/10_registry/views/main.vue b/src/modules/10_registry/views/main.vue index 9be68b5..3295f92 100644 --- a/src/modules/10_registry/views/main.vue +++ b/src/modules/10_registry/views/main.vue @@ -128,21 +128,6 @@ function onClickDownloadKp7(type: string) { }); } -/** - * logout keycloak - * confirm ก่อนออกจากระบบ - */ -const doLogout = () => { - dialogConfirm( - $q, - () => { - keycloak.logout(); - }, - "ยืนยันการออกจากระบบ", - "ต้องการออกจากระบบใช่หรือไม่" - ); -}; - /** * ฟังชั่นกลับหน้าหลัก */ @@ -150,6 +135,13 @@ const clickBack = () => { router.push(`/`); }; +/** + * function redirect ไปหน้ารายการคำร้องขอแก้ไขข้อมูล + */ +function redirectToPagePetition() { + router.push(`/registry/request-edit`); +} + onMounted(async () => { store.typeProfile = "OFFICER"; await getType(); @@ -171,7 +163,14 @@ onMounted(async () => { class="q-mr-sm" @click="clickBack" /> - ข้อมูลทะเบียนประวัติ + ข้อมูลทะเบียนประวัติ + + ยื่นคำร้องขอแก้ไขข้อมูล +
@@ -340,16 +339,6 @@ onMounted(async () => { > ดาวน์โหลด ก.พ.7/ก.ก. 1 -
diff --git a/src/modules/10_registry/views/requestEditMain.vue b/src/modules/10_registry/views/requestEditMain.vue new file mode 100644 index 0000000..096df4e --- /dev/null +++ b/src/modules/10_registry/views/requestEditMain.vue @@ -0,0 +1,446 @@ + + + + + diff --git a/src/plugins/http.ts b/src/plugins/http.ts index 494c9aa..e278860 100644 --- a/src/plugins/http.ts +++ b/src/plugins/http.ts @@ -1,5 +1,5 @@ import Axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; -import keycloak from "./keycloak"; +import keycloak, { kcLogout } from "./keycloak"; const http = Axios.create({ timeout: 1000000000, // เพิ่มค่า timeout @@ -10,7 +10,7 @@ const http = Axios.create({ http.interceptors.request.use( async function (config: AxiosRequestConfig) { - await keycloak.updateToken(1); + // await keycloak.updateToken(1); config.headers = config.headers ?? {}; const token = keycloak.token; // const token = localStorage.getItem("access_token") @@ -33,6 +33,8 @@ http.interceptors.response.use( // eslint-disable-next-line no-prototype-builtins if (error.hasOwnProperty("response")) { if (error.response.status === 401 || error.response.status === 403) { + kcLogout(); + // Store.commit("SET_ERROR_MESSAGE", error.response.data.message); // Store.commit("REMOVE_ACCESS_TOKEN") } diff --git a/src/plugins/keycloak.ts b/src/plugins/keycloak.ts index 587050d..19e0f9a 100644 --- a/src/plugins/keycloak.ts +++ b/src/plugins/keycloak.ts @@ -1,23 +1,70 @@ -/** - * front connect to keycloak - */ -import Keycloak from "keycloak-js" -// import config from "../app.config"; -// import http from "../shared/http"; -// import router from "../router"; +// authen with keycloak client +import Keycloak from "keycloak-js"; -const initOptions = { - // realm: import.meta.env.VITE_REALM_KEYCLOAK, - // clientId: import.meta.env.VITE_CLIENTID_KEYCLOAK, - // url: import.meta.env.VITE_URL_KEYCLOAK, - realm: "bma-ehr", - clientId: "bma-ehr-vue3", - url: "https://id.frappet.synology.me/", -} //option keycloak ที่จะ connect +const ACCESS_TOKEN = "BMAHRIS_KEYCLOAK_IDENTITY"; +const REFRESH_TOKEN = "BMAHRIS_KEYCLOAK_REFRESH"; +const keycloakConfig = { + url: "https://id.frappet.synology.me", + realm: "bma-ehr", + clientId: "gettoken", + clientSecret: "qsFwDb5anVoXKKwoeivrByIn9VYWQNRn", +}; -const keycloak = Keycloak(initOptions) +const keycloak = new Keycloak(keycloakConfig); -keycloak.onAuthSuccess = () => {} //เพิ่มlogin สำเร็จจะมาทำฟังก์ชันนี้ +async function kcAuthen(access_token: string, refresh_token: string) { + await setCookie(ACCESS_TOKEN, access_token, 1); + await setCookie(REFRESH_TOKEN, refresh_token, 1); + window.location.href = "/"; +} -await keycloak.init({ onLoad: "check-sso", checkLoginIframe: false }) //ทำการ connect keycloak -export default keycloak +async function kcLogout() { + await deleteCookie(ACCESS_TOKEN); + await deleteCookie(REFRESH_TOKEN); + if (keycloak.authenticated !== undefined) { + keycloak.logout(); + } + window.location.href = "/login"; +} + +async function getToken() { + return { + token: getCookie(ACCESS_TOKEN), + refresh_token: getCookie(REFRESH_TOKEN), + }; +} + +function setCookie(name: string, value: any, days: number) { + let expires = ""; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} + +function getCookie(name: string) { + const nameEQ = name + "="; + const ca = document.cookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function deleteCookie(name: string) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; +} + +export default keycloak; +export { + keycloakConfig, + getToken, + kcAuthen, + kcLogout, + ACCESS_TOKEN, + REFRESH_TOKEN, +}; diff --git a/src/router/index.ts b/src/router/index.ts index e39062d..ef8b4eb 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"; const MainLayout = () => import("@/views/MainLayout.vue"); const Dashboard = () => import("@/modules/01_dashboard/views/Dashboard.vue"); -const TestPage = () => import("@/modules/01_dashboard/views/test.vue"); +const Error404NotFound = () => import("@/views/Error404NotFound.vue"); const loginMain = () => import("@/views/login.vue"); import ModuleTransfer from "@/modules/02_transfer/router"; @@ -19,7 +19,7 @@ import ModuleProbation from "@/modules/11_probation/router"; import ModuleOrganization from "@/modules/12_organization/router"; import ModulePortfolio from "@/modules/13_portfolio/router"; // TODO: ใช้หรือไม่? -import keycloak from "@/plugins/keycloak"; +import keycloak, { kcLogout } from "@/plugins/keycloak"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -38,16 +38,7 @@ const router = createRouter({ Key: [7], }, }, - { - path: "/test", - name: "test", - component: TestPage, - meta: { - Auth: true, - Key: [7], - }, - }, - + ...ModuleTransfer, ...ModuleRetire, ...ModuleCheckin, @@ -63,32 +54,37 @@ const router = createRouter({ ...ModulePortfolio, ], }, + { + // path: "/:catchAll(.*)*", // TODO: ใช้ pathMatch แทนตามในเอกสารแนะนำ คงไว้เผื่อจำเป็น + path: "/:pathMatch(.*)*", + component: Error404NotFound, + }, { path: "/login", name: "loginMain", component: loginMain, meta: { - Auth: true, - Key: [7], + Auth: false, }, }, + { + path: "/auth", + name: "auth", + component: () => import("@/views/auth.vue"), + }, ], }); +// authen with keycloak client 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 { - next(); + if (keycloak.authenticated === undefined && to.meta.Auth) { + kcLogout(); } } else { next(); } - // next(); + next(); }); export default router; diff --git a/src/stores/mixin.ts b/src/stores/mixin.ts index cd79acf..c823231 100644 --- a/src/stores/mixin.ts +++ b/src/stores/mixin.ts @@ -411,17 +411,30 @@ export const useCounterMixin = defineStore("mixin", () => { } } else { if (e.response.status == 401) { - //invalid_token - q.dialog({ - component: CustomComponent, - componentProps: { - title: `พบข้อผิดพลาด`, - message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, - icon: "warning", - color: "red", - onlycancel: true, - }, - }); + if (text !== "") { + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: text, + icon: "warning", + color: "red", + onlycancel: true, + }, + }); + } else { + //invalid_token + q.dialog({ + component: CustomComponent, + componentProps: { + title: `พบข้อผิดพลาด`, + message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`, + icon: "warning", + color: "red", + onlycancel: true, + }, + }); + } } else { q.dialog({ component: CustomComponent, @@ -1125,6 +1138,6 @@ export const useCounterMixin = defineStore("mixin", () => { calculateDurationYmd, findOrgName, findPosMasterNoOld, - findOrgNameOld + findOrgNameOld, }; }); diff --git a/src/views/Error404NotFound.vue b/src/views/Error404NotFound.vue new file mode 100644 index 0000000..e521ddd --- /dev/null +++ b/src/views/Error404NotFound.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/views/MainLayout.vue b/src/views/MainLayout.vue index ee0d691..094be6a 100644 --- a/src/views/MainLayout.vue +++ b/src/views/MainLayout.vue @@ -4,7 +4,7 @@ import config from "@/app.config"; import { onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useQuasar } from "quasar"; -import keycloak from "@/plugins/keycloak"; +import keycloak, { kcLogout } from "@/plugins/keycloak"; import { useCounterMixin } from "@/stores/mixin"; const route = useRoute(); @@ -91,8 +91,9 @@ const fetchlistNotification = async (index: number, type: string) => { const doLogout = () => { dialogConfirm( $q, - () => { - keycloak.logout(); + async () => { + // authen with keycloak client + kcLogout(); }, "ยืนยันการออกจากระบบ", "ต้องการออกจากระบบใช่หรือไม่" @@ -393,20 +394,6 @@ function onInfo() { - - + --> +
diff --git a/src/views/auth.vue b/src/views/auth.vue new file mode 100644 index 0000000..fe156c9 --- /dev/null +++ b/src/views/auth.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/views/login.vue b/src/views/login.vue index 7917556..c1ad245 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -1,11 +1,61 @@ + + -