ระบบจัดการประเภทของปัญหา

This commit is contained in:
Net 2024-02-16 16:20:18 +07:00
parent c6b9236a04
commit b4c4325cd1
11 changed files with 813 additions and 77 deletions

View file

@ -0,0 +1,122 @@
<script setup lang="ts">
import { ref } from "vue";
import { useSupportStore } from "@/modules/00_support/store/Main";
const store = useSupportStore();
const open = ref<boolean>(false);
const statusIssue: ("new" | "ongoing" | "resolved")[] = [
"new",
"ongoing",
"resolved",
];
const status = ref<"new" | "ongoing" | "resolved">(store.correntStatusIssue);
defineProps({
issueId: String,
});
</script>
<template>
<q-btn icon="more_vert" dense flat size="11px" @click.stop>
<q-menu transition-show="jump-down" transition-hide="jump-up">
<q-list style="min-width: 100px">
<q-item
clickable
v-close-popup
@click="
() => {
console.log('เเก้ไข ' + issueId);
open = true;
}
"
>
<q-item-section>
<div class="row items-center white">
<q-icon name="o_edit" color="positive" />
<span class="q-ml-sm">แกไขสถานะ</span>
</div></q-item-section
>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-dialog v-model="open" persistent>
<q-card>
<q-card-section class="row items-center">
<div class="flex items-center" style="flex-wrap: nowrap">
<div>
<h6 class="q-my-none">แกไขสถานะ</h6>
<q-btn-dropdown
v-if="store.currentIssue"
:label="
store.correntStatusIssue == 'new'
? 'ปัญหาใหม่'
: store.correntStatusIssue == 'ongoing'
? 'กำลังดำเนินการ'
: 'เสร็จสิ้น'
"
@click.stop
>
<q-list v-for="itemStatusIssue in statusIssue">
<q-item
clickable
v-close-popup
@click="
() => {
status = itemStatusIssue;
}
"
>
<q-item-section>
<q-item-label>{{
itemStatusIssue == "new"
? "ปัญหาใหม่"
: itemStatusIssue == "ongoing"
? "กำลังดำเนินการ"
: "เสร็จสิ้น"
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
@click="
() => {
console.log('ยกเลิกการลบ');
}
"
label="ยกเลิก"
flat
v-close-popup
id="dialogDeleteClose"
/>
<q-btn
@click="
() => {
store.ChangeStatusIssue(
store.currentIssue,
store.currentTitle,
store.currentCategoryId,
status
);
}
"
color="primary"
v-close-popup
label="ยืนยัน"
id="dialogDeleteConfirm"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style scoped></style>

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import "moment/dist/locale/th";
import moment from "moment";
import DialogStatus from "@/modules/00_support/components/DialogStatus.vue";
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useSupportStore } from "@/modules/00_support/store/Main";
@ -15,11 +16,11 @@ const { scrollContainer } = storeToRefs(store);
onMounted(async () => {
await store.fetchIssue();
totalPageIssue.value = Math.ceil(store.currentTotalIssue || 0 / 6);
totalPageIssue.value = Math.ceil((store.currentTotalIssue || 0) / 6);
});
const onLoad = (async (_: any, done: any) => {
const totalPages = Math.ceil(store.currentTotalMessage || 0 / 30);
const totalPages = Math.ceil((store.currentTotalMessage || 1) / 30);
if (store.currentPage && totalPages > store.currentPage) {
await store.loadMessage(store.currentPage + 1);
done();
@ -28,7 +29,15 @@ const onLoad = (async (_: any, done: any) => {
</script>
<template>
<p class="text-h6 text-weight-medium">ถาม - ตอบ</p>
<div class="flex q-mt-md">
<p class="text-h6 text-weight-medium align-center">ถาม - ตอบ</p>
<q-space />
<p>
<router-link to="/category"
><q-btn dense size="16px">ดการประเภทของปญหา</q-btn></router-link
>
</p>
</div>
<div class="container">
<div class="i1 bg-white align-center">
@ -54,66 +63,154 @@ const onLoad = (async (_: any, done: any) => {
<q-avatar>
<img src="https://cdn.quasar.dev/img/avatar3.jpg" />
</q-avatar>
<q-item-section class="q-pl-sm">
<q-item-label>{{ store.currentTitle }}</q-item-label>
<q-item-section v-if="store.currentIssue" class="q-pl-sm">
<q-item-label
>{{ store.currentTitle }}
<q-badge color="blue">
{{ store.currentCategory }}
</q-badge></q-item-label
>
</q-item-section>
<q-space />
<div v-if="store.currentIssue">
<q-badge
color="white"
text-color="black"
:label="
store.correntStatusIssue == 'new'
? 'ปัญหาใหม่'
: store.correntStatusIssue == 'ongoing'
? 'กำลังดำเนินการ'
: 'เสร็จสิ้น'
"
/>
<dialog-status />
</div>
<!-- <q-btn-dropdown
v-if="store.currentIssue"
:label="
store.correntStatusIssue == 'new'
? 'ปัญหาใหม่'
: store.correntStatusIssue == 'ongoing'
? 'กำลังดำเนินการ'
: 'เสร็จสิ้น'
"
dense
flat
color="positive"
@click.stop
>
<q-list v-for="itemStatusIssue in statusIssue">
<q-item
clickable
v-close-popup
@click="
() => {
store.ChangeStatusIssue(
store.currentIssue,
store.currentTitle,
store.currentCategoryId,
itemStatusIssue
);
}
"
>
<q-item-section>
<q-item-label>{{
itemStatusIssue == "new"
? "ปัญหาใหม่"
: itemStatusIssue == "ongoing"
? "กำลังดำเนินการ"
: "เสร็จสิ้น"
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown> -->
<q-btn flat round dense icon="o_info" text-color="grey" />
</q-toolbar>
</div>
<div class="i3 bg-white">
<q-list class="text-primary">
<div v-for="data in store.issue?.result">
<q-item
clickable
v-ripple
:active="store.currentIssue === data.id"
@click="
store.currentIssue = data.id;
store.currentTitle = data.title;
store.fetchMessage(data.id);
store.fetchMessageStatus(data.id);
<div v-for="(item, index) in store.issue?.result" :key="index">
<div
@click="
async () => {
store.currentIssue = item.id;
store.currentTitle = item.title;
store.correntStatusIssue = item.status;
store.currentCategory = item.category.name;
store.issue
? (store.issue.result = store.issue.result.map((v) => {
if (v.id === item.id) {
v.unreadCount = 0;
}
return v;
}))
: '';
await store.fetchMessageStatus(item.id);
await store.fetchMessage(item.id);
}
"
:class="{ active: store.currentIssue === item.id }"
class="noactive row q-py-sm justify-between items-center q-px-md"
>
<div class="col-10 row items-center">
<div class="noactive-avatar">
<q-avatar color="grey-2" text-color="white" size="40px">
<img src="https://cdn.quasar.dev/img/avatar1.jpg" />
</q-avatar>
</div>
<div class="col column q-ml-md">
<span class="text-long col text-weight-bold line-ellipsis">
{{ item.title }}
</span>
if (store.issue) {
store.issue.result = store.issue.result.map((v) => {
if (v.id === data.id) v.unreadCount = 0;
return v;
});
}
"
active-class="my-menu-link"
>
<q-avatar>
<img src="https://cdn.quasar.dev/img/avatar1.jpg" />
</q-avatar>
<q-item-section class="q-pl-sm">
<q-item-label>{{ data.title }}</q-item-label>
<q-item-label class="flex" caption>
{{ data.lastMessage }}
<q-space />
<q-icon
v-if="data.lastMessage?.length === 0"
color="green"
size="18px"
name="mdi-send"
<span
class="col text-caption line-ellipsis"
:class="{
'text-weight-bold': item.unreadCount > 0,
'text-grey-8': item.unreadCount > 0,
'text-grey': item.unreadCount === 0,
}"
>{{ item.lastMessage }}</span
>
</div>
<div></div>
</div>
<div class="col-2 items-center text-right">
<q-icon
v-if="item.lastMessage?.length === 0"
name="mdi-send"
size="xs"
color="primary"
/>
<div v-else class="column">
<span class="col text-caption text-grey">20 s</span>
<div class="col">
<q-badge
v-if="item.unreadCount > 0"
rounded
color="negative"
text-color="white"
:label="item.unreadCount"
/>
<div v-else>
<q-badge
v-if="data.unreadCount > 0"
class=""
rounded
color="red"
:label="data.unreadCount"
/>
<q-icon v-else size="18px" name="done_all" />
</div>
</q-item-label>
</q-item-section>
</q-item>
<q-separator inset />
<q-icon v-else name="mdi-check-all" size="xs" color="grey" />
</div>
</div>
</div>
</div>
</q-list>
<q-separator inset />
</div>
</div>
<div class="i4 bg-grey-3" v-if="store.currentIssue">
@ -198,29 +295,35 @@ const onLoad = (async (_: any, done: any) => {
</div> -->
<div class="input-chat">
<q-input
dense
class="q-pa-xs"
standout="bg-teal text-white"
label="Aa"
v-model="content"
@keydown.enter.prevent="
() => {
store.sendMessage(content, store.currentIssue);
content = '';
if (store.currentIssue) {
store.sendMessage(content, store.currentIssue);
content = '';
}
}
"
/>
outlined
dense
placeholder="Aa"
v-model="content"
id="message"
>
</q-input>
</div>
<div class="btn-chat">
<q-btn
@click="
() => {
store.sendMessage(content, store.currentIssue);
content = '';
if (store.currentIssue) {
store.sendMessage(content, store.currentIssue);
content = '';
}
}
"
flat
color="primary"
class="col-2"
style="color: #009789"
label="ส่งข้อความ"
/>
</div>
@ -229,9 +332,24 @@ const onLoad = (async (_: any, done: any) => {
</template>
<style scoped>
.scroll {
overflow-y: auto;
.text-long {
width: 100px;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.active {
background: #ebf9f7;
border-left: 5px solid #03a898;
& {
.noactive-avatar {
border-radius: 50%;
border: 1px solid #03a898;
}
}
}
.align-center {
display: flex;
align-items: center;

View file

@ -0,0 +1,122 @@
<script setup lang="ts">
import { ref } from "vue";
import { useSupportStore } from "@/modules/00_support/store/Main";
const store = useSupportStore();
const open = ref<boolean>(false);
const statusIssue: ("new" | "ongoing" | "resolved")[] = [
"new",
"ongoing",
"resolved",
];
const status = ref<"new" | "ongoing" | "resolved">(store.correntStatusIssue);
defineProps({
issueId: String,
});
</script>
<template>
<q-btn icon="more_vert" dense flat size="11px" @click.stop>
<q-menu transition-show="jump-down" transition-hide="jump-up">
<q-list style="min-width: 100px">
<q-item
clickable
v-close-popup
@click="
() => {
console.log('เเก้ไข ' + issueId);
open = true;
}
"
>
<q-item-section>
<div class="row items-center white">
<q-icon name="o_edit" color="positive" />
<span class="q-ml-sm">แกไขสถานะ</span>
</div></q-item-section
>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-dialog v-model="open" persistent>
<q-card>
<q-card-section class="row items-center">
<div class="flex items-center" style="flex-wrap: nowrap">
<div>
<h6 class="q-my-none">แกไขสถานะ</h6>
<q-btn-dropdown
v-if="store.currentIssue"
:label="
store.correntStatusIssue == 'new'
? 'ปัญหาใหม่'
: store.correntStatusIssue == 'ongoing'
? 'กำลังดำเนินการ'
: 'เสร็จสิ้น'
"
@click.stop
>
<q-list v-for="itemStatusIssue in statusIssue">
<q-item
clickable
v-close-popup
@click="
() => {
status = itemStatusIssue;
}
"
>
<q-item-section>
<q-item-label>{{
itemStatusIssue == "new"
? "ปัญหาใหม่"
: itemStatusIssue == "ongoing"
? "กำลังดำเนินการ"
: "เสร็จสิ้น"
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
@click="
() => {
console.log('ยกเลิกการลบ');
}
"
label="ยกเลิก"
flat
v-close-popup
id="dialogDeleteClose"
/>
<q-btn
@click="
() => {
store.ChangeStatusIssue(
store.currentIssue,
store.currentTitle,
store.currentCategoryId,
status
);
}
"
color="primary"
v-close-popup
label="ยืนยัน"
id="dialogDeleteConfirm"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style scoped></style>

View file

@ -0,0 +1,103 @@
<script setup lang="ts">
import { useSupportStore } from "@/modules/00_support/store/Main";
const store = useSupportStore();
const [open] = defineModel<Boolean>();
const input = defineModel<string>("input");
const prop = withDefaults(
defineProps<{
open: boolean;
status: string;
category: string;
input: string;
}>(),
{}
);
function controlAction(
categoryId: string = "",
statusAction?: string,
name: string = ""
) {
if (statusAction == "add") store.newCategory(name);
if (statusAction == "edit") store.editCategory(categoryId, name);
if (statusAction == "delete") store.deleteCategory(categoryId);
}
</script>
<template>
<q-dialog v-model="prop.open" persistent>
<q-card>
<q-card-section>
<div class="flex items-center" style="flex-wrap: nowrap">
<div class="q-pa-sm">
<h6 v-if="prop.status == 'add'" class="q-my-none">
เพมประเภทของปญหา
</h6>
<h6 v-if="prop.status == 'edit'" class="q-my-none">
แกไขประเภทของปญหา
</h6>
<div
v-if="prop.status == 'delete'"
style="border-radius: 50%"
class="q-pa-sm"
>
<q-icon
name="mdi-trash-can-outline"
color="negative"
size="2.5rem"
/>
</div>
</div>
<div v-if="prop.status == 'delete'">
<h6 class="q-my-none">นยนการลบขอม</h6>
<p class="q-my-none">องการยนยนการลบขอมลนหรอไม</p>
</div>
</div>
</q-card-section>
<q-card-section
style="width: 400px"
class="items-center"
v-if="prop.status !== 'delete'"
>
<div style="flex-wrap: ">
<div>
<q-input dense outlined v-model="input" />
</div>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
@click="
() => {
open = false;
}
"
label="ยกเลิก"
flat
v-close-popup
id="dialogDeleteClose"
/>
<q-btn
@click="
() => {
controlAction(prop.category, prop.status, input);
open = false;
}
"
:color="prop.status == 'delete' ? 'negative' : 'primary'"
v-close-popup
label="ยืนยัน"
id="dialogDeleteConfirm"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style scoped></style>

View file

@ -0,0 +1,92 @@
<script setup lang="ts">
import { useSupportStore } from "@/modules/00_support/store/Main";
import DialogCategory from "@/modules/00_support/components/category/DialogCategory.vue";
import { onMounted, ref } from "vue";
const store = useSupportStore();
const open = ref<boolean>(false);
const currentName = ref<string>("");
const status = ref<"add" | "edit" | "delete">("add");
const categoryId = ref<string>("");
onMounted(async () => {
await store.fetchCategory();
});
</script>
<template>
<div class="flex q-my-md">
<p class="text-h6 text-weight-medium align-center">ดการประเภคของปญหา</p>
<q-space />
<q-btn
dense
size="16px"
@click="
() => {
open = true;
status = 'add';
}
"
>เพมประเภทของปญหา</q-btn
>
</div>
<div>
<q-table
:rows="store.rowsCategory?.result"
:columns="store.columnsCategory"
row-key="name"
>
<template v-slot:body-cell-actions="data">
<q-td>
<div>
<q-btn
class="q-ma-sm"
flat
dense
id="listViewFolderEdit"
icon="o_edit"
color="positive"
@click="
() => {
open = true;
status = 'edit';
categoryId = data.row.id;
currentName = data.row.name;
console.log(currentName);
}
"
/>
<q-btn
class="q-ma-sm"
flat
dense
id="listViewFolderDelete"
color="negative"
icon="mdi-trash-can-outline"
:data-testid="data.row.name"
@click="
() => {
open = true;
status = 'delete';
categoryId = data.row.id;
}
"
/>
</div>
</q-td>
</template>
</q-table>
</div>
<dialog-category
v-model.open="open"
:open="open"
:status="status"
:category="categoryId"
:input="currentName"
/>
</template>
<style scoped></style>

View file

@ -30,7 +30,7 @@ export interface SupportIssue {
createdAt: string;
updatedAt: string;
title: string;
status: string;
status: "new" | "ongoing" | "resolved";
category: SupportIssueCategory;
unreadCount: number;
lastMessage: string;

View file

@ -1,4 +1,6 @@
const supportMain = () => import("@/modules/00_support/views/MainPage.vue");
const supportCategory = () =>
import("@/modules/00_support/views/ManageCategory.vue");
export default [
{
@ -11,4 +13,14 @@ export default [
Role: "evaluate",
},
},
{
path: "/category",
name: "supportCategory",
component: supportCategory,
meta: {
Auth: true,
Key: [1.1],
Role: "evaluate",
},
},
];

View file

@ -10,9 +10,10 @@ import type {
SupportIssueResponse,
SupportStatusUser,
SupportMessageStatus,
SupportIssueCategory,
} from "@/modules/00_support/interface/index/Main";
import keycloak from "@/plugins/keycloak";
import { useQuasar } from "quasar";
import { useQuasar, type QTableProps } from "quasar";
export const useSupportStore = defineStore("supportServiceStore", () => {
const { showLoader, hideLoader, messageError } = useCounterMixin();
@ -24,11 +25,40 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
const statusUser = ref<SupportStatusUser[]>([]);
const currentIssue = ref<string>("");
const currentTitle = ref<string>("");
const currentCategoryId = ref<string>("");
const correntStatusIssue = ref<"new" | "ongoing" | "resolved">("new");
const currentTotalMessage = ref<number>();
const currentPage = ref<number>();
const scrollContainer = ref();
const currentPageIssue = ref<number>();
const currentTotalIssue = ref<number>();
const currentCategory = ref<string>();
const columnsCategory = [
{
name: "id",
label: "id",
align: "center",
field: "id",
sortable: true,
},
{
name: "name",
align: "center",
label: "name",
field: "name",
sortable: true,
},
{
name: "actions",
align: "center",
label: "",
field: "",
},
] satisfies QTableProps["columns"];
const rowsCategory = ref<SupportIssueCategory>();
function scrollToEnd(position: Number = 1) {
setTimeout(() => {
scrollContainer.value?.setScrollPercentage("vertical", position);
@ -146,10 +176,37 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
}
}
async function ChangeStatusIssue(
issueId: string,
title: string,
categoryId: string,
status: "new" | "ongoing" | "resolved"
) {
showLoader();
const requestBody = {
title: title,
categoryId: categoryId,
status: status,
};
const res = await http
.patch(config.API.supportIssueChangeStatus(issueId), requestBody)
.catch((err) => {
messageError($q, err);
})
.finally(() => {
hideLoader();
});
if (res) {
fetchIssue();
correntStatusIssue.value = status;
}
}
async function fetchIssue(page: number = 1) {
showLoader();
const res = await http
.get(`${config.API.supportIssue}?page=${page}&&pageSize=6`)
.get(`${config.API.supportIssue}?page=${page}&pageSize=6`)
.catch((err) => {
messageError($q, err);
})
@ -159,29 +216,110 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
if (res && res.data) {
issue.value = res.data;
currentPageIssue.value = res.data.page;
currentTotalIssue.value = res.data.total;
}
}
async function newCategory(name: string) {
showLoader();
const res = await http
.post(config.API.supportCategory, {
name: name,
})
.catch((err) => {
messageError($q, err);
})
.finally(() => {
hideLoader();
});
if (res) {
fetchCategory();
}
}
async function fetchCategory() {
showLoader();
const res = await http
.get(config.API.supportCategory)
.catch((err) => {
messageError($q, err);
})
.catch((err) => {
messageError($q, err);
})
.finally(() => {
hideLoader();
});
if (res && res.data) {
rowsCategory.value = res.data;
}
}
async function deleteCategory(CategoryId: string) {
showLoader();
const res = await http
.delete(config.API.supportCategoryAction(CategoryId))
.catch((err) => {
messageError($q, err);
})
.finally(() => {
hideLoader();
});
if (res) {
fetchCategory();
}
}
async function editCategory(CategoryId: string, name: string) {
showLoader();
const res = await http
.patch(config.API.supportCategoryAction(CategoryId), {
name: name,
})
.catch((err) => {
messageError($q, err);
})
.finally(() => {
hideLoader();
});
if (res) {
fetchCategory();
}
}
return {
userId,
issue,
message,
fetchIssue,
fetchMessage,
fetchMessageStatus,
sendMessage,
scrollToEnd,
scrollContainer,
currentIssue,
currentTitle,
socket,
messageStatus,
loadMessage,
currentTotalMessage,
currentPage,
currentPageIssue,
currentTotalIssue,
currentCategoryId,
correntStatusIssue,
currentCategory,
rowsCategory,
columnsCategory,
fetchIssue,
fetchMessage,
fetchMessageStatus,
sendMessage,
scrollToEnd,
loadMessage,
ChangeStatusIssue,
newCategory,
fetchCategory,
deleteCategory,
editCategory,
};
});

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import FormChat from "@/modules/00_support/components/FormChat.vue";
import { useSupportStore } from "@/modules/00_support/store/Main.ts";
import { useSupportStore } from "@/modules/00_support/store/Main";
import { onMounted, onUnmounted, ref } from "vue";
const store = useSupportStore();

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useSupportStore } from "@/modules/00_support/store/Main";
import TableCategory from "@/modules/00_support/components/category/TableCategory.vue";
import { onMounted, onUnmounted, ref } from "vue";
const store = useSupportStore();
onMounted(async () => {
store.socket.connect();
});
onUnmounted(async () => {
store.socket.disconnect();
});
</script>
<template>
<table-category />
</template>
<style scoped></style>