Merge branch 'develop' into dev
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m58s

This commit is contained in:
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 2026-05-14 17:28:21 +07:00
commit f58dfbc97f
5 changed files with 250 additions and 39 deletions

View file

@ -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",

View file

@ -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}`,

View file

@ -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;

View file

@ -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<string>("");
const year = ref<number>(calculateFiscalYear(new Date()) + 543);
const order = ref<number>(1);
@ -49,6 +52,11 @@ const modalScore = ref<boolean>(false);
const modalCandidate = ref<boolean>(false);
const modalResult = ref<boolean>(false);
const selected_row_id = ref<string>("");
const jobStatus = ref<{
candidate?: "running" | "completed" | "failed";
score?: "running" | "completed" | "failed";
result?: "running" | "completed" | "failed";
}>({});
const rowsHistory = ref<ResponseHistoryObject[]>([]); //select data history
const tittleHistory = ref<string>("ประวัติการนำเข้าข้อมูล"); //
const filterHistory = ref<string>(""); //search data table history
@ -62,6 +70,54 @@ const textTittle = ref<string>("");
const textTittleScore = ref<string>("");
const textTittleCandidate = ref<string>("");
const textTittleResult = ref<string>("");
// 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<ResponseRecruitPeriod[]>([]);
const rowsData = ref<ResponseRecruitPeriod[]>([]);
const initialPagination = ref<Pagination>({
@ -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<boolean> {
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 () => {
<div class="toptitle text-dark col-12 row items-center">
ดการรอบสอบแขงข
</div>
<q-card flat bordered class="col-12 q-mt-sm q-pt-sm q-pa-md">
<div>
<Table
@ -669,16 +830,16 @@ onMounted(async () => {
</div>
<div v-else-if="col.name == 'examCount'" class="table_ellipsis2">
<q-btn
v-if="
(col.value == null || col.value == '0') &&
checkPermission($route)?.attrIsUpdate
"
flat
dense
size="12px"
color="green"
round
@click.stop.prevent="clickUpload(props.row.id)"
v-if="
(col.value == null || col.value == '0') &&
checkPermission($route)?.attrIsUpdate
"
>
<q-icon name="mdi-file-excel-outline" size="20px" />
<q-tooltip>นำเขาไฟลสมครสอบ</q-tooltip>
@ -715,6 +876,9 @@ onMounted(async () => {
</div>
<div v-else-if="col.name == 'scoreCount'" class="table_ellipsis2">
<q-btn
v-if="
col.value == null && checkPermission($route)?.attrIsUpdate
"
:disable="props.row.examCount == 0"
flat
dense
@ -722,9 +886,6 @@ onMounted(async () => {
round
color="green"
@click.stop.prevent="clickEdit(props.row.id)"
v-if="
col.value == null && checkPermission($route)?.attrIsUpdate
"
>
<q-icon name="mdi-file-excel-outline" size="20px" />
<!-- นำเขาไฟลผลคะแนนสอบ -->
@ -751,6 +912,11 @@ onMounted(async () => {
<div v-else-if="col.name == 'result'" class="table_ellipsis2">
<q-btn
v-if="
(props.row.score == null ||
props.row.score.resultCount == 0) &&
checkPermission($route)?.attrIsUpdate
"
:disable="props.row.score == null"
flat
dense
@ -758,11 +924,6 @@ 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
"
>
<q-icon name="mdi-file-excel-outline" size="20px" />
<q-tooltip>นำเขาไฟลผลการสอบ (ญชรายช)</q-tooltip>
@ -921,12 +1082,12 @@ onMounted(async () => {
<q-dialog v-model="modalCandidate" persistent>
<q-card style="width: 600px">
<DialogHeadTemplate
:title="textTittleCandidate"
:close="clickCloseCandidate"
title-type="ข้อมูลผู้สมัครสอบ"
/>
<q-form ref="myFormScore">
<DialogHeadTemplate
:title="textTittleCandidate"
:close="clickCloseCandidate"
title-type="ข้อมูลผู้สมัครสอบ"
/>
<q-separator />
<q-card-section>
<div class="col-12 row items-center q-col-gutter-sm">
@ -965,12 +1126,12 @@ onMounted(async () => {
<q-dialog v-model="modalScore" persistent>
<q-card style="width: 600px">
<DialogHeadTemplate
:title="textTittleScore"
:close="clickCloseScore"
title-type="บัญชีรวมคะแนน"
/>
<q-form ref="myFormScore">
<DialogHeadTemplate
:title="textTittleScore"
:close="clickCloseScore"
title-type="บัญชีรวมคะแนน"
/>
<q-separator />
<q-card-section>
<div class="col-12 row items-center q-col-gutter-sm">
@ -1009,12 +1170,12 @@ onMounted(async () => {
<q-dialog v-model="modalResult" persistent>
<q-card style="width: 600px">
<DialogHeadTemplate
:title="textTittleResult"
:close="clickCloseResult"
title-type="ผลการสอบ (บัญชีรายชื่อ)"
/>
<q-form ref="myFormScore">
<DialogHeadTemplate
:title="textTittleResult"
:close="clickCloseResult"
title-type="ผลการสอบ (บัญชีรายชื่อ)"
/>
<q-separator />
<q-card-section>
<div class="col-12 row items-center q-col-gutter-sm">

View file

@ -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<PendingUpload[]>([]);
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
});