diff --git a/package.json b/package.json index 088a0ead7..75b2a4f43 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "moment": "^2.29.4", "pdf-lib": "^1.17.1", "pinia": "^2.0.29", + "pinia-plugin-persistedstate": "^3.2.3", "quasar": "^2.11.1", "socket.io-client": "^4.7.4", "structure-chart": "^0.0.9", diff --git a/src/api/recruiting/api.recruit.ts b/src/api/recruiting/api.recruit.ts index 3a9f220a8..8bfda023b 100644 --- a/src/api/recruiting/api.recruit.ts +++ b/src/api/recruiting/api.recruit.ts @@ -23,6 +23,7 @@ export default { uploadCandidates: (id: string) => `${recruit}candidate/${id}`, uploadResult: (id: string) => `${recruit}result/${id}`, getImportHistory: (id: string) => `${recruit}history/${id}`, + getImportStatus: (jobId: string) => `${recruit}import/status/${jobId}`, //upload periodRecruitDoc: (examId: string) => `${recruit}doc/${examId}`, diff --git a/src/main.ts b/src/main.ts index 570937672..d75e834e2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import th from "quasar/lang/th"; import "@vuepic/vue-datepicker/dist/main.css"; import http from "./plugins/http"; import { createPinia } from "pinia"; +import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; // import './assets/main.css' @@ -20,6 +21,7 @@ import filters from "./plugins/filters"; const app = createApp(App); const pinia = createPinia(); +pinia.use(piniaPluginPersistedstate); // เพิ่ม Global Filters ลงใน App app.config.globalProperties.$filters = filters; diff --git a/src/modules/03_recruiting/views/01_compete/Period.vue b/src/modules/03_recruiting/views/01_compete/Period.vue index d281f6d49..487de668c 100644 --- a/src/modules/03_recruiting/views/01_compete/Period.vue +++ b/src/modules/03_recruiting/views/01_compete/Period.vue @@ -11,6 +11,7 @@ import config from "@/app.config"; import { checkPermission } from "@/utils/permissions"; import { useCounterMixin } from "@/stores/mixin"; import { calculateFiscalYear } from "@/utils/function"; +import { useUploadProgressStore } from "@/stores/uploadProgress"; import type { Pagination } from "@/modules/03_recruiting/interface/index/Main"; import type { @@ -33,9 +34,11 @@ const { messageError, onSearchDataTable, dialogRemove, + dialogMessage, } = mixin; const router = useRouter(); +const uploadProgress = useUploadProgressStore(); const name = ref(""); const year = ref(calculateFiscalYear(new Date()) + 543); const order = ref(1); @@ -49,6 +52,11 @@ const modalScore = ref(false); const modalCandidate = ref(false); const modalResult = ref(false); const selected_row_id = ref(""); +const jobStatus = ref<{ + candidate?: "running" | "completed" | "failed"; + score?: "running" | "completed" | "failed"; + result?: "running" | "completed" | "failed"; +}>({}); const rowsHistory = ref([]); //select data history const tittleHistory = ref("ประวัติการนำเข้าข้อมูล"); // const filterHistory = ref(""); //search data table history @@ -62,6 +70,54 @@ const textTittle = ref(""); const textTittleScore = ref(""); const textTittleCandidate = ref(""); const textTittleResult = ref(""); + +// Dialog message constants +const UPLOAD_RUNNING_DIALOG = { + title: "ไม่สามารถอัปโหลดไฟล์ได้", + message: "อยู่ระหว่างนำเข้าข้อมูล ระบบจะแจ้งผลให้ทราบทันทีเมื่อเสร็จสิ้น", + icon: "mdi-progress-alert", + btnLabel: "ตกลง", + color: "primary", +}; + +const UPLOAD_SUCCESS_DIALOG = { + title: "อัปโหลดไฟล์สำเร็จ", + message: + "ระบบกำลังนำเข้าข้อมูลคุณสามารถใช้งานเมนูอื่นในระหว่างนี้ได้ โดยระบบจะแจ้งผลการดำเนินการให้ทราบทันทีเมื่อเสร็จสิ้น", + icon: "check", + btnLabel: "ตกลง", + color: "primary", +}; + +// Function to show upload running dialog +function showUploadRunningDialog() { + dialogMessage( + $q, + UPLOAD_RUNNING_DIALOG.title, + UPLOAD_RUNNING_DIALOG.message, + UPLOAD_RUNNING_DIALOG.icon, + UPLOAD_RUNNING_DIALOG.btnLabel, + UPLOAD_RUNNING_DIALOG.color, + undefined, + undefined, + true + ); +} + +// Function to show upload success dialog +function showUploadSuccessDialog() { + dialogMessage( + $q, + UPLOAD_SUCCESS_DIALOG.title, + UPLOAD_SUCCESS_DIALOG.message, + UPLOAD_SUCCESS_DIALOG.icon, + UPLOAD_SUCCESS_DIALOG.btnLabel, + UPLOAD_SUCCESS_DIALOG.color, + undefined, + undefined, + true + ); +} const rows = ref([]); const rowsData = ref([]); const initialPagination = ref({ @@ -335,9 +391,15 @@ function clickDetail(id: string) { * @param id รอบสอบเเข่งขัน */ async function clickUpload(id: string) { - modalCandidate.value = true; - textTittleCandidate.value = "นำเข้าผู้สมัครสอบแข่งขัน"; selected_row_id.value = id; + const isRunning = await checkJobStatus(id, "candidate"); + + if (isRunning) { + showUploadRunningDialog(); + } else { + modalCandidate.value = true; + textTittleCandidate.value = "นำเข้าผู้สมัครสอบแข่งขัน"; + } } /** @@ -345,9 +407,15 @@ async function clickUpload(id: string) { * @param id รอบสอบเเข่งขัน */ async function clickEdit(id: string) { - modalScore.value = true; - textTittleScore.value = "นำเข้าบัญชีรวมคะแนน"; selected_row_id.value = id; + const isRunning = await checkJobStatus(id, "score"); + + if (isRunning) { + showUploadRunningDialog(); + } else { + modalScore.value = true; + textTittleScore.value = "นำเข้าบัญชีรวมคะแนน"; + } } /** @@ -355,9 +423,57 @@ async function clickEdit(id: string) { * @param id รอบสอบเเข่งขัน */ async function clickResult(id: string) { - modalResult.value = true; - textTittleResult.value = "นำเข้าไฟล์ผลการสอบ"; selected_row_id.value = id; + const isRunning = await checkJobStatus(id, "result"); + + if (isRunning) { + showUploadRunningDialog(); + } else { + modalResult.value = true; + textTittleResult.value = "นำเข้าไฟล์ผลการสอบ"; + } +} + +/** + * ตรวจสอบสถานะการนำเข้าข้อมูล + * @param id รอบสอบเเข่งขัน + * @param type ประเภทไฟล์ที่ต้องการตรวจสอบ + * @return true ถ้ากำลัง running, false ถ้าไม่มี job หรือ job เสร็จแล้ว + */ +async function checkJobStatus( + id: string, + type: "candidate" | "score" | "result" +): Promise { + const uploads = uploadProgress.pendingUploads.filter( + (u) => u.periodId === id && u.type === type + ); + + let hasRunningJob = false; + + for (const upload of uploads) { + try { + const res = await http.get(config.API.getImportStatus(upload.jobId)); + const status = res.data.result.status.toLowerCase(); // 'running', 'completed', 'failed' + + if (status === "completed" || status === "failed") { + status === "completed" && fetchData(); + uploadProgress.removeUpload(upload.jobId); + } else if (status === "running") { + jobStatus.value[type] = "running"; + hasRunningJob = true; + } + } catch (e) { + // if error, remove from pending + uploadProgress.removeUpload(upload.jobId); + } + } + + // if no running jobs, clear status + if (!uploadProgress.isUploading(id, type)) { + jobStatus.value[type] = undefined; + } + + return hasRunningJob; } /** @@ -463,11 +579,25 @@ async function checkSaveCandidate() { await http .post(config.API.uploadCandidates(selected_row_id.value), fd) .then((res) => { - success($q, "นำเข้าข้อมูลผู้สมัครสอบสำเร็จ"); + const jobId = res.data.result.jobId; + uploadProgress.addUpload(jobId, selected_row_id.value, "candidate"); + + dialogMessage( + $q, + "อัปโหลดไฟล์สำเร็จ", + "ระบบกำลังนำเข้าข้อมูลคุณสามารถใช้งานเมนูอื่นในระหว่างนี้ได้ โดยระบบจะแจ้งผลการดำเนินการให้ทราบทันทีผ่านหน้าจอเมื่อเสร็จสิ้น", + "check", + "ตกลง", + "primary", + undefined, + undefined, + true + ); + modalCandidate.value = false; files_candidate.value = null; selected_row_id.value = ""; - fetchData(); + // fetchData(); }) .catch((e) => { messageError($q, e); @@ -477,7 +607,7 @@ async function checkSaveCandidate() { }); } -/** บันทึด คะเเนน */ +/** บันทึด คะเแนน */ async function checkSaveScore() { const fd = new FormData(); fd.append("attachment", files_score.value[0]); @@ -485,11 +615,24 @@ async function checkSaveScore() { await http .post(config.API.saveScores(selected_row_id.value), fd) .then((res) => { - success($q, "นำเข้าข้อมูลผลคะแนนสอบสำเร็จ"); + const jobId = res.data.result.jobId; + uploadProgress.addUpload(jobId, selected_row_id.value, "score"); + + dialogMessage( + $q, + "อัปโหลดไฟล์สำเร็จ", + "ระบบกำลังนำเข้าข้อมูลคุณสามารถใช้งานเมนูอื่นในระหว่างนี้ได้ โดยระบบจะแจ้งผลการดำเนินการให้ทราบทันทีผ่านหน้าจอเมื่อเสร็จสิ้น", + "check", + "ตกลง", + "primary", + undefined, + undefined, + true + ); modalScore.value = false; files_score.value = null; selected_row_id.value = ""; - fetchData(); + // fetchData(); }) .catch((e) => { messageError($q, e); @@ -507,7 +650,21 @@ async function checkSaveResult() { await http .post(config.API.uploadResult(selected_row_id.value), fd) .then((res) => { - success($q, "นำเข้าข้อมูลผลการสอบแข่งขันฯ (บัญชีรายชื่อ)"); + const jobId = res.data.result.jobId; + uploadProgress.addUpload(jobId, selected_row_id.value, "result"); + + // success($q, "นำเข้าข้อมูลผลการสอบแข่งขันฯ (บัญชีรายชื่อ)"); + dialogMessage( + $q, + "อัปโหลดไฟล์สำเร็จ", + "ระบบกำลังนำเข้าข้อมูลคุณสามารถใช้งานเมนูอื่นในระหว่างนี้ได้ โดยระบบจะแจ้งผลการดำเนินการให้ทราบทันทีผ่านหน้าจอเมื่อเสร็จสิ้น", + "check", + "ตกลง", + "primary", + undefined, + undefined, + true + ); modalResult.value = false; files_result.value = null; selected_row_id.value = ""; @@ -532,6 +689,9 @@ async function checkSave() { await http .post(config.API.saveCandidates, fd) .then((res) => { + const jobId = res.data.result.jobId; + uploadProgress.addUpload(jobId, selected_row_id.value, "period"); + success($q, "นำเข้าข้อมูลผู้สมัครสอบแข่งขันสำเร็จ"); modalAdd.value = false; fetchData(); @@ -563,6 +723,7 @@ onMounted(async () => {
จัดการรอบสอบแข่งขัน
+
{
นำเข้าไฟล์ผู้สมัครสอบ @@ -715,6 +876,9 @@ onMounted(async () => {
{ round color="green" @click.stop.prevent="clickEdit(props.row.id)" - v-if=" - col.value == null && checkPermission($route)?.attrIsUpdate - " > @@ -751,6 +912,11 @@ onMounted(async () => {
{ color="green" round @click.stop.prevent="clickResult(props.row.id)" - v-if=" - (props.row.score == null || - props.row.score.resultCount == 0) && - checkPermission($route)?.attrIsUpdate - " > นำเข้าไฟล์ผลการสอบ (บัญชีรายชื่อ) @@ -921,12 +1082,12 @@ onMounted(async () => { + -
@@ -965,12 +1126,12 @@ onMounted(async () => { + -
@@ -1009,12 +1170,12 @@ onMounted(async () => { + -
diff --git a/src/stores/uploadProgress.ts b/src/stores/uploadProgress.ts new file mode 100644 index 000000000..94fcc7536 --- /dev/null +++ b/src/stores/uploadProgress.ts @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +interface PendingUpload { + jobId: string; + type: 'candidate' | 'score' | 'result' | 'period'; + periodId: string; +} + +export const useUploadProgressStore = defineStore('uploadProgress', () => { + const pendingUploads = ref([]); + + function addUpload(jobId: string, periodId: string, uploadType: 'candidate' | 'score' | 'result' | 'period') { + pendingUploads.value.push({ + jobId, + type: uploadType, + periodId + }); + } + + function removeUpload(jobId: string) { + const index = pendingUploads.value.findIndex(u => u.jobId === jobId); + if (index !== -1) { + pendingUploads.value.splice(index, 1); + } + } + + function removeByPeriodAndType(periodId: string, uploadType: string) { + const index = pendingUploads.value.findIndex( + u => u.periodId === periodId && u.type === uploadType + ); + if (index !== -1) { + pendingUploads.value.splice(index, 1); + } + } + + function isUploading(periodId: string, uploadType: string): boolean { + return pendingUploads.value.some( + u => u.periodId === periodId && u.type === uploadType + ); + } + + return { pendingUploads, addUpload, removeUpload, removeByPeriodAndType, isUploading }; +}, { + persist: true +});