PAMapp/assets/scss/vendors/elementUI/_dateTimePicker.scss
@@ -52,6 +52,9 @@ td.available:hover { color: $CORAL; } td.today.current span { color: $PRIMARY_WHITE; } } } .el-year-table { PAMapp/components/Appointment/AppointmentInterviewList.vue
@@ -13,27 +13,57 @@ </template> <template v-if="interviewList.length"> <div class="interview--future"> <div v-for="(item, index) in futureList" :key="index + 'feature'" class="interview--future" @click="editInterview(item)" > <div class="record-card"> <div class="record-card-date"> <span class="bold">01/10</span> <span class="mt-5 line-space">09:00</span> <div> <UiDateFormat class="date bold" :date="item.interviewDate" onlyShowSection="DAY" /> </div> <div> <UiDateFormat class="time mt-5 line-space" :date="item.interviewDate" onlyShowSection="TIME" /> </div> </div> <div class="record-card-content"> <span>é è¨æé客æ¶ç´å¨å ¬å¸æ¨ä¸åå¡å»³ï¼æ¨è¦çå®å¿é«çéªèé²ç«ä¿å®</span> <span>{{item.content}}</span> </div> </div> </div> <section class="interview--past"> <section class="interview--past" v-for="(item, index) in pastList" :key="index + 'past'" @click="editInterview(item)" > <div class="record-card"> <div class="record-card-date"> <span class="bold">01/08</span> <span class="mt-5 line-space">09:00</span> <div> <UiDateFormat class="date bold" :date="item.interviewDate" onlyShowSection="DAY" /> </div> <div> <UiDateFormat class="time mt-5 line-space" :date="item.interviewDate" onlyShowSection="TIME" /> </div> </div> <div class="record-card-content"> <span>çè°æåä¿¡æä¾é»è©±è碼ï¼å»é»æ¨è¦é ç´æé</span> <span>{{item.content}}</span> </div> </div> </section> @@ -46,18 +76,49 @@ </template> <script lang="ts"> import { Vue, Component } from 'nuxt-property-decorator'; import { Vue, Component, Prop, Watch, Mutation } from 'nuxt-property-decorator'; import { InterviewRecord } from '~/shared/models/appointment.model'; @Component export default class AppointmentInterviewList extends Vue { @Prop() interviewList!: InterviewRecord[]; interviewList = []; @Mutation updateInterviewRecord!: (data: InterviewRecord) => void; appointmentId!: string; futureList: InterviewRecord[] = []; pastList: InterviewRecord[] = []; ////////////////////////////////////////////////////////////////////// mounted() { this.appointmentId = this.$route.params.appointmentId; } ////////////////////////////////////////////////////////////////////// @Watch('interviewList', {immediate: true}) updateInterviewList() { if (this.interviewList && this.interviewList.length > 0) { this.futureList = this.interviewList .filter(item => new Date(item.interviewDate).getTime() >= new Date().getTime()) this.pastList = this.interviewList .filter(item => new Date(item.interviewDate).getTime() < new Date().getTime()); } } ////////////////////////////////////////////////////////////////////// addInterview(): void { const appointmentId = this.$route.params.appointmentId; this.$router.push(`/appointment/${appointmentId}/interview/new`); this.$router.push(`/appointment/${this.appointmentId}/interview/new`); } editInterview(interviewRecord) { this.updateInterviewRecord(interviewRecord); this.$router.push(`/appointment/${this.appointmentId}/interview/${interviewRecord.id}`); } } PAMapp/components/BackActionBar.vue
@@ -68,6 +68,9 @@ case 'accountSetting': featureLabel = 'å人帳èè¨å®'; break; case 'appointmentAgenda': featureLabel = 'å³å°ç´è¨ªæç¨'; break; case 'consultantAccountSetting': featureLabel = 'æ¥ç帳èè³è¨'; break; PAMapp/components/Client/ClientCard.vue
@@ -6,7 +6,7 @@ class="rowStyle cursor--pointer" justify="space-between" :class="{'new': newAppointment }" @click.native="viewDetail" @click.native="viewAppointmentDetail" > <div class="test"> <div class="unread" v-if="isReserved"> @@ -14,9 +14,23 @@ </div> <div class="pl-10"> <div class="smTxt_bold name">{{ client.name }}</div> <div class="my-10 xsTxt">é ç´æå</div> <div class="professionals"> <div class="smTxt_bold name">{{ client.name || 'NO NAME' }}</div> <div v-if="client.communicateStatus === contactStatus.RESERVED" class="my-10 xsTxt">é ç´æå</div> <div class="xsTxt mb-10 mt-10" v-else-if="client.communicateStatus === contactStatus.CONTACTED"> ç´è¨ªç´é </div> <div class="xsTxt mb-10 mt-10" v-else> 滿æåº¦ <span v-if="client.satisfactionScore" class="xsTxt text--primary"> <UiReviewScore :score="client.satisfactionScore"></UiReviewScore> </span> <span v-else class="xsTxt text--mid_grey">æªå¡«</span> </div> <div class="professionals mb-10" v-if="client.communicateStatus === contactStatus.RESERVED"> <template v-if="client.requirement"> <span v-for="(item, index) in requirements" @@ -54,20 +68,20 @@ <div class="invite-msg smTxt_bold" @click.stop="makeInterview" @click.stop="showAddInterviewDialog" v-if="client.communicateStatus === contactStatus.RESERVED"> å³éç´è¨ªéç¥ </div> <div class="invite-msg smTxt_bold" @click.stop="closeAppointment" @click.stop="navigateToCloseAppointment" v-else-if="client.communicateStatus === contactStatus.CONTACTED"> çµæ¡ </div> <div class="invite-msg smTxt_bold" @click.stop="inviteReview" v-else> v-else-if="!client.satisfactionScore"> ç¼é滿æåº¦ </div> @@ -81,9 +95,9 @@ </el-row> <Ui-Dialog :isVisible.sync="isVisibleDialog" :isVisible.sync="isShowInformDialog" :width="dialogWidth" @closeDialog="closeDialog" @closeDialog="closeInformDialog" class="pam-myDemand-dialog" > <h5 class="subTitle text--center mb-30" @@ -142,7 +156,16 @@ </div> </Ui-Dialog> <InterviewMsg :isVisible.sync="isMsgDialog"></InterviewMsg> <InterviewMsg :client="client" :isVisible.sync="isShowAddInterviewDialog"> </InterviewMsg> <PopUpFrame :isOpen.sync="isShowInviteReviewDialog"> <div class="text--middle invite-review"> <div class="mb-30 mt-10">å·²ç¼é滿æåº¦</div> <div class="text--primary text--middle cursor--pointer text--underline" @click="isShowInviteReviewDialog = false" :size="'250px'">æç¥éäº</div> </div> </PopUpFrame> </div> </template> @@ -150,9 +173,9 @@ import { Vue, Component, Prop, Action, namespace, Watch } from 'nuxt-property-decorator'; import appointmentService from '~/shared/services/appointment.service'; import myConsultantService from '~/shared/services/my-consultant.service'; import UtilsService from '~/shared/services/utils.service'; import { hideReviews } from '~/shared/const/hide-reviews'; import myConsultantService from '~/shared/services/my-consultant.service'; import { ElRow } from 'element-ui/types/row'; import { Appointment, AppointmentMemoInfo } from '~/shared/models/appointment.model'; import { ContactStatus } from '~/shared/models/enum/contact-status'; @@ -187,21 +210,21 @@ @localStorage.Mutation storageClearAppointmentIdFromMsg!: () => void; isVisibleDialog = false; isMsgDialog = false; dialogWidth = ''; hideReviews = hideReviews; contactStatus = ContactStatus; contactStatus = ContactStatus; // currentAppointmentStatus = this.contactStatus.RESERVED; dialogWidth = ''; hideReviews = hideReviews; isEdit = false; isShowAddInterviewDialog = false; isShowInformDialog = false; isShowInviteReviewDialog = false; memo = ''; isEdit = false; memoInfo: AppointmentMemoInfo = { appointmentId: 0, content: '', id: 0 } memo = ''; content : '', id : 0 }; ////////////////////////////////////////////////////////////////////// @@ -224,15 +247,15 @@ ////////////////////////////////////////////////////////////////////// viewDetail(): void { viewAppointmentDetail(): void { this.$router.push(`/appointment/${this.client.id}`); } makeInterview(): void { this.isMsgDialog = true; showAddInterviewDialog(): void { this.isShowAddInterviewDialog = true; } closeAppointment(): void { navigateToCloseAppointment(): void { this.$router.push(`/appointment/${this.client.id}/close`); } @@ -240,59 +263,8 @@ alert('MAKE AN APPOINTMENT!'); } get newAppointment(): boolean { return !this.client.consultantViewTime && this.client.communicateStatus === 'reserved'; } get isReserved() { return this.client.communicateStatus === 'reserved'; } get isRead() { return !!this.client.consultantReadTime; } get requirements() { return this.client.requirement.split(','); } get gender() { if (this.client.gender) { return this.client.gender === 'male' ? 'ç·æ§' : '女æ§'; } return ''; } get hopeContactTime() { const contactList = this.client.hopeContactTime.split("'").map(item => item.slice(0, item.length)); return contactList.filter(item => !!item && item !== ",") } get reservedTxt(): string { if (this.isReserved) { return this.client.consultantReadTime ? 'å·²è®' : 'æªè®'; } else { return 'å·²è¯çµ¡' } } get displayTime(): string { if (this.isReserved) { return this.client.appointmentDate; } else { return this.client.lastModifiedDate; } } get time() { const formatDate = (this.$options.filters as any).formatDate(this.displayTime); return formatDate.split(' ')[1] } get date() { const formatDate = (this.$options.filters as any).formatDate(this.displayTime); return formatDate.split(' ')[0]; inviteReview(): void { this.isShowInviteReviewDialog = true ; } openDetail() { @@ -300,17 +272,17 @@ (this.$refs.clientCardRef as any).$el.classList.add('currentShowStyle'); }, 0) this.dialogWidth = UtilsService.isMobileDevice() ? '80%' : ''; this.isVisibleDialog = true; this.isShowInformDialog = true; } markAppointment() { myConsultantService.markAsContact(this.client.id).then(data => { this.updateMyAppointment(data); this.isVisibleDialog = false; this.isShowInformDialog = false; }) } closeDialog(): void { closeInformDialog(): void { const unread = !this.client.consultantReadTime; if (unread) { appointmentService.recordRead(this.client.id).then((_) => { @@ -377,54 +349,109 @@ this.memo = this.memoInfo.content; } get newAppointment(): boolean { return !this.client.consultantViewTime && this.client.communicateStatus === 'reserved'; } get isReserved() { return this.client.communicateStatus === 'reserved'; } get isRead() { return !!this.client.consultantReadTime; } get requirements() { return this.client.requirement.split(','); } get gender() { if (this.client.gender) { return this.client.gender === 'male' ? 'ç·æ§' : '女æ§'; } return ''; } get hopeContactTime() { const contactList = this.client.hopeContactTime.split("'").map(item => item.slice(0, item.length)); return contactList.filter(item => !!item && item !== ",") } get reservedTxt(): string { if (this.isReserved) { return this.client.consultantReadTime ? 'å·²è®' : 'æªè®'; } else { return 'å·²è¯çµ¡' } } get displayTime(): string { if (this.isReserved) { return this.client.appointmentDate; } else { return this.client.lastModifiedDate; } } get date() { const formatDate = (this.$options.filters as any).formatDate(this.displayTime); return formatDate.split(' ')[0]; } get time() { const formatDate = (this.$options.filters as any).formatDate(this.displayTime); return formatDate.split(' ')[1] } } </script> <style lang="scss" scoped> .rowStyle { padding: 10px 15px 10px 5px; background-color: $PRIMARY_WHITE; margin-bottom: 10px; display: flex; justify-content: space-between; border-left : solid 4px transparent; display : flex; justify-content : space-between; margin-bottom : 10px; padding : 10px 15px 10px 5px; transition: background-color 0.5s; &.new { border-left: solid 4px $YELLOW; border-color: $YELLOW; } &.currentShowStyle { background-color: rgba(236, 195, 178, 0.5); transition: background-color 0.5s; transition : background-color 0.5s; } .unread { align-self: center; .circle { width: 10px; height: 10px; border-radius: 50%; background-color: $PRIMARY_RED; margin: auto; border-radius : 50%; height : 10px; margin : auto; width : 10px; } } .satisfaction { font-size: 12px; font-size : 12px; font-weight: bold; margin-top: 5px; margin-top : 5px; .unfilled { color : $MID_GREY; font-weight: lighter; color: $MID_GREY; } } .professionals { overflow: hidden; white-space: nowrap; overflow : hidden; text-overflow: ellipsis; white-space : nowrap; .professionalsTxt { font-size: 12px; font-size : 12px; margin-right: 5px; } .noProfessionalsTxt { color: $PRUDENTIAL_GREY; color : $PRUDENTIAL_GREY; font-weight: lighter; } } @@ -442,24 +469,23 @@ } } .flex-column { display: flex; flex-direction: column; display : flex; flex-direction : column; justify-content: space-between; } .dialogTxt { font-size: 20px; overflow-y:scroll; font-size : 20px; max-height: 25vh; overflow-y: scroll; @include desktop { height: 400px; } } .memoTitleStyle { display: flex; flex-direction: row; display : flex; flex-direction : row; justify-content: space-between; .edit { .edit { align-self: flex-end; } } @@ -471,6 +497,11 @@ } .invite-msg{ color: $PRIMARY_RED; @extend .text--underline; @extend .text--underline; } .invite-review{ align-items : center; display : flex; flex-direction: column; } </style> PAMapp/components/DateTimePicker.vue
@@ -2,19 +2,28 @@ <template> <div class="dateTime"> <UiDatePicker @changeDate="changeDateTime($event, 'date')"></UiDatePicker> <UiTimePicker @changeTime="changeDateTime($event, 'time')"></UiTimePicker> <UiDatePicker @changeDate="changeDateTime($event, 'date')" :defaultValue="defaultValue" ></UiDatePicker> <UiTimePicker @changeTime="changeDateTime($event, 'time')" :defaultValue="defaultValue" ></UiTimePicker> </div> </template> <script lang="ts"> import { Component, Emit, Vue } from "nuxt-property-decorator"; import { Component, Emit, Prop, Vue } from "nuxt-property-decorator"; @Component export default class DateTimePicker extends Vue { changeDate!: Date; changeTime!: string; @Prop() defaultValue!: string; @Emit('changeDateTime') changeDateTime(event, type) { if (type === 'date') { PAMapp/components/Interview/InterviewAdd.vue
@@ -1,43 +1,229 @@ <template> <div class="edit-appointment-record"> <div class="edit-appointment-record-date"> <span>ä»å¤© 11:00 建ç«</span> <span>ä»å¤© 11:00 æ´æ°</span> <div class="edit-appointment-record-date" v-if="interviewId"> <span>{{interviewRecord.createdDate | formatDate}} 建ç«</span> <span>{{interviewRecord.lastModifiedDate | formatDate}} æ´æ°</span> </div> <el-row class="mdTxt mb-10"> <el-col :xs="16" :sm="20">ç´è¨ªæé</el-col> <el-col :xs="8" :sm="4" class="text--right" v-if="interviewId"> <span v-if="!isEdit" class="mr-10 text--primary text--underline cursor--pointer" @click="showCancelPopUp = true" >åªé¤</span> <span v-if="!isEdit" class="text--primary text--underline cursor--pointer" @click="isEdit = !isEdit" >編輯</span> </el-col> </el-row> <template v-if="!interviewId || isEdit"> <DateTimePicker @changeDateTime="interviewTime = $event" :defaultValue="defaultValue" ></DateTimePicker> </template> <template v-else> <div class="mdTxt lighter mt-20"> {{formatInterviewDate}} </div> </template> <div class="mdTxt mb-10 mt-30">ç´è¨ªç´é</div> <template v-if="!interviewId || isEdit"> <el-input type="textarea" :rows="5" placeholder="è«è¼¸å ¥ç´è¨ªç´é" resize="none" v-model="content" > </el-input> </template> <template v-else> <div class="mdTxt lighter mt-20"> {{content}} </div> </template> <div class="edit-appointment-record-btn" v-if="!interviewId || isEdit"> <el-button @click="cancel">åæ¶</el-button> <el-button :disabled="!interviewTime || !content" @click="saveInterviewRecord" >確å®</el-button> </div> <div class="mdTxt mb-10">ç´è¨ªæé</div> <DateTimePicker @changeDateTime="interviewTime = $event" ></DateTimePicker> <PopUpFrame :isOpen.sync="showCancelPopUp" @closePopUp="showCancelPopUp = false" > <div class="text--center mdTxt">æ¯å¦åªé¤æ¤çç´è¨ªè¨éï¼</div> <div class="text--center mt-30"> <el-button @click="showCancelPopUp = false">å¦</el-button> <el-button @click="deleteInterviewRecord" type="primary">æ¯</el-button> </div> </PopUpFrame> <div class="mdTxt mb-10 mt-10">ç´è¨ªç´é</div> <el-input type="textarea" :rows="5" placeholder="ç´è¨ªéç¥" resize="none"> </el-input> <PopUpFrame :isOpen.sync="showConfirmPopup" @closePopUp="closePopup"> <div class="text--center mdTxt">{{confirmTxt}}ï¼</div> <div class="text--center mt-30"> <el-button @click="closePopup" type="primary">確å®</el-button> </div> </PopUpFrame> <div class="edit-appointment-record-btn"> <el-button>åæ¶</el-button> <el-button :disabled="!interviewTime">確å®</el-button> </div> <PopUpFrame :isOpen.sync="showFutureDateConfirmPopup" @closePopUp="closePopup"> <div class="text--center mdTxt">{{confirmTxt}}ï¼</div> <div class="text--center mdTxt">ç«å³ç¼éç´è¨ªéç¥ï¼</div> <div class="text--center mt-30"> <el-button @click="closePopup">å ä¸ç¼é</el-button> <el-button @click="showInterviewMsgPopup = true" type="primary">å³éç´è¨ªéç¥</el-button> </div> </PopUpFrame> <InterviewMsg :isVisible.sync="showInterviewMsgPopup" @closeDialog="closePopup" ></InterviewMsg> </div> </template> <script lang="ts"> import { AppointmentLog } from '~/shared/models/appointment.model'; import { Vue, Component, Prop } from 'nuxt-property-decorator'; import authService from '~/shared/services/auth.service'; import { InterviewRecord, InterviewRecordInfo } from '~/shared/models/appointment.model'; import { Vue, Component, Prop, State, Mutation, Watch, Action } from 'nuxt-property-decorator'; import appointmentService from '~/shared/services/appointment.service'; @Component export default class InterviewAdd extends Vue { @State interviewRecord!: InterviewRecord; @Mutation updateInterviewRecord!: (data: InterviewRecord) => void; @Mutation clearInterviewRecord!: () => void; interviewTime = ''; content = ''; // @Prop() // myAppointmentReviewLogList!: AppointmentLog[]; interviewId = ''; appointmentId = ''; confirmTxt: 'æ°å¢æå' | '編輯æå' | 'åªé¤æå' = 'æ°å¢æå'; // isUserLogin = false; isEdit = false; ////////////////////////////////////////////////////////////////////// // mounted() { // this.isUserLogin = authService.isUserLogin(); // } showConfirmPopup = false; showCancelPopUp = false; showInterviewMsgPopup = false; showFutureDateConfirmPopup = false; defaultValue!: Date; //////////////////////////////////////////////////////////////////// mounted() { this.interviewId = this.$route.params.interviewId; this.appointmentId = this.$route.params.appointmentId; const isEditPage = this.interviewId && this.interviewRecord; if (isEditPage) { this.checkInterviewRecord(); } } private checkInterviewRecord() { if (this.interviewRecord.appointmentId !== +this.appointmentId || this.interviewRecord.id !== +this.interviewId) { appointmentService.getAppointmentDetail(+this.appointmentId).then((data) => { const currentInterviewRecord = data.interviewRecordDTOs.filter(item => item.id === +this.interviewId)[0]; this.updateInterviewRecord(currentInterviewRecord); }) } } destroyed() { this.clearInterviewRecord(); } //////////////////////////////////////////////////////////////////// @Watch('interviewRecord', {immediate: true}) watchInterviewRecord() { if (this.interviewRecord && this.interviewRecord.content) { this.content = this.interviewRecord.content; this.defaultValue = new Date(this.interviewRecord.interviewDate); } } //////////////////////////////////////////////////////////////////// saveInterviewRecord() { const interviewRecordInfo: InterviewRecordInfo = { content: this.content, interviewDate: this.interviewTime, appointmentId: +this.appointmentId }; if (!this.interviewId) { this.createdRecord(interviewRecordInfo); } else { const updateInterviewRecord = { ...interviewRecordInfo, id: +this.interviewId } this.updateRecord(updateInterviewRecord); } } private createdRecord(interviewRecordInfo) { appointmentService.createInterviewRecord(interviewRecordInfo).then(res => { this.confirmTxt = 'æ°å¢æå' this.showPopUp(); }); } private updateRecord(updateInterviewRecord) { appointmentService.updateInterviewRecord(updateInterviewRecord).then(res => { this.confirmTxt = '編輯æå'; this.showPopUp(); }); } private showPopUp() { if (new Date(this.interviewTime).getTime() >= new Date().getTime()) { this.showFutureDateConfirmPopup = true; } else { this.showConfirmPopup = true; } } closePopup() { this.$router.push(`/appointment/${this.appointmentId}`); } deleteInterviewRecord() { appointmentService.deleteInterviewRecord(this.interviewId).then(res => { this.confirmTxt = 'åªé¤æå'; this.showConfirmPopup = true; }); } cancel() { if (this.interviewId) { this.content = this.interviewRecord.content; this.defaultValue = new Date(this.interviewRecord.interviewDate); this.isEdit = false; } else { this.$router.push(`/appointment/${this.appointmentId}`); } } //////////////////////////////////////////////////////////////////// get formatInterviewDate() { const interviewDate = new Date(this.interviewRecord.interviewDate); return `${interviewDate.getFullYear()}/${interviewDate.getMonth() + 1}/${interviewDate.getDate()} ${interviewDate.getHours()}:${interviewDate.getMinutes()}`; } } </script> PAMapp/components/Interview/InterviewMsg.vue
@@ -19,23 +19,39 @@ resize="none" v-model="interviewTxt"> </el-input> <div class="mdTxt mt-30 mb-10">é è¨ç´è¨ªææ®µ</div> <DateTimePicker @changeDateTime="interviewTime = $event" ></DateTimePicker> <div class="msg-dialog-btn"> <el-button :disabled="!interviewTime">å³é</el-button> <el-button @click="addInterview" :disabled="!interviewTime">å³é</el-button> </div> </el-dialog> <PopUpFrame :isOpen.sync="isShowSuccessAlert"> <div class="text--middle invite-review"> <div class="mb-30 mt-10">å·²ç¼éç´è¨ªéç¥</div> <div class="text--primary text--middle cursor--pointer text--underline" @click="closeAllDialog " :size="'250px'">æç¥éäº</div> </div> </PopUpFrame> </div> </template> <script lang="ts"> import { Vue, Component, Prop, PropSync, Emit, Action } from 'nuxt-property-decorator'; import appointmentService from '~/shared/services/appointment.service'; import { Appointment, ToInformAppointment } from '~/shared/models/appointment.model'; @Component export default class InterviewMsg extends Vue { @Action storeMyAppointmentList!: () => Promise<number>; @PropSync('isVisible') dialogVisible!: boolean; @@ -43,15 +59,39 @@ dialogWidth!:string; @Prop() appointmentId!: number; client!: Appointment; @Emit('closeDialog') closeDialog() { return; } isShowSuccessAlert = false; interviewTxt = ""; interviewTime = ''; ////////////////////////////////////////////////////////////////////// addInterview() { console.log('client', this.client); const appointmentInformation: ToInformAppointment = { appointmentId: this.client.id, email : this.client?.email, interviewDate: this.interviewTime, message : this.interviewTxt, phone : this.client?.phone, }; appointmentService.informAppointment(appointmentInformation).then((_) => { this.isShowSuccessAlert = true ; }); } closeAllDialog() { this.isShowSuccessAlert = false ; this.dialogVisible = false; this.storeMyAppointmentList(); } } </script> @@ -83,5 +123,9 @@ display: flex; justify-content: center; } .invite-review{ display: flex; flex-direction: column; align-items: center; } </style> PAMapp/components/Interview/interviewNotification.vue
¤ñ¹ï·sÀÉ®× @@ -0,0 +1,76 @@ <template> <div class="remind-card"> <div class="remind-card-header"> <div class="mdTxt text--black">å³å°ç´è¨ªæç¨</div> <div class="smTxt_bold text--primary sub-title cursor--pointer" @click="toAgendaPage">æ¥çå ¨é¨</div> </div> <div class="remind-container"> <div class="remind-content"> <div class="remind-first-line mr-10"> <div class="txt-margin">2021</div> <div>12/25</div> </div> <div class="remind-content-txt"> <div class="txt-margin">09:00</div> <div>ç´è¨ªæå¿é¶</div> </div> </div> </div> </div> </template> <script lang="ts"> import { Vue, Component, Prop, Emit, Action, State, namespace } from 'nuxt-property-decorator'; @Component export default class ConsultantAppointmentRemind extends Vue { toAgendaPage(){ this.$router.push('/appointmentAgenda') } ////////////////////////////////////////////////////////////////////// } </script> <style lang="scss" scoped> .remind-card{ display: flex; flex-direction:column; justify-content: center; border: 1px solid #CCCCCC; height: 131px; padding: 10px 20px; background-color: #fff; .remind-card-header{ display: flex; justify-content: space-between; align-items: baseline; .sub-title{ border-bottom: 1px solid $PRIMARY_RED; } } .remind-container{ margin-top: 13px; border: 1px solid #CCCCCC; height: 61px; border-radius: 5px; margin-bottom: 20px; .remind-content{ display: flex; padding: 13px 10px; .remind-first-line{ display: flex; flex-direction: column; align-items: center; font-weight: bold; } } } } .remind-content-txt{ display: flex; flex-direction: column; } .txt-margin{ margin-bottom: 3px; } </style> PAMapp/components/Ui/UiDatePicker.vue
@@ -14,15 +14,26 @@ </template> <script lang="ts"> import { Component, Emit, Vue } from "nuxt-property-decorator"; import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator"; @Component export default class UiDatePicker extends Vue { dateValue = ''; @Prop() defaultValue!: string; @Emit('changeDate') changeDate() { return this.dateValue; } @Watch('defaultValue', {immediate: true}) updateDefault() { if (this.defaultValue) { this.dateValue = this.defaultValue; this.changeDate(); } } } </script> PAMapp/components/Ui/UiTimePicker.vue
@@ -14,7 +14,7 @@ </template> <script lang="ts"> import { Component, Emit, Vue } from "nuxt-property-decorator"; import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator"; @Component export default class UiTimePicker extends Vue { @@ -25,9 +25,22 @@ end: '21:00' } @Prop() defaultValue!: string; @Emit('changeTime') changeTime() { return this.timeValue; } @Watch('defaultValue', {immediate: true}) updateDefault() { if (this.defaultValue) { const hours = new Date(this.defaultValue).getHours(); const minutes = new Date(this.defaultValue).getMinutes(); this.timeValue = `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`; this.changeTime(); } } } </script> PAMapp/pages/appointment/_appointmentId/close/index.vue
@@ -40,11 +40,7 @@ type="flex" class="pam-paragraph"> <UiField label="é²ä»¶æé" :labelSize="20"> <input class="appointment-client-detail-close__input" v-model="appointmentCloseInfo.planCode" placeholder="TBD: æ¥æå ä»¶" type="text"> <DateTimePicker @changeDateTime="appointmentCloseDate = $event"></DateTimePicker> </UiField> </el-row> </template> @@ -65,7 +61,8 @@ <div style="display: flex" class="mt-10"> <input v-if="appointmentCloseInfo.closedReason === 'other'" v-if="appointmentCloseInfo.closedReason === 'other' || appointmentCloseInfo.closedReason === 'no_suitable_commodity'" class="appointment-client-detail-close__input" v-model="appointmentCloseInfo.closedOtherReason" placeholder="è«è¼¸å ¥åå ï¼é50åã" @@ -106,6 +103,12 @@ <el-button @click="closeAppointment">確èª</el-button> </el-row> <PopUpFrame :isOpen.sync="isShowSuccessAlert"> <div class="text--middle invite-review"> <div class="mb-30 mt-10">çµæ¡æå</div> <el-button type="primary" @click="closeAlert">確å®</el-button> </div> </PopUpFrame> </div> </template> @@ -122,11 +125,14 @@ contactStatus = ContactStatus; appointmentCloseDate = ''; isShowSuccessAlert = false; appointmentCloseInfo = { closedOtherReason : '', closedReason : 'other', planCode : '', policyEntryDate : '', policyEntryDate : this.appointmentCloseDate, policyholderIdentityId: '', remark : '', selectCloseOption : 'done', @@ -145,9 +151,29 @@ appointmentFailReason = [ { key: 'ç¡æ³è¯ç¹«å®¢æ¶', value: 'cannot_to_contact_customer' }, { key: 'å®ç´è«®è©¢', value: 'only_consultation' }, { key: 'ç¡åé©åå', value: 'no_suitable_commodity' }, { key: 'æ ¸ä¿åé¡- 髿³ã財åãè·æ¥', value: 'prohibited_factors' }, { key: 'ç¶æ¿å ç´ ', value: 'economy' }, { key: 'å ¶ä»', value: 'other' } }, ]; closeAppointment(): void { @@ -161,6 +187,7 @@ policyholderIdentityId: this.appointmentCloseInfo.policyholderIdentityId, } appointmentService.closeAppointment(toDoneAppointment).then((res) => res); this.isShowSuccessAlert = true; } else { const toCloseAppointment: ToCloseAppointment = { appointmentId : appointmentId, @@ -170,7 +197,13 @@ remark : this.appointmentCloseInfo.remark, } appointmentService.closeAppointment(toCloseAppointment).then((res) => res); this.isShowSuccessAlert = true; } } closeAlert(){ this.isShowSuccessAlert = false ; this.$router.push(`/myAppointmentList/contactedList`); } } @@ -189,4 +222,9 @@ color: $MID_GREY; } } .invite-review{ display: flex; flex-direction: column; align-items: center; } </style> PAMapp/pages/appointment/_appointmentId/index.vue
@@ -1,10 +1,10 @@ <template> <div class="appointment-client-detail-page"> <div class="date-detail"> <!-- TODO: è¦ä¾æä¸å step 顯示ä¸å Date [Tomas, 2022/1/11] --> <div>{{ appointmentDetail.appointmentDate }}</div> <div>{{ appointmentDetail.consultantReadTime }}</div> <div>{{ appointmentDetail.appointmentDate | formatDate }}</div> <div>{{ appointmentDetail.consultantReadTime | formatDate }}</div> </div> <!-- TODO: re-send api to update progress [Tomas, 2022/1/17 17:02] --> <AppointmentProgress class="mt-10" :currentStep="appointmentDetail.communicateStatus" @@ -22,7 +22,7 @@ </div> </div> <div class="client-detail-info__information"> <div>{{ appointmentDetail.age || '--' }}æ²</div> <div>{{ appointmentDetail.age | toAgeLabel }}</div> <div>{{ appointmentDetail.phone }}</div> <div class="text--underline"> {{ appointmentDetail.email }} @@ -43,18 +43,22 @@ </div> </div> <div class="client-detail-action"> <div class="client-detail-action" v-if="showWhenAppointmentHasClosed"> <el-button >ç¼é滿æåº¦</el-button> </div> <div class="client-detail-action" v-else> <el-button @click="closeAppointment" >çµæ¡</el-button> <el-button @click="sendMsg" style="margin-left: 0px">éç¥/ç´è¨ª</el-button> <!-- <el-button>ç¼é滿æåº¦</el-button> --> </div> </section> <section class="close-appointment-detail"> <section class="close-appointment-detail" v-if="showWhenAppointmentHasClosed"> <div class="close-appointment-detail-nav"> <div class="mdTxt">çµæ¡æ¹å¼</div> <div class="mdTxt text--primary text--underline">編輯</div> <div class="mdTxt text--primary text--underline cursor--pointer" @click="editAppointmentHasClosed">編輯</div> </div> <span class="mt-10 mb-30">æäº¤</span> @@ -69,15 +73,18 @@ </section> <InterviewMsg :isVisible.sync="isVisibleDialog"></InterviewMsg> <InterviewMsg :isVisible.sync="isVisibleDialog" :client="appointmentDetail"> </InterviewMsg> <section class="mt-30"> <AppointmentInterviewList /> <AppointmentInterviewList :interviewList="appointmentDetail.interviewRecordDTOs" /> </section> <section class="mt-30"> <AppointmentRecordList /> <AppointmentRecordList :noticeLogs="appointmentDetail.appointmentNoticeLogs" /> </section> </div> @@ -89,14 +96,17 @@ import { Vue, Component } from 'vue-property-decorator'; import appointmentService from '~/shared/services/appointment.service'; import { AppointmentDetail } from '~/shared/models/appointment.model'; import { Appointment } from '~/shared/models/appointment.model'; import { ContactStatus } from '~/shared/models/enum/contact-status'; @Component export default class AppointmentDetailComponent extends Vue { appointmentDetail!: AppointmentDetail; appointmentDetail!: Appointment; isVisibleDialog = false; interviewTxt = ""; contactStatus = ContactStatus; ////////////////////////////////////////////////////////////////////// async asyncData(context: Context) { @@ -112,9 +122,19 @@ this.$router.push(`/appointment/${this.appointmentDetail.id}/close`); } sendMsg():void{ sendMsg():void { this.isVisibleDialog = true; } editAppointmentHasClosed(): void{ this.$router.push(`/appointment/${this.appointmentDetail.id}/close`); } get showWhenAppointmentHasClosed(): boolean { return this.appointmentDetail.communicateStatus === this.contactStatus.DONE || this.appointmentDetail.communicateStatus === this.contactStatus.CLOSE || this.appointmentDetail.communicateStatus === this.contactStatus.CANCEL; } } </script> PAMapp/pages/appointmentAgenda/index.vue
¤ñ¹ï·sÀÉ®× @@ -0,0 +1,78 @@ <template> <div> <div class="mdTxt">å³å°ç´è¨ªæç¨(3)</div> <div class="remind-container"> <div class="remind-content"> <div class="remind-first-line mr-10"> <div class="txt-margin">2021</div> <div>12/25</div> </div> <div class="remind-content-txt"> <div class="txt-margin">09:00</div> <div>ç´è¨ªæå¿é¶</div> </div> </div> </div> <div class="remind-container"> <div class="remind-content"> <div class="remind-first-line mr-10"> <div class="txt-margin">2021</div> <div>12/22</div> </div> <div class="remind-content-txt"> <div class="txt-margin">13:00</div> <div>ç´è¨ªçè°æ</div> </div> </div> </div> <div class="remind-container"> <div class="remind-content"> <div class="remind-first-line mr-10"> <div class="txt-margin">2021</div> <div>12/18</div> </div> <div class="remind-content-txt"> <div class="txt-margin">09:00</div> <div>ç´è¨ªæä¼¯ä¼¯</div> </div> </div> </div> </div> </template> <script lang="ts"> import { Vue, Component } from 'nuxt-property-decorator'; @Component export default class Agenda extends Vue { ////////////////////////////////////////////////////////////////////// } </script> <style lang="scss" scoped> .remind-container{ margin-top: 13px; border: 1px solid #CCCCCC; height: 61px; border-radius: 5px; margin-bottom: 20px; background-color: #fff; .remind-content{ display: flex; padding: 13px 10px; .remind-first-line{ display: flex; flex-direction: column; align-items: center; font-weight: bold; } } } .remind-content-txt{ display: flex; flex-direction: column; } .txt-margin{ margin-bottom: 3px; } </style> PAMapp/pages/myAppointmentList.vue
@@ -1,6 +1,6 @@ <template> <div> <div class="pam-myAppointment-banner"></div> <InterviewNotification></InterviewNotification> <div class="pam-container"> <div class="pam-cus-tabs mb-30"> <div @@ -15,7 +15,7 @@ :class="{'is-active': activeTabName === 'contactedList'}" @click="clickTab('contactedList')" > <span class="smTxt">ç´è¨ªä¸({{ appointmentList.length }})</span> <span class="smTxt">ç´è¨ªä¸({{ contactedList.length }})</span> </div> <div class="cus-tab-item" @@ -110,13 +110,18 @@ } } // TODO: 調æ´ç¨å¼ç¢¼ [Tomas, 2022/1/14 12:02] private redirectAppointmentStatus() { const currentAppointmentIndex = this.myAppointmentList .findIndex(item => item.id === +this.currentAppointmentIdFromMsg); if (currentAppointmentIndex > -1) { const communicateStatus = this.myAppointmentList[currentAppointmentIndex].communicateStatus; const pathName = communicateStatus === 'reserved' ? 'appointmentList' : 'closedList'; let pathName = 'closedList' if (communicateStatus === this.contactStatus.RESERVED || communicateStatus === this.contactStatus.PICKED) { pathName = 'contactedList'; } if (communicateStatus === this.contactStatus.CONTACTED) { pathName = 'contactedList'; } this.$router.push( { path: '/myAppointmentList/' + pathName, PAMapp/pages/myAppointmentList/closedList.vue
@@ -1,9 +1,9 @@ <template> <div> <div class="pam-closed-appointment-list"> <el-input type="text" placeholder="è«è¼¸å ¥ééµå" class="mb-30 pam-clientReserved-input" class="mb-10 pam-clientReserved-input" v-model="keyWord" @keyup.enter.native="search" > @@ -13,6 +13,12 @@ @click="search" ></i> </el-input> <div class="closed-appointment__tag-filter"> <el-radio v-model="selectedClosedCategory" :label="'all'" border>å ¨é¨({{ itemSum }})</el-radio> <el-radio v-model="selectedClosedCategory" :label="'done'" border>æäº¤({{ doneItemSum }})</el-radio> <el-radio v-model="selectedClosedCategory" :label="'closed'" border>æªæäº¤({{ closedItemSum }})</el-radio> </div> <ClientList :clients="pageList" @@ -36,7 +42,7 @@ const localStorage = namespace('localStorage'); @Component export default class ClientContactedList extends Vue { export default class ClientClosedList extends Vue { @State('myAppointmentList') myAppointmentList!: Appointment[]; @@ -44,12 +50,16 @@ @localStorage.Getter currentAppointmentIdFromMsg!: string; contactedList: Appointment[] = []; closedItemSum = 0; closedList: Appointment[] = []; contactStatus= ContactStatus; currentPage : number = 1; doneItemSum = 0; filterList : Appointment[] = []; itemSum = 0; keyWord : string = ''; pageList : Appointment[] = []; currentPage : number = 1; contactStatus= ContactStatus; selectedClosedCategory: 'all' | 'done' | 'closed' = 'all'; ////////////////////////////////////////////////////////////////////// @@ -61,12 +71,14 @@ @Watch('myAppointmentList') onMyAppointmentListChange() { this.contactedList = (this.myAppointmentList || []) this.closedList = (this.myAppointmentList || []) .filter(item => item.communicateStatus === this.contactStatus.DONE || item.communicateStatus === this.contactStatus.CLOSE) .map((item) => ({...item, sortTime: new Date(item.contactTime)})) .sort((prevItem, nextItem) => +nextItem.sortTime - +prevItem.sortTime); this.filterList = this.contactedList; this.filterList = this.closedList; this.itemSum = this.closedList.length; this.doneItemSum = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.DONE).length; this.closedItemSum = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.CLOSE).length; this.getCurrentPage(); } @@ -78,12 +90,24 @@ } } @Watch('selectedClosedCategory') onSelectedClosedCategoryChanges() { this.search(); } ////////////////////////////////////////////////////////////////////// search(): void { this.filterList = this.contactedList.filter(item => { return item?.name?.match(this.keyWord) || item?.requirement?.match(this.keyWord) }) if (this.selectedClosedCategory === this.contactStatus.DONE) { this.filterList = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.DONE); } else if (this.selectedClosedCategory === this.contactStatus.CLOSE) { this.filterList = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.CLOSE); } else { this.filterList = this.closedList; } this.filterList = this.filterList.filter(item => { return item?.name?.match(this.keyWord) || item?.requirement?.match(this.keyWord) }) } changePage(pageList: Appointment[]): void { @@ -92,3 +116,30 @@ } </script> <style lang="scss"> .pam-closed-appointment-list { .closed-appointment__tag-filter { display: flex; .el-radio { border-color: $PRIMARY_BLACK; border-width: 2px; font-size : 16px; margin-left : 0 !important; margin-right: 10px; padding : 10px; @extend .fix-chrome-click--issue; &.is-checked { background-color: #D0D0CE; } .el-radio__input { display: none; } .el-radio__label { color : $PRIMARY_BLACK !important; padding: 0px !important; } } } } </style> PAMapp/shared/models/appointment.model.ts
@@ -34,6 +34,7 @@ phone : string; requirement : string; satisfactionScore : number; appointmentNoticeLogs: NoticeLogs[]; }; export interface AppointmentMemoInfo { @@ -51,37 +52,21 @@ lastModifiedBy : string; lastModifiedDate: string; } export interface NoticeLogs { id: number, phone: string, email: string, appointmentId: number, content: string, createdDate: string } export interface AppointmentWithConsultantInfo extends Appointment { consultantAvatar : string; consultantExpertList: string[]; consultantName : string; contactStatus : string; updateTime : string; } export interface AppointmentDetail { age : string; agentNo : string; appointmentDate : string; appointmentMemoList?: string[]; communicateStatus : string; consultantReadTime : string; consultantViewTime : string; contactTime : string; contactType : string; customerId : number; email : string; gender : string; hopeContactTime : string; id : number; interviewRecordDTOs : string[]; job : string; lastModifiedDate : string; name : string; otherRequirement : string; phone : string; requirement : string; satisfactionScore? : number; } export interface AppointmentParams { age : string; @@ -150,3 +135,21 @@ remark? : string; } export interface ToInformAppointment { appointmentId: number; email : string; interviewDate: string; message : string; phone : string; } export interface InterviewRecordInfo { content: string; interviewDate: string; appointmentId: number; } export interface UpdateInterviewRecordInfo { /** interviewRecord id */ id: number; } PAMapp/shared/services/appointment.service.ts
@@ -1,6 +1,6 @@ import { http } from "./httpClient"; import { Appointment, AppointmentDetail, AppointmentMemoInfo, createdMemoInfo, EditAppointmentParams, ToCloseAppointment, ToDoneAppointment, updatedMemoInfo } from "~/shared/models/appointment.model"; import { Appointment, AppointmentMemoInfo, createdMemoInfo, EditAppointmentParams, InterviewRecordInfo, ToCloseAppointment, ToDoneAppointment, ToInformAppointment, updatedMemoInfo, UpdateInterviewRecordInfo } from "~/shared/models/appointment.model"; class AppointmentService { @@ -26,7 +26,7 @@ } // åå¾é ç´å®ç´°ç¯ async getAppointmentDetail(appointmentId: number):Promise<AppointmentDetail> { async getAppointmentDetail(appointmentId: number):Promise<Appointment> { return http.get(`/appointment/getDetail/${appointmentId}`).then((res) => res.data); } @@ -59,6 +59,26 @@ async closeAppointment(appointmentInfo: ToDoneAppointment | ToCloseAppointment) { return http.post(`/appointment/close`, appointmentInfo).then((res) => res.data); } // ç´è¨ªéç¥ API async informAppointment(appointmentInformation: ToInformAppointment) { return http.post(`/notice/send`, appointmentInformation).then((res) => res.data); } // æ°å¢ç´è¨ªè¨é async createInterviewRecord(interviewRecordInfo: InterviewRecordInfo) { return http.post('/interview_record/create', interviewRecordInfo).then(res => res.data); } // ä¿®æ¹ç´è¨ªè¨é async updateInterviewRecord(updateInterviewRecordInfo: UpdateInterviewRecordInfo) { return http.post('/interview_record/update', updateInterviewRecordInfo) } // åªé¤ç´è¨ªè¨é async deleteInterviewRecord(interviewRecordId) { return http.delete(`/interview_record/${interviewRecordId}`); } } export default new AppointmentService(); PAMapp/store/index.ts
@@ -9,8 +9,10 @@ import reviewsService from '~/shared/services/reviews.service'; import { Consultant } from '~/shared/models/consultant.model'; import { Appointment, AppointmentLog } from '~/shared/models/appointment.model'; import { Appointment, AppointmentLog, InterviewRecord } from '~/shared/models/appointment.model'; import { AgentOfStrictQuery } from '~/shared/models/strict-query.model'; import { AgentInfo } from '~/shared/models/agent-info.model'; import { agentCommunicationStyleList } from '~/shared/const/agent-communication-style-list'; @Module export default class Store extends VuexModule { recommendList: Consultant[] = []; @@ -21,6 +23,16 @@ myNewAppointmentSum: number = 0; myAppointmentReviewLogList: AppointmentLog[] = []; interviewRecord: InterviewRecord = { appointmentId : 0, content : '', createdBy : '', createdDate : '', id : 0, interviewDate : '', lastModifiedBy : '', lastModifiedDate: '' } get isUserLogin() { return this.context.getters['localStorage/isUserLogin']; @@ -56,6 +68,25 @@ this.myAppointmentReviewLogList = data; } @Mutation updateInterviewRecord(data: InterviewRecord) { this.interviewRecord = data; } @Mutation clearInterviewRecord() { this.interviewRecord = { appointmentId : 0, content : '', createdBy : '', createdDate : '', id : 0, interviewDate : '', lastModifiedBy : '', lastModifiedDate: '' } } @Action storeRecommendList() { queryConsultantService.getRecommendConsultantList().then(data => {