Merge branch 'phatt-dev' into develop

This commit is contained in:
puri-ph4tt 2024-02-16 17:30:46 +07:00
commit e6d623074d
8 changed files with 280 additions and 193 deletions

63
package-lock.json generated
View file

@ -23,6 +23,7 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"quasar": "^2.11.1", "quasar": "^2.11.1",
"socket.io-client": "^4.7.4",
"structure-chart": "^0.0.9", "structure-chart": "^0.0.9",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
@ -1191,6 +1192,11 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@tato30/vue-pdf": { "node_modules/@tato30/vue-pdf": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/@tato30/vue-pdf/-/vue-pdf-1.6.2.tgz", "resolved": "https://registry.npmjs.org/@tato30/vue-pdf/-/vue-pdf-1.6.2.tgz",
@ -2894,7 +2900,6 @@
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
@ -3056,6 +3061,26 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io-client": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enquirer": { "node_modules/enquirer": {
"version": "2.3.6", "version": "2.3.6",
"dev": true, "dev": true,
@ -5456,7 +5481,6 @@
}, },
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
@ -6702,6 +6726,32 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/socket.io-client": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"dev": true, "dev": true,
@ -7804,7 +7854,6 @@
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.11.0", "version": "8.11.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@ -7835,6 +7884,14 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"dev": true, "dev": true,

View file

@ -1,53 +1,44 @@
/**config api */ /**config api */
import { ref } from "vue"; import { ref } from "vue"
const env = ref<string>(process.env.NODE_ENV || "development"); const env = ref<string>(process.env.NODE_ENV || "development")
export const apiUrlConfig = import.meta.env.VITE_API_URI_CONFIG; export const apiUrlConfig = import.meta.env.VITE_API_URI_CONFIG
export const apiUrlConfigPublish = import.meta.env.VITE_API_PUBLISH_URL; export const apiUrlConfigPublish = import.meta.env.VITE_API_PUBLISH_URL
// if (process.env.VUE_APP_TEST) { // if (process.env.VUE_APP_TEST) {
// env = "test"; // env = "test";
// } // }
const config = ref<any>({ const config = ref<any>({
development: { development: {
// API_URI: "https://localhost:7260/api", // API_URI: "https://localhost:7260/api",
API_URI: "https://bma-ehr.frappet.synology.me/api/v1", API_URI: "https://bma-ehr.frappet.synology.me/api/v1",
API_URL_SUPPORT: "http://192.168.1.10:3000/api/v1/support", API_URL_SUPPORT: "https://bma-ehr.frappet.synology.me/api/v1/support",
API_SUPPORT_SOCKET: "http://192.168.1.10:3000/", MEET_URI: "meet.frappet.com",
MEET_URI: "meet.frappet.com", LINK_EVALUATE_PUBLISH: "https://bma-ehr-publish.frappet.synology.me",
LINK_EVALUATE_PUBLISH: "https://bma-ehr-publish.frappet.synology.me", },
}, test: {
test: { API_URI: "http://localhost:5010/api/v1",
API_URI: "http://localhost:5010/api/v1", MEET_URI: "meet.frappet.com",
MEET_URI: "meet.frappet.com", },
}, production: {
production: { API_URI: apiUrlConfig,
API_URI: apiUrlConfig, API_URL_SUPPORT: `${apiUrlConfig}/support`,
API_URL_SUPPORT: "http://192.168.1.10:3000/api/v1/support", API_URI_ORG_TREE: "https://s3cluster.frappet.com/bma-ehr-fpt/organization/strueture/tree_20230707_115124.json",
API_SUPPORT_SOCKET: "http://192.168.1.10:3000/", MEET_URI: "meet.frappet.com",
API_URI_ORG_TREE: LINK_EVALUATE_PUBLISH: apiUrlConfigPublish,
"https://s3cluster.frappet.com/bma-ehr-fpt/organization/strueture/tree_20230707_115124.json", },
MEET_URI: "meet.frappet.com", })
LINK_EVALUATE_PUBLISH: apiUrlConfigPublish,
},
});
const API_URI = ref<string>(config.value[env.value].API_URI); const API_URI = ref<string>(config.value[env.value].API_URI)
const API_URL_SUPPORT = ref<string>(config.value[env.value].API_URL_SUPPORT); const API_URL_SUPPORT = ref<string>(config.value[env.value].API_URL_SUPPORT)
const API_SUPPORT_SOCKET = ref<string>( const MEET_URI = ref<string>(config.value[env.value].MEET_URI)
config.value[env.value].API_SUPPORT_SOCKET const LINK_EVALUATE_PUBLISH = ref<string>(config.value[env.value].LINK_EVALUATE_PUBLISH)
);
const MEET_URI = ref<string>(config.value[env.value].MEET_URI);
const LINK_EVALUATE_PUBLISH = ref<string>(
config.value[env.value].LINK_EVALUATE_PUBLISH
);
export default { export default {
env: env.value, env: env.value,
config: config.value, config: config.value,
API_URI: API_URI.value, API_URI: API_URI.value,
API_URL_SUPPORT: API_URL_SUPPORT.value, API_URL_SUPPORT: API_URL_SUPPORT.value,
API_SUPPORT_SOCKET: API_SUPPORT_SOCKET.value, MEET_URI: MEET_URI.value,
MEET_URI: MEET_URI.value, LINK_EVALUATE_PUBLISH: LINK_EVALUATE_PUBLISH.value,
LINK_EVALUATE_PUBLISH: LINK_EVALUATE_PUBLISH.value, }
};

