PAMapp/assets/scss/utilities/_utilities.scss
@@ -5,6 +5,10 @@ margin-bottom: 50px; } .mt-50 { margin-top: 50px; } .mt-30 { margin-top: 30px; } @@ -21,6 +25,7 @@ margin-bottom: 20px; } .mt-10 { margin-top: 10px; } PAMapp/assets/scss/vendors/elementUI/_rate.scss
@@ -45,6 +45,7 @@ display: flex; justify-content: center; margin-top: 10px; @extend .fix-chrome-click--issue; .el-rate__item { .el-rate__icon { font-size: 30px; PAMapp/assets/scss/vendors/elementUI/_select.scss
@@ -31,6 +31,9 @@ line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; &:before { content: "\e910"; } } } PAMapp/components/BackActionBar.vue
@@ -74,9 +74,6 @@ case 'accountSetting': featureLabel = 'å人帳èè¨å®'; break; case 'appointmentAgenda': featureLabel = 'å³å°ç´è¨ªæç¨'; break; case 'consultantAccountSetting': featureLabel = 'æ¥ç帳èè³è¨'; break; PAMapp/components/Client/ClientCard.vue
@@ -213,6 +213,9 @@ @appointmentStore.Action getAppointmentDetail!: (appointmentId: number) => Promise<Appointment>; @appointmentStore.Action updateAppointmentDetail!: (id: number) => Appointment; @appointmentStore.Getter appointmentProgress!: ContactStatus; @@ -258,7 +261,10 @@ viewAppointmentDetail(): void { this.getAppointmentDetail(this.client.id).then((_) => { this.readAppointment(); const unread = !this.client.consultantReadTime; if (unread) { this.readAppointment(); } this.$router.push(`/appointment/${this.client.id}`); }); } @@ -277,7 +283,6 @@ reviewsService.sendSatisfactionToClient(this.client.id).then(res => { this.isShowInviteReviewDialog = true ; }) } openDetail() { @@ -302,14 +307,12 @@ } private readAppointment(): void { const unread = !this.client.consultantReadTime; if (unread) { appointmentService.recordRead(this.client.id).then((_) => { const updatedClient = {...this.client}; updatedClient.consultantReadTime = new Date().toString(); this.updateMyAppointmentList(updatedClient); }); }; appointmentService.recordRead(this.client.id).then((_) => { const updatedClient = {...this.client}; updatedClient.consultantReadTime = new Date().toString(); this.updateMyAppointmentList(updatedClient); this.updateAppointmentDetail(this.client.id); }); } private clearAppointmentIdFromMsg() { PAMapp/components/Consultant/ConsultantCard.vue
@@ -31,7 +31,7 @@ <div class="delete" v-if="showRemoveBtn" @click="removeAgent" @click="isRemoveAgentPopup = true" >ç§»é¤</div> <div v-if="notScoreAppointmentYet" @@ -125,10 +125,18 @@ </div> </PopUpFrame> <PopUpFrame :isOpen.sync="isConfirmPopup"> <div class="text--center mdTxt">å·²æååæ¶æ¤çé ç´</div> <PopUpFrame :isOpen.sync="isConfirmPopup"> <div class="text--center mdTxt">å·²æå{{confirmTxt}}</div> <div class="text--center mt-30"> <el-button @click="isConfirmPopup = false" type="primary">確å®</el-button> </div> </PopUpFrame> <PopUpFrame :isOpen.sync="isRemoveAgentPopup"> <div class="text--center mdTxt">æ¯å¦ç§»é¤é¡§å <span class="text--primary">{{agentInfo.name}}</span>ï¼</div> <div class="text--center mt-30"> <el-button @click="isRemoveAgentPopup = false">å¦</el-button> <el-button @click="removeAgent" type="primary">æ¯</el-button> </div> </PopUpFrame> </div> @@ -181,6 +189,8 @@ isCancelPopup = false; hideReviews = hideReviews; isConfirmPopup = false; isRemoveAgentPopup = false; confirmTxt = ''; appointmentDetail: any = { age : '', @@ -339,6 +349,12 @@ removeAgent() { this.removeFromMyConsultantList(this.agentInfo.agentNo).then((removeOk) => { this.isRemoveAgentPopup = false; setTimeout(() => { this.confirmTxt = 'ç§»é¤é¡§å'; this.isConfirmPopup = true; }, 300); }); } @@ -370,7 +386,9 @@ this.isVisibleDialog = false; this.isCancelPopup = false; setTimeout(() => { this.confirmTxt = 'åæ¶æ¤çé ç´'; this.isConfirmPopup = true; }, 300); }); } @@ -422,6 +440,7 @@ } .delete { display: inline-block; color: $PRIMARY_RED; font-size: 14px; font-weight: bold; PAMapp/components/DateTimePicker.vue
@@ -5,12 +5,14 @@ <UiDatePicker @changeDate="changeDateTime($event, 'date')" :isPastDateDisabled="isPastDateDisabled" :isFutureDateDisabled="isFutureDateDisabled" :defaultValue="defaultValue" ></UiDatePicker> <UiTimePicker @changeTime="changeDateTime($event, 'time')" :defaultValue="defaultValue" :isPastDateDisabled="isPastDateDisabled" :isFutureDateDisabled="isFutureDateDisabled" :changeDate="changeDate" ></UiTimePicker> </div> @@ -30,6 +32,9 @@ @Prop() isPastDateDisabled!: boolean; @Prop() isFutureDateDisabled!: boolean; @Emit('changeDateTime') changeDateTime(event, type) { if (type === 'date') { PAMapp/components/Interview/interviewNotification.vue
Àɮפw§R°£ PAMapp/components/Ui/UiDatePicker.vue
@@ -5,6 +5,7 @@ v-model="dateValue" :clearable="false" type="date" :editable="false" format="yyyy/MM/dd" placeholder="é¸ææ¥æ" prefix-icon="icon-down down-icon" @@ -27,6 +28,9 @@ @Prop({default: false}) isPastDateDisabled!: boolean; @Prop({default: false}) isFutureDateDisabled!: boolean; @Emit('changeDate') changeDate() { return this.dateValue; @@ -41,16 +45,26 @@ } get pickerOptions() { const date = new Date(); const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; if (this.isPastDateDisabled) { return { disabledDate(time: Date) { const date = new Date(); const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}` const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`; return new Date(pickedDate).getTime() < new Date(currentDate).getTime(); } } } if (this.isFutureDateDisabled) { return { disabledDate(time: Date) { const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`; return new Date(pickedDate).getTime() > new Date(currentDate).getTime(); } } } } } PAMapp/components/Ui/UiTimePicker.vue
@@ -4,6 +4,7 @@ popper-class="pam-time-popper" v-model="timeValue" :clearable="false" :editable="false" :picker-options="pickerOptions" placeholder="鏿æé" prefix-icon="icon-down down-icon" @@ -29,6 +30,9 @@ @Prop() isPastDateDisabled!: boolean; @Prop() isFutureDateDisabled!: boolean; /////////////////////////////////////////////////////////////////////// @Emit('changeTime') @@ -49,25 +53,40 @@ get pickerOptions() { let minTime = ''; let maxTime = ''; const currentDate = new Date(); if (this.isPastDateDisabled && this.changeDate && this.isPickedToday(currentDate)) { minTime = this.formatTimeString(currentDate); this.isPickedDisableTime(currentDate, minTime); if (this.changeDate && this.isPickedToday(currentDate)) { if (this.isPastDateDisabled) { minTime = this.formatTimeString(currentDate); this.isPickedDisableTime(currentDate, minTime); } if (this.isFutureDateDisabled) { maxTime = this.formatTimeString(currentDate); this.isPickedDisableTime(currentDate, maxTime); } } return { start: '09:00', step: '00:15', end: '21:00', minTime: minTime minTime: minTime, maxTime: maxTime } } private isPickedDisableTime(currentDate: Date, minTime: string) { const currentTime = this.getTimeValue(currentDate, minTime); private isPickedDisableTime(currentDate: Date, minMaxTime: string) { const currentTime = this.getTimeValue(currentDate, minMaxTime); const pickedTime = this.getTimeValue(currentDate, this.timeValue); if (pickedTime < currentTime) { if (this.isPastDateDisabled && pickedTime < currentTime) { this.timeValue = ''; this.changeTime(); } if (this.isFutureDateDisabled && currentTime < pickedTime) { this.timeValue = ''; this.changeTime(); } PAMapp/middleware/getUrlQuery.ts
@@ -3,7 +3,15 @@ const getUrlQuery: Middleware = (context) => { const currentRouteName = context.route.name; const satisfactionIdFromMsg = context.route.query.appointmentId; const queryNotContactAppointmentId = context.route.query.notContactAppointmentId; const isUserLogin = context.store.getters['localStorage/isUserLogin']; if (currentRouteName === 'index' && queryNotContactAppointmentId) { context.store.commit('localStorage/storageNotContactAppointmentIdFromMsg', queryNotContactAppointmentId); if (!isUserLogin) { context.redirect('/login'); } } if (currentRouteName === 'index' && satisfactionIdFromMsg) { context.store.commit('localStorage/storageSatisfactionIdFromMsg', satisfactionIdFromMsg); @@ -11,6 +19,8 @@ context.redirect('/login'); } } } export default getUrlQuery export default getUrlQuery PAMapp/pages/appointment/_appointmentId/close/index.vue
@@ -46,6 +46,7 @@ <UiField label="é²ä»¶æé" :labelSize="20" class="required"> <DateTimePicker :defaultValue="appointmentCloseInfo.policyEntryDate" :isFutureDateDisabled="true" @changeDateTime="appointmentCloseDate = $event"></DateTimePicker> </UiField> </el-row> PAMapp/pages/index.vue
@@ -41,10 +41,9 @@ </div> <Ui-Dialog :isVisible.sync="isVisibleDialog" :width="width" :isVisible.sync="isShowAppointmentDialog" :width="appointmentDialogWidth" class="pam-myDemand-dialog pam-dialog-reserved" @closeDialog="clearSatisfactionId" > <div v-if="appointmentDetail"> <h5 class="subTitle text--center mb-30">é ç´æå</h5> @@ -71,16 +70,44 @@ </div> </div> <div v-if="!appointmentDetail.satisfactionScore" class="reserved-btn"> <div v-if="notScoreAppointmentYet" class="reserved-btn"> <el-button type="primary" @click.native="reviewsBtn = true">çµ¦äºæ»¿æåº¦è©å</el-button> @click.native="isShowReviewDialog = true">çµ¦äºæ»¿æåº¦è©å</el-button> </div> </div> </Ui-Dialog> <PopUpFrame :isOpen.sync="reviewsBtn" @closePopUp="clearSatisfactionId" :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"> ä¿éªé¡§å滿æåº¦ @@ -110,11 +137,16 @@ <script lang="ts"> import { Vue, Component, State, Action, Watch, namespace } from 'nuxt-property-decorator'; import { Appointment, AppointmentClosedInfo } from '~/shared/models/appointment.model'; import { Consultant } from '~/shared/models/consultant.model'; import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model'; import { ContactStatus } from '~/shared/models/enum/contact-status'; import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model'; import { StrictQueryParams } from '~/shared/models/strict-query.model'; import appointmentService from '~/shared/services/appointment.service'; import reviewsService from '~/shared/services/reviews.service'; import reviewsService from '~/shared/services/reviews.service'; import UtilsService from '~/shared/services/utils.service'; import myConsultantService from '~/shared/services/my-consultant.service'; import { AgentInfo } from '~/shared/models/agent-info.model'; const localStorage = namespace('localStorage'); const roleStorage = namespace('localStorage'); @@ -136,7 +168,8 @@ @Action storeRecommendList!: any; @Action storeConsultantList!: any; @Action storeConsultantList!: any; @localStorage.Mutation storageClearQuickFilter!: () => void; @@ -147,37 +180,55 @@ @localStorage.Getter currentSatisfactionIdFromMsg!: string; @localStorage.Getter currentNotContactAppointmentIdFromMsg!: string; @localStorage.Mutation storageClearSatisfactionIdFromMsg!: () => void; @localStorage.Mutation storageClearNotContactAppointmentIdFromMsg!: () => void; @localStorage.Mutation storageStrickQueryItem!: (strictQueryDto: StrictQueryParams) => void; consultantList: Consultant[] = []; appointmentDetail: any = { age : '', agentNo : '', appointmentDate : '', communicateStatus : '', consultantReadTime: null, consultantViewTime: null, contactTime : '', contactType : '', customerId : 0, email : '', gender : '', hopeContactTime : "", id : 0, job : "", lastModifiedDate : '', name : '', otherRequirement : null, phone : "", requirement : '', satisfactionScore : 0, appointmentDialogWidth = ''; inputScore = 0; isShowAppointmentDialog = false; isShowReAppointmentDialog = false; isShowReviewDialog = false; consultantName = ''; contactStatus = ContactStatus; 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, }; isVisibleDialog = false; width = ''; reviewsBtn = false; inputScore = 0; agentInfo: Consultant = { agentNo : '', name : '', @@ -211,7 +262,7 @@ } destroyed() { this.clearSatisfactionId(); this.removeUrlQueryParameter(); } ////////////////////////////////////////////////////////////////////// @@ -223,36 +274,81 @@ .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)})) .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate); if (this.currentSatisfactionIdFromMsg) { this.agentInfo = this.myConsultantList.filter(item => { const satisfactionIdIndex = item.appointments?.findIndex(i => i.id === +this.currentSatisfactionIdFromMsg); return satisfactionIdIndex !== undefined && satisfactionIdIndex > -1; })[0]; if (this.agentInfo) { this.openAppointmentInfo(); } if (this.currentNotContactAppointmentIdFromMsg) { this.autoOpenAppointmentBy('askReAppointment', +this.currentNotContactAppointmentIdFromMsg); return; } if (this.currentSatisfactionIdFromMsg) { this.autoOpenAppointmentBy('inviteReviewConsultant',+this.currentSatisfactionIdFromMsg); this.storageClearSatisfactionIdFromMsg(); return; } } private openAppointmentInfo() { appointmentService.getAppointmentDetail(+this.currentSatisfactionIdFromMsg).then(res => { this.appointmentDetail = res; this.width = UtilsService.isMobileDevice() ? '80%' : ''; this.isVisibleDialog = true; if (!this.appointmentDetail.satisfactionScore) { setTimeout(() => { this.reviewsBtn = true; }, 500) } 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 = UtilsService.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); } reAppointment(): void { appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => { const requirements = this.appointmentDetail.requirement.split(','); console.log('requirements', requirements) this.storeConsultantList(); this.storageStrickQueryItem({ requirements: requirements }); this.storageClearNotContactAppointmentIdFromMsg(); this.$router.push('/recommendConsultant'); }); } cancelAppointment(): void { appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => { this.storeConsultantList(); this.storageClearNotContactAppointmentIdFromMsg(); this.$router.push(''); }); } userReviewsConsultants() { @@ -263,14 +359,22 @@ this.appointmentDetail.satisfactionScore = this.inputScore; reviewsService.userReviewsConsultants(reviewParams).then((res) => { this.reviewsBtn = false; this.isShowReviewDialog = false; }); } clearSatisfactionId() { console.log('close'); this.$router.push({query: {}}); this.storageClearSatisfactionIdFromMsg(); 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); } /////////////////////////////////////////////////////////////////////////////// @@ -288,6 +392,13 @@ 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; } } </script> PAMapp/pages/myAppointmentList.vue
@@ -1,7 +1,6 @@ <template> <div> <div class="pam-myAppointment-banner"></div> <InterviewNotification></InterviewNotification> <div class="pam-container"> <div class="pam-cus-tabs mb-10"> <div PAMapp/pages/notification/index.vue
@@ -1,8 +1,9 @@ <template> <div> <div class="text--right mb-10" @click="showNotificationHint = true"> <!-- TODO: å ¨é¨å·²è®/å ¨é¨åªé¤ åè½æªå®æä¸é éæ±å¾ ç¢ºèª å é±è --> <!-- <div class="text--right mb-10" @click="showNotificationHint = true"> <i class="satisfaction-icon icon-edit"></i> </div> </div> --> <div v-if="isUserLogin && unReviewLogList.length" class="satisfaction-banner my-10 cursor--pointer" @@ -18,7 +19,8 @@ align="middle" class="notification-card" > <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col> <!-- TODO: å ¨é¨å·²è®/å ¨é¨åªé¤ åè½æªå®æä¸é éæ±å¾ ç¢ºèª å é±è --> <!-- <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col> --> <el-col :span="18"> <p class="text">{{item.content}}</p> </el-col> PAMapp/pages/questionnaire/_agentNo.vue
@@ -24,8 +24,12 @@ <div class="ques-header__input-block"> <span>Emailï¼</span> <input class="ques-header__input" :class="{ 'is-invalid': !emailValid}" placeholder="è«è¼¸å ¥" v-model="myRequest.email"> </div> <div class="error mt-5 mb-5" style="margin-left:65px"> <span v-show="!emailValid">Emailæ ¼å¼æèª¤</span> </div> </div> </div> @@ -117,19 +121,24 @@ <PopUpFrame :isOpen.sync="sendReserve" @update:isOpen="closeReservePopUp"> <div class="mdTxt mt-30 sendReserve-txt">é ç´æåï¼</div> <div class="mdTxt sendReserve-txt mb-30">æ¨é ç´çé¡§åæåéèæ¨è¯çµ¡ï¼</div> <div class="pam-app-review mb-10"> <!-- TODO: æªä¸²æ¥ api, é±è平尿»¿æåº¦ --> <!-- <div class="pam-app-review mb-10"> <div class="mdTxt mb-10">å°æ¼ <span class="mdTxt text--primary text--bold ">æååªå</span> å¹³å°çæ´é«æåï¼ </div> <div class="mdTxt">æ¨çµ¦äºå¹¾é¡æè©å¹ï¼</div> </div> <el-rate v-model="score" class="pam-satisfaction-rate fix-chrome-click--issue"></el-rate> <el-rate v-model="score" class="pam-satisfaction-rate fix-chrome-click--issue"></el-rate> --> <div class="text--center mdTxt"> <el-button @click="closeReservePopUp">ç¥é</el-button> <!-- <el-button @click="closeReservePopUp">ç¥é</el-button> <el-button type="primary" @click="closeReservePopUp"> éåº </el-button> --> <el-button type="primary" @click="closeReservePopUp"> æç¥éäº </el-button> </div> </PopUpFrame> @@ -466,6 +475,11 @@ : true; } get emailValid() { const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; return this.myRequest.email ? rule.test(this.myRequest.email) : true; } get userInfo(): RegisterInfo { const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!); return initUserInfo; @@ -473,7 +487,7 @@ get isDisabledSubmitBtn(): boolean { return _.includes(this.myRequest.contactType,ContactType.PHONE) ? !this.isHopeContactTimeDone() ? !this.isHopeContactTimeDone() || !this.emailValid : !this.phoneValid; } PAMapp/pages/satisfactionList.vue
@@ -14,18 +14,32 @@ çæ´é«æåï¼æ¨çµ¦äºå¹¾é¡æè©å¹ï¼ </div> </div> <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"></el-rate> <el-rate v-model="item.satisfaction" class="pam-satisfaction-rate mt-10 fix-chrome-click--issue" @change="isBtnDisabled = false" ></el-rate> </div> <div class="text--center mt-30"> <el-button type="primary" :disabled="isBtnDisabled">éåº</el-button> <div class="text--center mt-30" v-if="mapUnReviewLogList.length"> <el-button type="primary" :disabled="isBtnDisabled" @click="sent">éåº</el-button> </div> </div> <PopUpFrame :isOpen.sync="showConfirmPopup" @closePopUp="closePopup"> <div class="text--center mdTxt">ç¼éæå</div> <div class="text--center mt-30"> <el-button @click="closePopup" type="primary">確å®</el-button> </div> </PopUpFrame> </div> </template> <script lang="ts"> import { Vue, Component, Action, State, Watch } from 'nuxt-property-decorator'; import { AppointmentLog } from '~/shared/models/appointment.model'; import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model'; import reviewsService from '~/shared/services/reviews.service'; @Component({ layout: 'home' @@ -35,7 +49,12 @@ @State unReviewLogList!: AppointmentLog[]; @Action storeMyAppointmentReviewLog!: () => void; mapUnReviewLogList: AppointmentReviewLog[] = []; showConfirmPopup = false; isBtnDisabled = true; /////////////////////////////////////////////////////// @@ -53,12 +72,26 @@ /////////////////////////////////////////////////////// get isBtnDisabled() { if (this.mapUnReviewLogList.length) { return this.mapUnReviewLogList.findIndex(item => item.satisfaction > 0) === -1; } return false; sent() { const reviewParams: UserReviewsConsultantsParams[] = this.mapUnReviewLogList .filter(item => item.satisfaction > 0) .map(item => { return { appointmentId: item.appointmentId, score: item.satisfaction } }) reviewsService.allUserReviewsConsultants(reviewParams).then((res) => { this.showConfirmPopup = true; }); } closePopup() { this.showConfirmPopup = false; this.storeMyAppointmentReviewLog(); } } interface AppointmentReviewLog extends AppointmentLog { PAMapp/shared/models/strict-query.model.ts
@@ -1,14 +1,14 @@ export interface StrictQueryParams { gender : string; avgScore : number; status : string; //phase 1 disable area : string; requirements : string[]; otherRequirement: string; seniority : string; popularTags : string[]; otherPopularTags: string; gender? : string; avgScore? : number; status? : string; //phase 1 disable area? : string; requirements? : string[]; otherRequirement?: string; seniority? : string; popularTags? : string[]; otherPopularTags?: string; } export interface AgentOfStrictQuery { PAMapp/shared/services/httpClient.ts
@@ -16,8 +16,11 @@ withCredentials: true }); let apiNumber = 0; http.interceptors.request.use( (config: AxiosRequestConfig) => { apiNumber += 1; loadingStart(); addHttpHeader(config); return config; @@ -26,11 +29,17 @@ http.interceptors.response.use( (response: AxiosResponse) => { loadingFinish(); apiNumber -= 1; if (apiNumber === 0) { loadingFinish(); } return response; }, (error: AxiosError) => { loadingFinish(); apiNumber -= 1; if (apiNumber === 0) { loadingFinish(); } showErrorMessageBox(error) return Promise.reject(error); } PAMapp/shared/services/reviews.service.ts
@@ -5,11 +5,17 @@ class ReviewsService { //客æ¶é²è¡æ»¿æåº¦è©å //客æ¶é²è¡æ»¿æåº¦è©å(å®ç) userReviewsConsultants(data: UserReviewsConsultantsParams) { return http.post('/satisfaction/score', data ); return http.post('/satisfaction/score', data); } // 客æ¶é²è¡æ»¿æåº¦(å¤ç) allUserReviewsConsultants(data: UserReviewsConsultantsParams[]) { return http.post('/satisfaction/score/all', data); } //å徿æè©åç´é async getMyReviewLog(): Promise<AppointmentLog[]> { return http.get('/satisfaction/getMySatisfaction').then(res => res.data); PAMapp/store/localStorage.ts
@@ -1,6 +1,7 @@ import { Module, Mutation, VuexModule ,Action } from 'vuex-module-decorators'; import { Role } from '~/shared/models/enum/Role'; import { Selected } from '~/shared/models/quick-filter.model'; import { StrictQueryParams } from '~/shared/models/strict-query.model'; @Module export default class LocalStorage extends VuexModule { id_token = localStorage.getItem('id_token'); @@ -10,6 +11,7 @@ recommendConsultantItem = localStorage.getItem('recommendConsultantItem'); appointmentIdFromMsg = localStorage.getItem('appointmentIdFromMsg'); satisfactionIdFromMsg = localStorage.getItem('satisfactionIdFromMsg'); notContactAppointmentIdFromMsg = localStorage.getItem('notContactAppointmentIdFromMsg'); get idToken(): string|null { return this.id_token; @@ -41,6 +43,10 @@ get currentSatisfactionIdFromMsg(): string|null { return this.satisfactionIdFromMsg; } get currentNotContactAppointmentIdFromMsg(): string|null { return this.notContactAppointmentIdFromMsg; } @Mutation storageIdToken(token: string): void { @@ -78,6 +84,11 @@ this.satisfactionIdFromMsg = localStorage.getItem('satisfactionIdFromMsg'); } @Mutation storageNotContactAppointmentIdFromMsg(id: string) { localStorage.setItem('notContactAppointmentIdFromMsg', id); this.notContactAppointmentIdFromMsg = id; } @Mutation storageClear(): void { localStorage.removeItem('myRequests'); localStorage.removeItem('userInfo'); @@ -112,6 +123,16 @@ this.appointmentIdFromMsg = localStorage.getItem('satisfactionIdFromMsg'); } @Mutation storageClearNotContactAppointmentIdFromMsg() { localStorage.removeItem('notContactAppointmentIdFromMsg'); this.appointmentIdFromMsg = localStorage.getItem('notContactAppointmentIdFromMsg'); } @Mutation storageStrickQueryItem(queryItem: StrictQueryParams): void { localStorage.setItem('recommendConsultantItem', JSON.stringify(queryItem)); this.recommendConsultantItem = localStorage.getItem('recommendConsultantItem'); } @Action actionStorageClear(): void { this.context.commit("storageClear"); } pamapi/src/doc/sql/20220122_w.sql
@@ -2,5 +2,5 @@ id bigserial NOT NULL, appointment_id bigserial NOT NULL, send_time timestamp NULL, CONSTRAINT appointment_pending_notify_record_pk PRIMARY KEY (id) CONSTRAINT appointment_expiring_notify_record_pk PRIMARY KEY (id) ); pamapi/src/doc/sql/²bªÅ¾ãÓ¨t²Î¸ê®Æ(°£ÅU°Ý).sql
¤ñ¹ï·sÀÉ®× @@ -0,0 +1,12 @@ truncate public.appointment; truncate public.appointment_closed_info; truncate public.appointment_expiring_notify_record; truncate public.appointment_memo; truncate public.appointment_notice_log; truncate public.customer; truncate public.interview_record; truncate public.login_record; truncate public.otp_tmp; truncate public.personal_notification; truncate public.satisfaction; truncate public.customer_favorite_consultant; pamapi/src/doc/¹w¬ù³æ/«È¤á¨ú±o³Ì·s¹w¬ùªº¥¼³B²z¹w¬ù³æ.txt
@@ -4,7 +4,7 @@ ç°¡è¨åemailæä»¥è©²ç¶²åé²å ¥é¦é -> http://localhost:3000?notContactAppointmentId={ææ°ä¸çæªèçé ç´å®} response body: è¥ææå³200並給以ä¸è³æï¼è¥ç¡(æªæä»»ä½é¾ææªèçé ç´å®ï¼åæå404) response body: è¥ææå³200並給以ä¸è³æï¼è¥ç¡(æªæä»»ä½é¾ææªèçé ç´å®ï¼åæånull) { "id": 385, "phone": "0911223344", pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java
@@ -1,5 +1,6 @@ package com.pollex.pam.config; import com.pollex.pam.enums.SendEmailMsgMethod; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -19,7 +20,6 @@ private String eServiceLoginFunc; private String eServiceLoginSys; private String frontEndDomain; private boolean sendNotifyMsg; private SMS sms; private Email email; private String fileFolderPath; @@ -88,14 +88,6 @@ this.frontEndDomain = frontEndDomain; } public boolean isSendNotifyMsg() { return sendNotifyMsg; } public void setSendNotifyMsg(boolean sendNotifyMsg) { this.sendNotifyMsg = sendNotifyMsg; } public SMS getSms() { return sms; } @@ -118,6 +110,7 @@ private String sender; private String smsType; private String subject; private boolean sendNotifyMsg; public String getUrl() { return url; @@ -158,12 +151,22 @@ public void setSubject(String subject) { this.subject = subject; } public boolean isSendNotifyMsg() { return sendNotifyMsg; } public void setSendNotifyMsg(boolean sendNotifyMsg) { this.sendNotifyMsg = sendNotifyMsg; } } public static class Email { private String url; private String functionId; private String senderEmail; private boolean sendNotifyMsg; private SendEmailMsgMethod method; public String getUrl() { return url; @@ -188,6 +191,22 @@ public void setSenderEmail(String senderEmail) { this.senderEmail = senderEmail; } public boolean isSendNotifyMsg() { return sendNotifyMsg; } public void setSendNotifyMsg(boolean sendNotifyMsg) { this.sendNotifyMsg = sendNotifyMsg; } public SendEmailMsgMethod getMethod() { return method; } public void setMethod(SendEmailMsgMethod method) { this.method = method; } } public String getFileFolderPath() { return fileFolderPath; pamapi/src/main/java/com/pollex/pam/config/Constants.java
@@ -30,5 +30,7 @@ */ public static final int SEND_EXPIRING_NOTIFY_LIMIT = 1; public static final String SPRING_PROFILE_POLLEX_DEVELOPMENT = "pollex"; private Constants() {} } pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java
¤ñ¹ï·sÀÉ®× @@ -0,0 +1,6 @@ package com.pollex.pam.enums; public enum SendEmailMsgMethod { PAM_EMAIL_SERVICE, POLLEX_GMAIL } pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
@@ -200,7 +200,8 @@ public void sendAppointmentNotify(Appointment appointment) { Assert.notNull(appointment, "appointment entity cannot be null"); log.debug("is need send appointment notify msg? = {}", applicationProperties.isSendNotifyMsg()); log.debug("is need send appointment notify msg? sms = {}, email = {}", applicationProperties.getSms().isSendNotifyMsg(), applicationProperties.getEmail().isSendNotifyMsg()); log.debug("sending appointment notify, appointmentId = {}", appointment.getId()); sendAppointmentNotifyBySMS(appointment); pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
@@ -73,12 +73,17 @@ consultantWithPendingAppointments.forEach((agentNo, pendingAppointments) -> { int pendingAppointmentsSum = pendingAppointments.size(); Consultant consultant = consultantService.findByAgentNo(agentNo); String consultantPhoneNumber = consultant.getPhoneNumber(); String consultantEmail = consultant.getEmail(); Optional<String> optionalPhone = Optional.ofNullable(consultant.getPhoneNumber()).filter(StringUtils::hasText); Optional<String> optionalEmail = Optional.ofNullable(consultant.getEmail()).filter(StringUtils::hasText); String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum); sendMsgService.sendMsgBySMS(consultantPhoneNumber, String.format("æ¨æ%såé ç´å®æªé²è¡è¯ç¹«ï¼è«ç¡éèç", pendingAppointmentsSum)); sendMsgService.sendMsgByEmail(consultantEmail, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true); optionalPhone.ifPresent(phone -> { sendMsgService.sendMsgBySMS(phone, String.format("æ¨æ%såé ç´å®æªé²è¡è¯ç¹«ï¼è«ç¡éèç", pendingAppointmentsSum)); }); optionalEmail.ifPresent(email -> { sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true); }); }); log.info("Sending appointment pending notify to consultant finish"); @@ -94,7 +99,7 @@ .filter(appointment -> appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL) ) .filter(this::isAppointmentNotifyNotOnLimit) .filter(this::isAppointmentExpiringNotifyNotOnLimit) .collect(Collectors.toList()); allByCommunicateStatus.forEach(appointment -> { @@ -104,10 +109,10 @@ optionalPhone.ifPresent(phone -> sendMsgService.sendMsgBySMS(phone, String.format("徿±æï¼æ¨é ç´%s顧忣å¿ç¢ä¸ï¼è«æ¨åæ¶é ç´ä¸¦æ¹é¸å ¶ä»é¡§åï¼è«é»æç¶²åï¼%s" , consultant.getName(), getAppointmentUrl(appointment.getId()))) , consultant.getName(), getAppointmentExpiringNotifyUrl(appointment.getId()))) ); optionalEmail.ifPresent(email -> sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentUrl(appointment.getId())), true) sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentExpiringNotifyUrl(appointment.getId())), true) ); AppointmentExpiringNotifyRecord record = new AppointmentExpiringNotifyRecord(); @@ -121,7 +126,7 @@ } // todo é確èªè©²æé, otis todo=134497 @Scheduled(cron = "0 0 9 * * *") @Scheduled(cron = "0 30 8 * * *") public void sendNotFillSatisfactionToPersonalNotification() { Map<Long, List<Satisfaction>> customerNotFillSatisfactions = satisfactionService.getByStatus(SatisfactionStatusEnum.UNFILLED) .stream() @@ -132,14 +137,14 @@ ); } private boolean isAppointmentNotifyNotOnLimit(AppointmentCustomerView appointment) { private boolean isAppointmentExpiringNotifyNotOnLimit(AppointmentCustomerView appointment) { int sendNotifyToCustomerRecordSum = appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size(); return sendNotifyToCustomerRecordSum < Constants.SEND_EXPIRING_NOTIFY_LIMIT; } private String getAppointmentUrl(Long appointmentId) { private String getAppointmentExpiringNotifyUrl(Long appointmentId) { return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId; } pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java
@@ -2,7 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.pollex.pam.config.ApplicationProperties; import com.pollex.pam.config.ApplicationProperties.Email; import com.pollex.pam.config.ApplicationProperties.SMS; import com.pollex.pam.config.Constants; import com.pollex.pam.enums.SendEmailMsgMethod; import com.pollex.pam.repository.ConsultantRepository; import com.pollex.pam.service.dto.*; import com.pollex.pam.service.util.HttpRequestUtil; @@ -11,9 +14,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.thymeleaf.spring5.SpringTemplateEngine; import tech.jhipster.config.JHipsterConstants; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; @@ -38,13 +44,19 @@ @Autowired SpringTemplateEngine springTemplateEngine; @Autowired Environment environment; @Autowired MailService mailService; public SendSMSResponse sendMsgBySMS(String toMobile, String content) throws SendSMSFailException { if(!applicationProperties.isSendNotifyMsg()) { SMS smsProperties = applicationProperties.getSms(); if(!smsProperties.isSendNotifyMsg()) { // return getMockSMSResponse(); return null; } SMS smsProperties = applicationProperties.getSms(); return null; } SendSMSRequest sendSMSRequest = new SendSMSRequest(); sendSMSRequest.setpKey(UUID.randomUUID().toString()); @@ -94,7 +106,7 @@ public String sendMsgByEmail(String toAddress, String subject, String content, boolean htmlFormat, List<String> toCCAddress, List<String> attachments) throws SendEmailFailException { String fromAddress = applicationProperties.getEmail().getSenderEmail(); SendMailRequest sendMailRequest = new SendMailRequest(); sendMailRequest.setSendMailAddresses(Collections.singletonList(toAddress)); sendMailRequest.setFrom(fromAddress); @@ -109,25 +121,48 @@ } public String sendMsgByEmail(SendMailRequest sendMailRequest) throws SendEmailFailException{ if(!applicationProperties.isSendNotifyMsg()) { return null; } try { final Email emailProperties = applicationProperties.getEmail(); if(!emailProperties.isSendNotifyMsg()) { return null; } if(emailProperties.getMethod() == SendEmailMsgMethod.POLLEX_GMAIL) { return sendMsgByPollexGmail(sendMailRequest); } else if(emailProperties.getMethod() == SendEmailMsgMethod.PAM_EMAIL_SERVICE) { return sendMsgByPamEmailService(sendMailRequest); } return null; } private String sendMsgByPollexGmail(SendMailRequest sendMailRequest) { String subject = sendMailRequest.getSubject(); String content = sendMailRequest.getContent(); boolean isHtml = sendMailRequest.isHtmlFormat(); sendMailRequest.getSendMailAddresses().forEach(receiver -> mailService.sendEmail(receiver, subject, content, false, isHtml)); return null; } private String sendMsgByPamEmailService(SendMailRequest sendMailRequest) { final Email emailProperties = applicationProperties.getEmail(); try { ResponseEntity<String> responseEntity = HttpRequestUtil.postWithJson( applicationProperties.getEmail().getUrl(), sendMailRequest, String.class); HttpRequestUtil.postWithJson(emailProperties.getUrl(), sendMailRequest, String.class); log.debug("responseEntity = {}", responseEntity); String rawResponseString = responseEntity.getBody(); SendMailResponse sendMailResponse = new ObjectMapper().readValue(rawResponseString, SendMailResponse.class); log.debug("sendMailResponse = {}", sendMailResponse); if(sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) { if (sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) { throw new SendEmailFailException("send email service return error msg! raw response string= " + rawResponseString); } return responseEntity.getBody(); } catch (SendEmailFailException e) { } catch (SendEmailFailException e) { throw e; } catch (Exception e) { log.warn("send email fail by other reason", e); pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
@@ -86,12 +86,7 @@ Long customerId = SecurityUtils.getCustomerDBId(); AppointmentCustomerViewDTO customerNewestExpiringAppointment = appointmentService.getCustomerNewestExpiringAppointment(customerId); if(Objects.nonNull(customerNewestExpiringAppointment)) { return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK); } @GetMapping("/consultant/pending/sum") pamapi/src/main/resources/config/application-dev.yml
@@ -45,10 +45,16 @@ # Remove 'faker' if you do not want the sample data to be loaded automatically contexts: dev, faker mail: host: localhost port: 25 username: password: host: smtp.gmail.com port: 587 username: pollex.testing@gmail.com password: ilismmmhtscppxft properties: mail: smtp: auth: true starttls: enable: true messages: cache-duration: PT1S # 1 second, see the ISO 8601 standard thymeleaf: @@ -120,15 +126,17 @@ e-service-login-func: ValidateUsrLogin e-service-login-sys: epos front-end-domain: http://localhost:3000 send-notify-msg: false sms: send-notify-msg: false url: https://localhost:8081/testSMS source-code: ePos sender: POS sms-type: '0017' subject: 'åªåå¹³å°éç¥' email: send-notify-msg: false url: https://localhost:8081/testEmail function-id: epos sender-email: noreply@pcalife.com.tw method: 'POLLEX_GMAIL' file-folder-path: C://pam_file pamapi/src/main/resources/config/application-pollex.yml
@@ -43,10 +43,16 @@ # Remove 'faker' if you do not want the sample data to be loaded automatically contexts: pollex, faker mail: host: localhost port: 25 username: password: host: smtp.gmail.com port: 587 username: pollex.testing@gmail.com password: ilismmmhtscppxft properties: mail: smtp: auth: true starttls: enable: true messages: cache-duration: PT1S # 1 second, see the ISO 8601 standard thymeleaf: @@ -118,15 +124,17 @@ e-service-login-func: ValidateUsrLogin e-service-login-sys: epos front-end-domain: http://dev.pollex.com.tw:5566/pam send-notify-msg: false sms: send-notify-msg: false url: https://localhost:8081/testSMS source-code: ePos sender: POS sms-type: '0017' subject: 'åªåå¹³å°éç¥' email: send-notify-msg: true url: https://localhost:8081/testEmail function-id: epos sender-email: noreply@pcalife.com.tw method: 'POLLEX_GMAIL' file-folder-path: C://pam_file pamapi/src/main/resources/config/application-prod.yml
@@ -140,15 +140,17 @@ e-service-login-func: ValidateUsrLogin e-service-login-sys: epos front-end-domain: https://vtwlifeopensysuat.pru.intranet.asia/pam send-notify-msg: true sms: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource source-code: ePos sender: POS sms-type: '0017' subject: 'åªåå¹³å°éç¥' email: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail function-id: epos sender-email: noreply@pcalife.com.tw method: 'PAM_EMAIL_SERVICE' file-folder-path: /sfs_omo/vtwlifewpsfs01/SensitiveData4AP$/OMO pamapi/src/main/resources/config/application-sit.yml
@@ -118,15 +118,17 @@ e-service-login-func: ValidateUsrLogin e-service-login-sys: epos front-end-domain: https://vtwlifeopensyssit.pru.intranet.asia/pam send-notify-msg: true sms: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource source-code: ePos sender: POS sms-type: '0017' subject: 'åªåå¹³å°éç¥' email: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail function-id: epos sender-email: noreply@pcalife.com.tw method: 'PAM_EMAIL_SERVICE' file-folder-path: /sfs_omo/vtwlifewuftp66/sensitivedata4ap$/OMOSIT pamapi/src/main/resources/config/application-uat.yml
@@ -118,15 +118,17 @@ e-service-login-func: ValidateUsrLogin e-service-login-sys: epos front-end-domain: https://vtwlifeopensysuat.pru.intranet.asia/pam send-notify-msg: true sms: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource source-code: ePos sender: POS sms-type: '0017' subject: 'åªåå¹³å°éç¥' email: send-notify-msg: true url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail function-id: epos sender-email: noreply@pcalife.com.tw method: 'PAM_EMAIL_SERVICE' file-folder-path: /sfs_omo/vtwlifewuftp66/sensitivedata4ap$/OMO