From 6054289ebe70ade6a245f6d40e48a84f9e6e0560 Mon Sep 17 00:00:00 2001 From: Tomas <tomasysh@gmail.com> Date: 星期一, 31 七月 2023 14:49:02 +0800 Subject: [PATCH] Update: 更新諮詢方式相關 API 使用方式 --- PAMapp/pages/index.vue | 489 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 files changed, 453 insertions(+), 36 deletions(-) diff --git a/PAMapp/pages/index.vue b/PAMapp/pages/index.vue index 55baff7..d2dc803 100644 --- a/PAMapp/pages/index.vue +++ b/PAMapp/pages/index.vue @@ -2,33 +2,37 @@ <div> <Ui-Carousel></Ui-Carousel> <div class="page-container"> - <div class="mb-30"> + + <div> <h5 class="mdTxt">����憿批��</h5> <div class="mt-10 pam-reserveBtn--block"> <el-button class="reserveBtn recommendConsultant" - @click="routerPush('/recommendConsultant')"> + @click="navigateToRoute('/recommendConsultant')"> <p>������</p> </el-button> <el-button class="reserveBtn quickFilter" - @click="routerPush('/quickFilter')"> + @click="navigateToRoute('/quickFilter')"> <p>敹恍�祟�</p> </el-button> </div> </div> + <div class="pam-paragraph"> <el-row class="rowStyle"> <el-col :span="16"> <span class="mdTxt">���“���</span> - <span class="smTxt_bold amount">� {{consultantList.length}} 蝑�</span> + <span class="smTxt_bold amount">� {{ consultantList.length }} 蝑�</span> </el-col> <el-col :span="8" class="mdTxt readMore fix-chrome-click--issue" v-if="consultantList.length > 3" - @click.native="routerPush('/myConsultantList/consultantList')">���憭�</el-col> + @click.native="navigateToRoute('/myConsultantList/consultantList')">���憭�</el-col> </el-row> <ConsultantList class="mt-10" :agents="consultantList.slice(0, 3)"></ConsultantList> </div> + + <div class='pam-paragraph'> <div class="pam-recommend"> <h5 class="mdTxt">��靽憿批��</h5> @@ -39,49 +43,425 @@ <ConsultantSwiper :agents="recommendList"></ConsultantSwiper> </div> </div> + + <Ui-Dialog + :isVisible.sync="isShowAppointmentDialog" + :width="appointmentDialogWidth" + class="pam-myDemand-dialog pam-dialog-reserved" + > + <div v-if="appointmentDetail"> + <h5 class="subTitle text--center mb-30">������</h5> + <p class="smTxt">{{appointmentDetail.appointmentDate | formatDate}}</p> + <div class="reserved-info"> + <p>憪��{appointmentDetail.name}}</p> + <p>�閰梧�{appointmentDetail.phone}}</p> + <p>Email嚗{appointmentDetail.email}}</p> + <p>�批嚗{gender}}</p> + <p>撟湧翩嚗{appointmentDetail.age | toAgeLabel }}</p> + <p>�璆哨�{appointmentDetail.job}}</p> + <p>隢株岷�撘�{appointmentDetail.consultationMethod | toConsulType }}</p> + <p>��瘙�{ appointmentDetail.requirement ? appointmentDetail.requirement.split(',').join('��') : '--'}}</p> + <p + v-for="(item, index) in hopeContactTime" + :key="index" + >��蝯⊥�挾{{index + 1 | formatNumber}}嚗{ item | formatHopeContactTime }}</p> + <div v-if="appointmentDetail.satisfactionScore"> + <div class="mdTxt mt-10 mb-10">皛踵�漲</div> + <el-rate + :value="appointmentDetail.satisfactionScore" + class="pam-myDemand-dialog__rate" + disabled> + </el-rate> + </div> + </div> + + <div v-if="notScoreAppointmentYet" class="reserved-btn"> + <el-button type="primary" + @click.native="reviewsBtn = true">蝯虫�遛��漲閰��</el-button> + </div> + + <div v-if="appointmentDetail.communicateStatus === 'reserved'" class="text--center mt-10"> + <el-button @click="isCancelPopup = true">������</el-button> + <el-button @click="edit" type="primary">蝺刻摩</el-button> + </div> + </div> + </Ui-Dialog> + + <PopUpFrame + :isOpen.sync="isShowReAppointmentDialog" + @closePopUp="removeUrlQueryParameter('notContactAppointmentId')" + > + <div class="pam-dialog-review"> + <div class="mt-30 text--middle" v-if="agentInfo"> + 敺甇������<span class="text--bold">{{ consultantName }}</span>憿批�迤敹�葉嚗������蒂���隞“��� + </div> + + <el-row + type="flex" + class="mt-50" + justify="center"> + <el-button + type="primary" + @click="reAppointment">��������隞“���</el-button> + </el-row> + <el-row + type="flex" + class="mt-20" + justify="center"> + <el-button + class="outline_btn" + @click="cancelAppointment">������</el-button> + </el-row> + </div> + </PopUpFrame> + + <PopUpFrame + :isOpen.sync="isShowReviewDialog" + @closePopUp="removeUrlQueryParameter('appointmentId')" + > + <div class="mdTxt pam-dialog-review"> + 靽憿批�遛��漲 + <span class="hint">������</span> + <div class="mt-30 review-content" v-if="agentInfo"> + <UiAvatar :size="80" :agentNo="agentInfo.agentNo"></UiAvatar> + <div class="review-text">撠憿批�� + <span class="text--primary">{{agentInfo.name}}</span> + ��擃���蝯虫�嗾憿��嚗� + </div> + </div> + + <div class="review-score"> + <el-rate v-model="inputScore" class="pam-rate mt-30"></el-rate> + </div> + + <div class="review-btn"> + <el-button + type="primary" + :disabled="!inputScore" + @click="userReviewsConsultants">�</el-button> + </div> + </div> + </PopUpFrame> + + <div class="video-container" + v-if="isShowFilmPlayer" + style="position: fixed; bottom: 30px; right: 30px; z-index: 9999; display: flex; justify-content: flex-end;" + > + <iframe width=��780�� + height=��440�� + allowfullscreen + src="https://www.youtube.com/embed/655JnwbuRGA?autoplay=1&mute=1" + ></iframe> + <div class="close-btn" @click="closeVideo()">X</div> + </div> + </div> </template> <script lang="ts"> - import { Vue, Component, State, Action, Watch, namespace } from 'nuxt-property-decorator'; - import { Consultant } from '~/assets/ts/models/consultant.model'; +import {Action, Component, Mutation, namespace, State, Vue, Watch} from 'nuxt-property-decorator'; - const localStorage = namespace('localStorage'); +import appointmentService from '~/shared/services/appointment.service'; +import utilService, {AccessFroms} from '~/shared/services/utils.service'; +import reviewsService from '~/shared/services/reviews.service'; +import myConsultantService from '~/shared/services/my-consultant.service'; +import {Appointment, AppointmentClosedInfo} from '~/shared/models/appointment.model'; +import {Consultant} from '~/shared/models/consultant.model'; +import {UserReviewParams} from '~/shared/models/reviews.model'; +import {StrictQueryParams} from '~/shared/models/strict-query.model'; +import {AgentInfo} from '~/shared/models/agent-info.model'; +import {ContactStatus} from '~/shared/models/enum/contact-status'; +import {SatisfactionType} from '~/shared/models/enum/satisfaction-type'; + +const localStorage = namespace('localStorage'); + const roleStorage = namespace('localStorage'); + @Component({ layout: 'home' }) export default class MainComponent extends Vue { + + @State('recommendList') + recommendList!: Consultant[]; + + @State('myConsultantList') + myConsultantList!: Consultant[]; + + @roleStorage.Getter + isAdminLogin!: boolean; + + @roleStorage.Getter + isUserLogin!: boolean; + + @Action + storeRecommendList!: any; + + @Action + storeConsultantList!: any; + + @Mutation + setAccessSource!: (accessSource: AccessFroms) => void; + + @localStorage.Mutation + storageClearQuickFilter!: () => void; + + @localStorage.Mutation + storageClearRecommendConsultant!: () => void; + + @localStorage.Getter + currentSatisfactionIdFromMsg!: string; + + @localStorage.Getter + currentNotContactAppointmentIdFromMsg!: string; + + @localStorage.Mutation + storageClearSatisfactionIdFromMsg!: () => void; + + @localStorage.Mutation + storageClearNotContactAppointmentIdFromMsg!: () => void; + + @localStorage.Mutation + storageStrickQueryItem!: (strictQueryDto: StrictQueryParams) => void; + consultantList: Consultant[] = []; - @State('recommendList') recommendList!: Consultant[]; - @Action storeRecommendList!: any; + appointmentDialogWidth = ''; + inputScore = 0; + isShowAppointmentDialog = false; + isShowReAppointmentDialog = false; + isShowReviewDialog = false; + consultantName = ''; + contactStatus = ContactStatus; - @State('myConsultantList') myConsultantList!: Consultant[]; - @Action storeConsultantList!: any; + appointmentDetail: Appointment = { + age : '', + agentNo : '', + appointmentClosedInfo: {} as AppointmentClosedInfo, + appointmentDate : '', + appointmentMemoList: [], + appointmentNoticeLogs: [], + communicateStatus: this.contactStatus.PICKED, + consultantReadTime: '', + consultantViewTime: '', + contactTime : '', + contactType : '', + customerId : 0, + email : '', + gender : '', + hopeContactTime : '', + interviewRecordDTOs: [], + id : 0, + job : '', + lastModifiedDate : '', + name : '', + otherRequirement : '', + phone : '', + requirement : '', + satisfactionScore : 0, + consultationMethod : '' + }; - @localStorage.Mutation storageClearQuickFilter!: () => void; - @localStorage.Mutation storageClearRecommendConsultant!: () => void; + agentInfo: Consultant = { + agentNo : '', + name : '', + img : '', + expertise : [], + avgScore : 0, + contactStatus : '', + createTime : '', + updateTime : '', + customerViewTime : '', + role : '', + seniority : '', + appointments : [] + }; + + isShowFilmPlayer = true; + + ////////////////////////////////////////////////////////////////////// + + mounted() { + if (this.$route.query.from) { + const fromSource = this.$route.query.from as AccessFroms; + this.setAccessSource(fromSource); + utilService.insertAccessFrom(fromSource); + } + if (this.isAdminLogin) { + this.$router.push('/myAppointmentList/appointmentList'); + } else { + if (!this.recommendList?.length) { + this.storeRecommendList(); + } + + this.storeConsultantList(); + this.storageClearQuickFilter(); + this.storageClearRecommendConsultant(); + + if (this.isUserLogin) { + appointmentService.getNotContactAppointment().then((appointment) => { + if (appointment) { + this.$router.push({ query: { notContactAppointmentId: appointment.id + ''}}); + this.autoOpenAppointmentBy('askReAppointment', appointment.id); + } + }); + } + + } + + } + + destroyed() { + this.removeUrlQueryParameter(); + } + + ////////////////////////////////////////////////////////////////////// @Watch('myConsultantList') onMyConsultantListChange() { this.consultantList = (this.myConsultantList || []) - .filter(item => item.contactStatus !== 'contacted') .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)})) - .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate) - } + .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate); - mounted() { - if (!this.recommendList?.length) { - this.storeRecommendList(); + if (this.currentNotContactAppointmentIdFromMsg) { + this.autoOpenAppointmentBy('askReAppointment', +this.currentNotContactAppointmentIdFromMsg); + return; } - this.storeConsultantList(); - this.storageClearQuickFilter(); - this.storageClearRecommendConsultant(); + if (this.currentSatisfactionIdFromMsg) { + this.autoOpenAppointmentBy('inviteReviewConsultant',+this.currentSatisfactionIdFromMsg); + this.storageClearSatisfactionIdFromMsg(); + return; + } } - routerPush(path: string) { + private autoOpenAppointmentBy(reason: string, targetAppointmentId: number): void { + const setAgentInfo = new Promise((resolve, reject) => { + this.agentInfo = this.myConsultantList.filter(item => { + const appointmentIndex = item.appointments?.findIndex(i => i.id === targetAppointmentId); + return appointmentIndex !== undefined && appointmentIndex > -1; + })[0]; + if (this.agentInfo) { + myConsultantService.getConsultantDetail(this.agentInfo.agentNo).then((res) => resolve(res)); + } + }); + + const setAppointment = new Promise((resolve, reject) => { + appointmentService.getAppointmentDetail(targetAppointmentId).then((res) => resolve(res)); + }); + + Promise.all([setAgentInfo, setAppointment]).then((values) => { + const agentInfo = values[0] as AgentInfo; + const appointmentInfo = values[1] as Appointment; + this.consultantName = agentInfo.name; + this.appointmentDetail = appointmentInfo; + this.appointmentDialogWidth = utilService.isMobileDevice() ? '80%' : ''; + this.isShowAppointmentDialog = true; + switch (reason) { + case 'inviteReviewConsultant': + if (this.notScoreAppointmentYet) { + setTimeout(() => { + this.isShowReviewDialog = true; + }, 500); + } + break; + case 'askReAppointment': + setTimeout(() => { + this.isShowReAppointmentDialog = true; + }, 500); + break; + } + }); + + } + + ////////////////////////////////////////////////////////////////////// + + navigateToRoute(path: string): void { this.$router.push(path); + } + + edit() { + this.isShowAppointmentDialog = false; + this.$router.push({path: `/questionnaire/${this.agentInfo.agentNo}`, query: {'edit': 'true'}}); + } + + reAppointment(): void { + appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => { + const requirements = this.appointmentDetail && this.appointmentDetail.requirement + ? this.appointmentDetail.requirement.split(',') + : []; + this.storeConsultantList(); + this.storageStrickQueryItem({ requirements: requirements }); + this.storageClearNotContactAppointmentIdFromMsg(); + this.removeUrlQueryParameter('notContactAppointmentId'); + this.$router.push('/recommendConsultant'); + }); + } + + cancelAppointment(): void { + appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => { + this.storeConsultantList(); + this.storageClearNotContactAppointmentIdFromMsg(); + this.removeUrlQueryParameter('notContactAppointmentId'); + this.isShowReAppointmentDialog = false; + this.isShowAppointmentDialog = false; + this.$router.push('/'); + }); + } + + userReviewsConsultants() { + const reviewParams: UserReviewParams = { + appointmentId: this.appointmentDetail.id, + score: this.inputScore, + type: SatisfactionType.APPOINTMENT + } + this.appointmentDetail.satisfactionScore = this.inputScore; + + reviewsService.userReviewsConsultants(reviewParams).then((res) => { + this.isShowReviewDialog = false; + }); + } + + removeUrlQueryParameter(targetKey?: string): void { + // NOTE: ���摰�� query parameter [Tomas, 2022/1/24 11:36] + // [REF] How to remove a parameter from this.$router.query Nuxt.js? https://reurl.cc/X45aMD + let newRouteQuery = {}; + if (targetKey) { + Object.keys(this.$route.query).forEach((key) => { + if (key !== targetKey) { + newRouteQuery[key] = this.$route.query[key] + } + }) + } + this.$router.push(newRouteQuery); + this.storageClearSatisfactionIdFromMsg(); + this.storageClearNotContactAppointmentIdFromMsg(); + } + + closeVideo() { + this.isShowFilmPlayer = false; + } + + /////////////////////////////////////////////////////////////////////////////// + + get gender(): string { + if (this.appointmentDetail.gender) { + return this.appointmentDetail.gender === 'male' ? '���' : '憟單��'; + } + return '' + } + + get hopeContactTime(): string[] { + const contactList = this.appointmentDetail.hopeContactTime + .split("'").map((item: any) => item.slice(0, item.length)); + return contactList.filter((item: any) => !!item && item !== ",") + } + + get notScoreAppointmentYet(): boolean { + if (this.appointmentDetail.communicateStatus === 'closed' || this.appointmentDetail.communicateStatus === 'done') { + return !this.appointmentDetail.satisfactionScore; + }; + return false; } } @@ -94,19 +474,19 @@ } .reserveBtn.el-button--default { - width: 100%; - height: 110px; - border-radius: 10px; - margin-bottom: 15px; - font-size: 32px; - font-weight: 700; + width : 100%; + height : 110px; + border-radius : 10px; + margin-bottom : 15px; + font-size : 32px; + font-weight : 700; background-position: right; - background-size: cover; - color: #68737A; - text-align: left; - background-repeat: no-repeat; - box-shadow: 0px 0px 6px #a79b9b29; - border-width: 0; + background-size : cover; + color : #68737A; + text-align : left; + background-repeat : no-repeat; + box-shadow : 0px 0px 6px #a79b9b29; + border-width : 0; p { text-shadow: 1px 1px 5px $PRIMARY_WHITE; @@ -141,6 +521,7 @@ align-items: center; } + @media (min-width: 576px) and (max-width: 767px) { .quickFilter.el-button--default { background-image: url('~/assets/images/quickFilter/banner_web.svg'); @@ -157,6 +538,15 @@ margin: 0 auto; } + .landing-container { + border: 2px solid blue; + border-radius: 10px; + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + } + .pam-reserveBtn--block { display: flex; justify-content: space-between; @@ -167,4 +557,31 @@ } } + /* 蝣箔�� .video-container ��撠�/蝯����摰祝摨� */ + .video-container { + position: relative; + width: 600px; /* �����蔣��偕撖貉矽� */ + } + + /* �����見撘� */ + .close-btn { + position: absolute; + top: -10px; + right: -10px; + cursor: pointer; + font-size: 20px; + color: #fff; + background-color: #000; + width: 30px; + height: 30px; + text-align: center; + line-height: 30px; + border-radius: 50%; + opacity: 0.7; + } + + .close-btn:hover { + opacity: 1; + } + </style> -- Gitblit v1.8.0