feat:issue

This commit is contained in:
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 2026-01-28 11:25:50 +07:00
parent 0078dff1af
commit d558f826f6
4 changed files with 451 additions and 0 deletions

View file

@ -194,4 +194,6 @@ export default {
workflowCommanderSign: `${workflow}/commander/sign`,
orgAssistance: (id: string) => `${orgProfile}/assistance/${id}`,
orgIssues: `${organization}/issues`,
};

View file

@ -0,0 +1,369 @@
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { useQuasar } from "quasar";
import axios from "axios";
import { storeToRefs } from "pinia";
import { useCounterMixin } from "@/stores/mixin";
import { usePositionKeycloakStore } from "@/stores/positionKeycloak";
import { useMenuDataStore } from "@/stores/menuList";
import http from "@/plugins/http";
import config from "@/app.config";
import DialogHeader from "@/components/DialogHeader.vue";
const $q = useQuasar();
const store = usePositionKeycloakStore();
const { dataPositionKeycloak } = storeToRefs(store);
const { findOrgName } = store;
const { menuList } = storeToRefs(useMenuDataStore());
const { dialogConfirm, showLoader, hideLoader, messageError, success } =
useCounterMixin();
const modal = defineModel<boolean>("modal", {
default: false,
});
const title = computed(() => "แจ้งปัญหาการใช้งานระบบ");
const orgName = computed(() => findOrgName(dataPositionKeycloak.value) || "");
const optionData = computed(() => {
return menuList.value.map((menu) => ({
label: menu.sysName,
value: menu.sysName,
disable: menu?.children?.length === 0 ? false : true,
children: menu?.children?.map((subMenu) => ({
label: subMenu.sysName,
value: `${menu.sysName}/${subMenu.sysName}`,
})),
}));
});
const menuSelect = ref();
const optionsMenu = ref(optionData.value);
const formData = reactive({
title: "",
description: "",
system: "mgt",
fileAttachments: [] as File[],
menu: "",
org: orgName.value,
});
/** ฟังก์ชันบันทึกข้อมูล */
function onSubmit() {
dialogConfirm($q, async () => {
try {
showLoader();
const payload = {
title: formData.title,
description: formData.description,
system: formData.system,
menu: formData.menu,
org: formData.org,
};
const res = await http.post(config.API.orgIssues, payload);
const issueCode = res.data.result.codeIssue;
await uploadProfile(issueCode);
success($q, "บันทึกข้อมูลเรียบร้อย");
onClose();
} catch (error) {
messageError($q, error);
} finally {
hideLoader();
}
});
}
/**
* งกนเพมไฟล
* @param files ไฟลองการเพ
*/
async function onAddfile(files: any) {
files.forEach((file: any) => {
formData.fileAttachments.push(file);
});
}
/**
* งกนลบไฟล
* @param files ไฟลองการลบ
*/
async function onRemoveFile(files: any) {
files.forEach((file: any) => {
const index = formData.fileAttachments.findIndex(
(x: any) => x.__key == file.__key
);
if (index > -1) {
formData.fileAttachments.splice(index, 1);
}
});
}
/**
* งกนสราง url ปโหลดไฟล
* @param code รห issue
*/
async function uploadProfile(code: string) {
if (formData.fileAttachments.length === 0) {
return;
}
try {
const fileName = formData.fileAttachments.map((file) => ({
fileName: file.name,
}));
const res = await http.post(
config.API.file("issueAttachments", "documents", code),
{
replace: false,
fileList: fileName,
}
);
for (const file of formData.fileAttachments) {
const fileInfo = res.data[file.name];
if (fileInfo && fileInfo.uploadUrl) {
await uploadFileDoc(fileInfo.uploadUrl, file);
}
}
} catch (e) {
messageError($q, e);
}
}
/**
* งกนอปโหลดไฟลเอกสาร
* @param uploadUrl งกปโหลดไฟล
* @param file ไฟลองการอปโหลด
*/
async function uploadFileDoc(uploadUrl: string, file: any) {
try {
await axios.put(uploadUrl, file, {
headers: {
"Content-Type": file.type,
},
});
} catch (e) {
messageError($q, e);
}
}
/**
* งกนกรองขอมลใน select
* @param val าทกรอง
* @param update งกนอปเดตคาหลงกรอง
*/
function filterSelector(val: string, update: Function) {
update(() => {
if (!val) {
optionsMenu.value = optionData.value;
return;
}
optionsMenu.value = optionData.value
.map((parent: any) => {
const matchParent = parent.label.indexOf(val) > -1;
const filteredChildren = (parent.children || []).filter(
(child: any) => child.label.indexOf(val) > -1
);
if (matchParent) {
return { ...parent };
} else if (filteredChildren.length > 0) {
return { ...parent, children: filteredChildren };
}
return null;
})
.filter((item: any) => item !== null);
});
}
/** ฟังก์ชันปิด dialog และรีเซ็ตข้อมูล */
function onClose() {
modal.value = false;
formData.menu = "";
formData.title = "";
formData.description = "";
formData.fileAttachments = [];
formData.org = orgName.value;
}
</script>
<template>
<q-dialog v-model="modal" persistent>
<q-card style="width: 700px; max-width: 80vw">
<q-form greedy @submit.prevent @validation-success="onSubmit">
<DialogHeader :tittle="title" :close="onClose" />
<q-separator />
<q-card-section>
<div class="row col q-col-gutter-md">
<div class="col-12">
<q-select
ref="menuSelect"
dense
outlined
label="ระบบ"
v-model="formData.menu"
:options="optionsMenu"
class="inputgreen"
:rules="[ (val: string) => !!val || 'กรุณาเลือกระบบ' ]"
hide-bottom-space
emit-value
map-options
use-input
@filter="(inputValue: string,
doneFn: Function) => filterSelector(inputValue, doneFn,
)"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
<q-list v-if="scope.opt.children">
<q-item
v-for="child in scope.opt.children"
:key="child.value"
clickable
@click.stop="
formData.menu = child.value;
menuSelect.hidePopup();
"
>
<q-item-section class="q-ml-md">
{{ child.label }}
</q-item-section>
</q-item>
</q-list>
</template>
</q-select>
</div>
<div class="col-12">
<q-input
dense
outlined
label="หัวข้อปัญหา"
v-model="formData.title"
class="inputgreen"
:rules="[ (val: string) => !!val || 'กรุณากรอกหัวข้อปัญหา' ]"
hide-bottom-space
/>
</div>
<div class="col-12">
<q-input
dense
outlined
type="textarea"
label="รายละเอียดปัญหา"
v-model="formData.description"
class="inputgreen"
:rules="[ (val: string) => !!val || 'กรุณากรอกรายละเอียดปัญหา' ]"
hide-bottom-space
/>
</div>
<div class="col-12">
<q-uploader
color="gray"
type="file"
flat
ref="uploader"
class="full-width"
text-color="dark"
:max-size="10000000"
accept=".jpg,.png,.pdf,.csv,.doc"
bordered
label="[ไฟล์ jpg,png,pdf,csv,doc ขนาดไม่เกิน 10MB]"
multiple
@added="onAddfile"
@removed="onRemoveFile"
>
<template v-slot:header="scope">
<div
class="row no-wrap items-center q-pa-sm q-gutter-xs text-white"
>
<q-btn
v-if="scope.queuedFiles.length > 0"
icon="clear_all"
@click="scope.removeQueuedFiles"
round
dense
flat
>
<q-tooltip>ลบทงหมด</q-tooltip>
</q-btn>
<q-btn
v-if="scope.uploadedFiles.length > 0"
icon="done_all"
@click="scope.removeUploadedFiles"
round
dense
flat
>
<q-tooltip>ลบไฟลปโหลด</q-tooltip>
</q-btn>
<q-spinner
v-if="scope.isUploading"
class="q-uploader__spinner"
/>
<div class="col">
<div class="q-uploader__title">
{{ "[ไฟล์ jpg,png,pdf,csv,doc ขนาดไม่เกิน 10MB]" }}
</div>
<div class="q-uploader__subtitle">
{{ scope.uploadSizeLabel }}
/
{{ scope.uploadProgressLabel }}
</div>
</div>
<q-btn
v-if="scope.canAddFiles"
type="a"
icon="add_box"
@click="scope.pickFiles"
round
dense
flat
>
<q-uploader-add-trigger />
<q-tooltip>เลอกไฟล</q-tooltip>
</q-btn>
<q-btn
v-if="scope.isUploading"
icon="clear"
@click="scope.abort"
round
dense
flat
>
<q-tooltip>ยกเลกการอปโหลด</q-tooltip>
</q-btn>
</div>
</template>
</q-uploader>
</div>
</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<q-btn
type="submit"
for="#submitForm"
class="q-px-md items-center"
color="public"
label="บันทึก"
>
<q-tooltip>นทกขอม</q-tooltip>
</q-btn>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<style scoped></style>