View file

@ -1,7 +1,6 @@
import env from "../index"; import env from "../index";
const support = `${env.API_URL_SUPPORT}`; const support = `${env.API_URL_SUPPORT}`;
const socket = `${env.API_SUPPORT_SOCKET}`;
export default { export default {
supportMessageStatus: (id: string) => `${support}/issue/${id}/message-status`, supportMessageStatus: (id: string) => `${support}/issue/${id}/message-status`,
@ -22,5 +21,5 @@ export default {
supportIssueCategory: `${support}/issue-category`, supportIssueCategory: `${support}/issue-category`,
supportNewIssue: `${support}/issue`, supportNewIssue: `${support}/issue`,
supportSocket: `${socket}`, supportSocket: `${new URL(support).origin}`,
}; };

View file

@ -10,7 +10,7 @@ function dateChat(timestamp: string) {
const diff = moment().diff(parsedTimestamp); const diff = moment().diff(parsedTimestamp);
if (diff < 86400000) { if (diff < 86400000) {
const formattedDate = parsedTimestamp.format("HH:mm:") + " น."; const formattedDate = parsedTimestamp.format("HH:mm") + " น.";
return formattedDate; return formattedDate;
} else { } else {
const beYear = parsedTimestamp.year() + 543; const beYear = parsedTimestamp.year() + 543;
@ -37,11 +37,14 @@ function dateChat(timestamp: string) {
<span> {{ dateChat(item.createdAt) }} </span> <span> {{ dateChat(item.createdAt) }} </span>
</template> </template>
<template v-slot:avatar> <template v-slot:avatar>
<img <q-img
v-if="item.fromUserId === store.userId" v-if="item.fromUserId === store.userId"
class="q-message-avatar q-message-avatar--sent" src="@/assets/avatar_user.jpg"
src="https://cdn.quasar.dev/img/avatar4.jpg" class="q-ml-sm"
spinner-color="white"
style="height: 42px; max-width: 42px; border-radius: 50%"
/> />
<div <div
v-else v-else
class="q-mr-sm" class="q-mr-sm"

View file

@ -2,10 +2,12 @@
import { ref } from "vue"; import { ref } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useSupportStore } from "@/modules/00_support/store/Main"; import { useSupportStore } from "@/modules/00_support/store/Main";
import { useCounterMixin } from "@/stores/mixin";
import ChatMessage from "@/modules/00_support/components/ChatMessage.vue"; import ChatMessage from "@/modules/00_support/components/ChatMessage.vue";
const store = useSupportStore(); const store = useSupportStore();
const content = ref<string>(""); const content = ref<string>("");
const { date2Thai } = useCounterMixin();
const { scrollContainer } = storeToRefs(store); const { scrollContainer } = storeToRefs(store);
function getOnlineStatus(option: "icon" | "status") { function getOnlineStatus(option: "icon" | "status") {
@ -13,20 +15,26 @@ function getOnlineStatus(option: "icon" | "status") {
if (option === "icon") return isAdmin ? "green" : "grey"; if (option === "icon") return isAdmin ? "green" : "grey";
if (option === "status") return isAdmin ? "ออนไลน์" : "ออฟไลน์"; if (option === "status") return isAdmin ? "ออนไลน์" : "ออฟไลน์";
} }
const thaiOptions: Intl.DateTimeFormatOptions = {
hour: "2-digit",
minute: "2-digit",
};
</script> </script>
<template> <template>
<div <div
class="row q-py-md q-px-lg justify-between items-center" class="row q-py-md q-px-lg justify-between items-center"
v-if="store.currentTitle && store.currentTitle.length > 0" v-if="store.currentTitle && store.currentTitle.length > 0"
style="flex-wrap: nowrap"
> >
<div class="col row items-center"> <div class="row items-center" style="flex-wrap: nowrap">
<q-btn <q-btn
flat flat
padding="none" padding="none"
class="q-mr-md" class="q-mr-md"
icon="mdi-chevron-left" icon="mdi-chevron-left"
@click="store.isOpen = false" @click="store.openChat = false"
v-if="!$q.screen.gt.xs" v-if="!$q.screen.gt.xs"
/> />
<div style="border-radius: 50%; border: 1px solid teal"> <div style="border-radius: 50%; border: 1px solid teal">
@ -34,8 +42,8 @@ function getOnlineStatus(option: "icon" | "status") {
<q-icon :name="store.icon" size="24px" color="teal" /> <q-icon :name="store.icon" size="24px" color="teal" />
</q-avatar> </q-avatar>
</div> </div>
<div class="col column q-ml-md"> <div class="column q-ml-md">
<span class="col text-weight-bold"> <span class="text-weight-bold ellipsis-2-lines">
{{ store.currentTitle }} {{ store.currentTitle }}
</span> </span>
<span> <span>
@ -56,7 +64,21 @@ function getOnlineStatus(option: "icon" | "status") {
name="mdi-information-outline" name="mdi-information-outline"
size="24px" size="24px"
color="grey" color="grey"
/> >
<q-tooltip class="column">
<span>{{ store.currentTitle }}</span>
<span>
{{ date2Thai(store.currentIssueDate) }}
{{
new Date(store.currentIssueDate).toLocaleTimeString(
"th-TH",
thaiOptions
)
}}
. <q-space />
</span>
</q-tooltip>
</q-icon>
</div> </div>
</div> </div>
<q-separator /> <q-separator />
@ -74,7 +96,10 @@ function getOnlineStatus(option: "icon" | "status") {
</q-scroll-area> </q-scroll-area>
<q-separator /> <q-separator />
<div class="row q-py-md q-px-lg justify-between items-center q-gutter-x-lg"> <div
class="row q-py-md q-px-lg justify-between items-center q-gutter-x-lg"
style="flex-wrap: nowrap"
>
<!-- <div> <!-- <div>
<q-btn flat disable class="col-1"> <q-btn flat disable class="col-1">
<q-icon <q-icon
@ -118,8 +143,10 @@ function getOnlineStatus(option: "icon" | "status") {
flat flat
class="col-2" class="col-2"
style="color: #009789" style="color: #009789"
label="ส่งข้อความ" >
/> <span v-if="$q.screen.gt.xs">ส่งข้อความ</span>
<q-icon v-else name="mdi-send" size="xs" color="primary" />
</q-btn>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,16 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { useSupportStore } from "@/modules/00_support/store/Main"; import { useSupportStore } from "@/modules/00_support/store/Main";
import DialogHeader from "@/components/DialogHeader.vue";
const store = useSupportStore(); const store = useSupportStore();
const isOpen = ref<boolean>(false);
const title = ref<string>(""); const title = ref<string>("");
const titleRef = ref(); const titleRef = ref();
const categoryId = ref<string>(""); const categoryId = ref<string>("");
const categoryRef = ref(); const categoryRef = ref();
const options = ref<{ label: string; value: string }[]>([]); const options = ref<{ label: string; value: string }[]>([]);
const props = defineProps({
modal: {
type: Boolean,
default: false,
},
clickClose: {
type: Function,
default: () => {},
},
});
onMounted(async () => { onMounted(async () => {
await store.fetchIssueCategory(); await store.fetchIssueCategory();
if (store.issueCategory) { if (store.issueCategory) {
@ -25,109 +36,77 @@ async function onSubmit() {
titleRef.value.validate(); titleRef.value.validate();
categoryRef.value.validate(); categoryRef.value.validate();
if (titleRef.value.hasError || categoryRef.value.hasError) return ""; if (titleRef.value.hasError || categoryRef.value.hasError) return "";
await store.newIssue(title.value, categoryId.value); await store.newIssue(title.value, categoryId.value).then(() => {
isOpen.value = false; props.clickClose();
title.value = "";
categoryId.value = "";
});
} }
</script> </script>
<template> <template>
<div <q-dialog v-model="props.modal" persistent>
@click="isOpen = true" <q-card style="width: 40vw; max-width: 40vw">
class="col-10 row items-center q-py-sm q-px-md new" <q-form ref="myForm" @submit.prevent.stop="onSubmit">
> <DialogHeader tittle="แจ้งปัญหา" :close="clickClose" />
<div class="noactive-avatar"> <q-separator />
<div class="new-avatar"> <q-card-section class="q-pa-sm bg-grey-1">
<q-icon name="mdi-plus" size="24px" color="primary" /> <div class="row col-12 q-col-gutter-sm">
</div> <div class="col-xs-12">
</div> <div class="col-12 row q-py-sm items-center q-col-gutter-sm">
<q-select
<div class="col column q-ml-md"> outlined
<span class="col text-grey"> เพมเรองปรกษาใหม </span> dense
</div> hide-bottom-space
</div> emit-value
map-options
<!-- Dialog --> lazy-rules
<q-dialog v-model="isOpen"> ref="categoryRef"
<q-card class="q-pa-md" style="width: 100%"> class="full-width inputgreen cursor-pointer"
<q-card-section class="row items-center"> option-value="value"
<div class="text-h6">เพมเรองปรกษาใหม</div> option-label="label"
<q-space /> label="หมวดหมู่"
<q-btn v-model="categoryId"
icon="close" :options="options"
unelevated :rules="[
round (val) =>
dense (val !== null && val !== '') || 'กรุณาเลือหมวดหมู่',
v-close-popup ]"
style="color: #ff8080; background-color: #ffdede" />
/> <q-input
</q-card-section> type="textarea"
ref="titleRef"
<form @submit.prevent.stop="onSubmit"> outlined
<q-card-section class="q-gutter-x-md row"> dense
<q-select hide-bottom-space
ref="categoryRef" lazy-rules
class="col-3" class="full-width inputgreen cursor-pointer"
outlined label="รายละเอียด"
dense v-model="title"
emit-value :rules="[
map-options (val) => (val && val.length > 0) || 'กรุณากรอกชื่อเรื่อง',
v-model="categoryId" ]"
:options="options" />
option-value="value" </div>
option-label="label" </div>
label="หมวดหมู่" </div>
lazy-rules
:rules="[
(val) => (val !== null && val !== '') || 'กรุณาเลือหมวดหมู่',
]"
/>
<q-input
ref="titleRef"
class="col-grow"
outlined
dense
v-model="title"
label="ชื่อเรื่อง"
lazy-rules
:rules="[
(val) => (val && val.length > 0) || 'กรุณาเพิ่มชื่อเรื่อง',
]"
/>
</q-card-section> </q-card-section>
<q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat v-close-popup label="ยกเลิก" text-color="primary" />
<q-btn <q-btn
dense
unelevated
label="แจ้งปัญหา"
color="public"
type="submit" type="submit"
label="บันทึก" class="q-px-md"
color="primary" v-close-popup
text-color="white" >
/> <!-- icon="mdi-content-save-outline" -->
<q-tooltip>แจงปญหา</q-tooltip>
</q-btn>
</q-card-actions> </q-card-actions>
</form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<style scoped lang="scss">
.new {
transition: 0.1s ease all;
opacity: 1;
cursor: pointer;
border-left: 5px solid transparent;
&:hover {
background: #ebf9f7;
}
}
.new-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px dashed #9e9e9e;
}
</style>

View file

@ -18,7 +18,7 @@ import { useCounterMixin } from "@/stores/mixin";
export const useSupportStore = defineStore("supportServiceStore", () => { export const useSupportStore = defineStore("supportServiceStore", () => {
const { showLoader, hideLoader, messageError } = useCounterMixin(); const { showLoader, hideLoader, messageError } = useCounterMixin();
const $q = useQuasar(); const $q = useQuasar();
const isOpen = ref<boolean>(false); const openChat = ref<boolean>(false);
const icon = ref<string>("mdi-account-check"); const icon = ref<string>("mdi-account-check");
const userId = ref<string | undefined>(keycloak.subject); const userId = ref<string | undefined>(keycloak.subject);
const userStatus = ref<SupportUserStatus[]>([]); const userStatus = ref<SupportUserStatus[]>([]);
@ -27,6 +27,7 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
const messageStatus = ref<SupportMessageStatusResponse>(); const messageStatus = ref<SupportMessageStatusResponse>();
const message = ref<SupportMessageResponse>(); const message = ref<SupportMessageResponse>();
const currentIssue = ref<string>(""); const currentIssue = ref<string>("");
const currentIssueDate = ref<any>("");
const currentTitle = ref<string>(""); const currentTitle = ref<string>("");
const items = ref<any>([{}, {}, {}, {}, {}, {}, {}]); const items = ref<any>([{}, {}, {}, {}, {}, {}, {}]);
const scrollContainer = ref(); const scrollContainer = ref();
@ -34,6 +35,7 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
const socket = io(config.API.supportSocket, { const socket = io(config.API.supportSocket, {
auth: { token: keycloak.token }, auth: { token: keycloak.token },
autoConnect: false, autoConnect: false,
path: "/api/v1/support/socket/",
}); });
socket.on("users", (data: SupportUserStatus[]) => { socket.on("users", (data: SupportUserStatus[]) => {
@ -63,6 +65,9 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
v.unreadCount++; v.unreadCount++;
v.lastMessage = r.content; v.lastMessage = r.content;
} }
if (currentIssue.value === r.issueId) {
v.unreadCount = 0;
}
return v; return v;
}); });
} }
@ -102,8 +107,10 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
function sendMessage(content: string, to: string) { function sendMessage(content: string, to: string) {
// console.log(content); // console.log(content);
// console.log(to); // console.log(to);
socket.emit("message", { to, content }); if (content.trim() !== "") {
scrollToEnd(); socket.emit("message", { to, content });
scrollToEnd();
}
} }
async function fetchMessageStatus(issueId: string) { async function fetchMessageStatus(issueId: string) {
@ -220,6 +227,7 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
messageStatus, messageStatus,
userStatus, userStatus,
currentIssue, currentIssue,
currentIssueDate,
currentTitle, currentTitle,
fetchIssue, fetchIssue,
fetchIssueCategory, fetchIssueCategory,
@ -228,7 +236,7 @@ export const useSupportStore = defineStore("supportServiceStore", () => {
sendMessage, sendMessage,
newIssue, newIssue,
searchIssue, searchIssue,
isOpen, openChat,
items, items,
socket, socket,
scrollContainer, scrollContainer,

View file

@ -7,6 +7,7 @@ import moment from "moment";
const store = useSupportStore(); const store = useSupportStore();
const searchData = ref<string>(""); const searchData = ref<string>("");
const newIssueModal = ref<boolean>(false);
function dateIssue(timestamp: string): string { function dateIssue(timestamp: string): string {
const parsedTimestamp = moment(timestamp); const parsedTimestamp = moment(timestamp);
@ -44,14 +45,16 @@ watch(searchData, () => {
<template> <template>
<div class="col-12 row justify-center"> <div class="col-12 row justify-center">
<div class="col-xs-12 col-sm-12 col-md-11"> <div class="col-xs-12 col-sm-12 col-md-11">
<div class="toptitle text-white col-12 row items-center">ถาม - ตอบ</div> <div class="toptitle text-white col-12 row items-center">
แจงปญหาการใชงาน
</div>
<div class="col-12"> <div class="col-12">
<q-card bordered> <q-card bordered>
<div class="row"> <div class="row" style="flex-wrap: nowrap">
<!-- Left --> <!-- Left -->
<div v-if="!store.isOpen" class="col-xs-12 col-sm-4 col-md-3"> <div v-if="!store.openChat || $q.screen.gt.xs" class="col-xs-12 col-sm-4 col-md-4">
<!-- New --> <!-- New -->
<div class="q-pt-md bg-white"> <div class="q-pt-md">
<div class="q-px-md q-pb-md"> <div class="q-px-md q-pb-md">
<q-input <q-input
rounded rounded
@ -63,35 +66,29 @@ watch(searchData, () => {
id="inputSearch" id="inputSearch"
@keydown.enter.prevent="store.searchIssue(searchData)" @keydown.enter.prevent="store.searchIssue(searchData)"
> >
<template v-slot:prepend <template v-slot:prepend>
><q-icon name="mdi-magnify" class="pointer" /> <q-icon name="mdi-magnify" class="pointer" />
</template> </template>
</q-input> </q-input>
</div> </div>
<q-separator inset /> <q-separator inset />
<div> <div>
<new-issue /> <div
@click="newIssueModal = true"
class="col-10 row items-center q-py-sm q-px-md new"
>
<div class="no-active-avatar">
<div class="new-avatar">
<q-icon name="mdi-plus" size="24px" color="primary" />
</div>
</div>
<div class="col column q-ml-md">
<span class="col text-grey"> แจงปญหา </span>
</div>
</div>
</div> </div>
<q-separator inset /> <q-separator inset />
<!-- No Issue -->
<div
v-if="store.issue?.result.length === 0"
class="col-10 row items-center q-pa-md new"
>
<div class="noactive-avatar">
<q-avatar
color="transparent"
text-color="white"
size="40px"
>
<q-icon name="mdi-close" size="24px" color="negative" />
</q-avatar>
</div>
<div class="col column q-ml-md">
<span class="col text-grey"> ไมพบเรองปรกษา </span>
</div>
</div>
</div> </div>
<!-- List Issue --> <!-- List Issue -->
@ -105,9 +102,10 @@ watch(searchData, () => {
<div <div
@click=" @click="
async () => { async () => {
$q.screen.gt.xs ? '' : (store.isOpen = true); $q.screen.gt.xs ? '' : (store.openChat = true);
store.currentIssue = item.id; store.currentIssue = item.id;
store.currentTitle = item.title; store.currentTitle = item.title;
store.currentIssueDate = item.createdAt
store.issue store.issue
? (store.issue.result = store.issue.result.map( ? (store.issue.result = store.issue.result.map(
(v) => { (v) => {
@ -123,10 +121,10 @@ watch(searchData, () => {
} }
" "
:class="{ active: store.currentIssue === item.id }" :class="{ active: store.currentIssue === item.id }"
class="noactive row q-py-md justify-between items-center q-px-md" class="no-active row q-py-md justify-between items-center q-px-md"
> >
<div class="col-9 row items-center"> <div class="col-9 row items-center">
<div class="noactive-avatar"> <div class="no-active-avatar">
<q-avatar color="grey-2" text-color="white" size="40px"> <q-avatar color="grey-2" text-color="white" size="40px">
<q-icon <q-icon
:name="store.icon" :name="store.icon"
@ -187,18 +185,22 @@ watch(searchData, () => {
<q-separator vertical /> <q-separator vertical />
<!-- Right --> <!-- Right -->
<div v-if="$q.screen.gt.xs || store.isOpen" class="col-grow"> <div v-if="$q.screen.gt.xs || store.openChat" style="width: 100%">
<form-chat v-if="store.currentIssue.length !== 0" /> <form-chat v-if="store.currentIssue.length !== 0" />
</div> </div>
</div> </div>
</q-card> </q-card>
</div> </div>
</div> </div>
<new-issue
:modal="newIssueModal"
:click-close="() => (newIssueModal = false)"
/>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.noactive { .no-active {
transition: 0.1s ease all; transition: 0.1s ease all;
opacity: 1; opacity: 1;
cursor: pointer; cursor: pointer;
@ -207,7 +209,7 @@ watch(searchData, () => {
&:hover { &:hover {
background: #ebf9f7; background: #ebf9f7;
.noactive-avatar { .no-active-avatar {
transition: 0.1s ease all; transition: 0.1s ease all;
border-radius: 50%; border-radius: 50%;
border: 1px solid $primary; border: 1px solid $primary;
@ -215,7 +217,7 @@ watch(searchData, () => {
} }
} }
.noactive-avatar { .no-active-avatar {
border-radius: 50%; border-radius: 50%;
border: 1px solid #f5f4f4; border: 1px solid #f5f4f4;
} }
@ -224,13 +226,34 @@ watch(searchData, () => {
background: #ebf9f7; background: #ebf9f7;
border-left: 5px solid $primary; border-left: 5px solid $primary;
& { & {
.noactive-avatar { .no-active-avatar {
border-radius: 50%; border-radius: 50%;
border: 1px solid $primary; border: 1px solid $primary;
} }
} }
} }
.new {
transition: 0.1s ease all;
opacity: 1;
cursor: pointer;
border-left: 5px solid transparent;
&:hover {
background: #ebf9f7;
}
}
.new-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px dashed #9e9e9e;
}
.line-ellipsis { .line-ellipsis {
display: block; display: block;
white-space: nowrap; white-space: nowrap;