View file

@ -0,0 +1,60 @@
import { ref } from "vue";
import { defineStore } from "pinia";
export const usePositionKeycloakStore = defineStore("positionKeycloak", () => {
const dataPositionKeycloak = ref<any>(null);
function setPositionKeycloak(data: any) {
dataPositionKeycloak.value = data;
}
function findOrgName(obj: any) {
if (obj) {
let name =
obj.child4 != null &&
obj.child4 !== "" &&
obj.child3 != null &&
obj.child3 !== ""
? obj.child4 + (obj.child3 ? "/" : "")
: obj.child4 != null && obj.child4 !== ""
? obj.child4
: "";
name +=
obj.child3 != null &&
obj.child3 !== "" &&
obj.child2 != null &&
obj.child2 !== ""
? obj.child3 + (obj.child2 ? "/" : "")
: obj.child3 != null && obj.child3 !== ""
? obj.child3
: "";
name +=
obj.child2 != null &&
obj.child2 !== "" &&
obj.child1 != null &&
obj.child1 !== ""
? obj.child2 + (obj.child1 ? "/" : "")
: obj.child2 != null && obj.child2 !== ""
? obj.child2
: "";
name +=
obj.child1 != null &&
obj.child1 !== "" &&
obj.root != null &&
obj.root !== ""
? obj.child1 + (obj.root ? "/" : "")
: obj.child1 != null && obj.child1 !== ""
? obj.child1
: "";
name += obj.root != null && obj.root !== "" ? obj.root : "";
return name == "" ? "-" : name;
} else {
return "";
}
}
return { dataPositionKeycloak, setPositionKeycloak, findOrgName };
});

View file

@ -6,6 +6,7 @@ import { storeToRefs } from "pinia";
import { scroll, useQuasar } from "quasar";
import { useCounterMixin } from "@/stores/mixin";
import { useMenuDataStore } from "@/stores/menuList";
import { usePositionKeycloakStore } from "@/stores/positionKeycloak";
import {
tokenParsed,
logout,
@ -26,6 +27,7 @@ import type {
import { tabList, tabListPlacement } from "../interface/request/main/main";
import LoginLinkage from "@/components/LoginLinkage.vue";
import DialogDebug from "@/components/Dialogs/DialogDebug.vue";
// landing page config url
const configParam = {
@ -63,6 +65,7 @@ const modalLoginLinkage = ref<boolean>(false); //เข้าสู่ระบ
// landing page redirect
const landingPageUrl = ref<string>(configParam.landingPageUrl);
const modalDebug = ref<boolean>(false);
async function fetchmsgNoread() {
await http
@ -524,6 +527,7 @@ async function fetchKeycloakPosition() {
.get(config.API.keycloakPosition())
.then(async (res) => {
const data = await res.data.result;
usePositionKeycloakStore().setPositionKeycloak(data);
if (data.avatarName) {
await getImg(data.profileId, data.avatarName);
} else {
@ -783,6 +787,21 @@ function onViewDetailNoti(url: string) {
</q-item-section>
</q-item>
<q-item clickable @click="modalDebug = true">
<q-item-section avatar>
<q-avatar
color="warning"
text-color="white"
icon="mdi-bug"
size="24px"
font-size="14px"
/>
</q-item-section>
<q-item-section class="q-py-sm">
แจงปญหาการใชงานระบบ
</q-item-section>
</q-item>
<q-item clickable @click="modalLoginLinkage = true">
<q-item-section avatar>
<q-avatar
@ -1216,6 +1235,7 @@ function onViewDetailNoti(url: string) {
</q-page-container>
<full-loader :visibility="loader" />
<LoginLinkage v-model:modal="modalLoginLinkage" />
<DialogDebug v-model:modal="modalDebug" />
</q-layout>
</template>