| | |
| | | <div> |
| | | <el-row |
| | | type="flex" |
| | | ref="clientCardRef" |
| | | class="rowStyle cursor--pointer" |
| | | justify="space-between" |
| | | :class="{'new': !isView && isReserved}" |
| | | @click.native="openDetail" |
| | | :class="{'new': newAppointment }" |
| | | @click.native="viewAppointmentDetail" |
| | | > |
| | | <el-col :xs="1" :sm="1" class="unread" align="middle" v-if="isReserved"> |
| | | <div class="test"> |
| | | <div class="unread" v-if="isReserved"> |
| | | <div class="circle" v-if="!isRead"></div> |
| | | </el-col> |
| | | <el-col :xs="5" :sm="3" align="middle"> |
| | | <el-avatar |
| | | :size="50" |
| | | src="" |
| | | ></el-avatar> |
| | | <div class="satisfaction"> |
| | | <template v-if="client.satisfactionScore"> |
| | | <i class="icon-star pam-icon icon--yellow satisfaction"></i> |
| | | <span>{{client.satisfactionScore}}</span> |
| | | </template> |
| | | <template v-else> |
| | | <div class="unfilled">未填滿意度</div> |
| | | </template> |
| | | </div> |
| | | |
| | | <div class="pl-10"> |
| | | <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> |
| | | </el-col> |
| | | <el-col :xs="14" :sm="14" class="pl-10"> |
| | | <div class="smTxt_bold name">{{client.name}}</div> |
| | | <div class="message">預約成功</div> |
| | | <div class="professionals"> |
| | | <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" |
| | |
| | | >(客戶未提供需求項目)</span> |
| | | </template> |
| | | </div> |
| | | </el-col> |
| | | <el-col class="flex-column contactInfo" :xs="4" :sm="6"> |
| | | <AppointmentProgress |
| | | :currentStep="client.communicateStatus" |
| | | ></AppointmentProgress> |
| | | </div> |
| | | </div> |
| | | <!-- <el-col :xs="5" :sm="3" align="middle"> |
| | | <el-avatar |
| | | :size="50" |
| | | ></el-avatar> |
| | | <div class="satisfaction" v-if="!hideReviews"> |
| | | <template v-if="client.satisfactionScore"> |
| | | <i class="icon-star pam-icon icon--yellow satisfaction"></i> |
| | | <span>{{client.satisfactionScore}}</span> |
| | | </template> |
| | | <template v-else> |
| | | <div class="unfilled">未填滿意度</div> |
| | | </template> |
| | | </div> |
| | | </el-col> --> |
| | | |
| | | <div class="flex-column contactInfo" :xs="4" :sm="6"> |
| | | |
| | | <div |
| | | class="smTxt_bold fix-chrome-click--issue" |
| | | :class="{'unread-txt': reservedTxt === '未讀', 'read-txt': reservedTxt !== '未讀'}" |
| | | >{{reservedTxt}}</div> |
| | | class="invite-msg smTxt_bold" |
| | | @click.stop="showAddInterviewDialog" |
| | | v-if="client.communicateStatus === contactStatus.RESERVED"> |
| | | 傳送約訪通知 |
| | | </div> |
| | | <div |
| | | class="date xsTxt text--mid_grey" |
| | | >{{date}}</div> |
| | | class="invite-msg smTxt_bold" |
| | | @click.stop="navigateToCloseAppointment" |
| | | v-else-if="client.communicateStatus === contactStatus.CONTACTED"> |
| | | 結案 |
| | | </div> |
| | | <div |
| | | class="invite-msg smTxt_bold" |
| | | @click.stop="inviteReview" |
| | | v-else-if="!client.satisfactionScore"> |
| | | 發送滿意度 |
| | | </div> |
| | | |
| | | <div |
| | | class="date xsTxt text--black" |
| | | >{{ date }}</div> |
| | | <div |
| | | class="xsTxt text--mid_grey" |
| | | >{{time}}</div> |
| | | </el-col> |
| | | >{{ time }}</div> |
| | | </div> |
| | | </el-row> |
| | | |
| | | <Ui-Dialog |
| | | :isVisible.sync="isVisibleDialog" |
| | | :width="width" |
| | | @closeDialog="close" |
| | | :isVisible.sync="isShowInformDialog" |
| | | :width="dialogWidth" |
| | | @closeDialog="closeInformDialog" |
| | | class="pam-myDemand-dialog" |
| | | > |
| | | <h5 class="subTitle text--center mb-30" |
| | | >{{isReserved ? '預約資訊' : '已聯絡資訊'}}</h5> |
| | | |
| | | <p v-if='isReserved' |
| | | class="smTxt text-right" |
| | | class="smTxt text--right" |
| | | ><span v-if="isRead">{{client.consultantReadTime | formatDate}}</span> 已讀</p> |
| | | <p |
| | | v-if="!isReserved" |
| | | class="smTxt text-right" |
| | | class="smTxt text--right" |
| | | >{{client.contactTime | formatDate}} 聯絡</p> |
| | | <p class="smTxt">{{client.appointmentDate | formatDate}} 預約</p> |
| | | |
| | |
| | | <p v-for="(item, index) in hopeContactTime" |
| | | :key="index" |
| | | >連絡時段{{index + 1 | formatNumber}}:<span>{{ item | formatHopeContactTime}}</span></p> |
| | | </div> |
| | | <div class="mt-30"> |
| | | <div class="memoTitleStyle"> |
| | | <div class="mdTxt">內容描述</div> |
| | | <div |
| | | class="smTxt text--bold text--primary cursor--pointer text--underline edit" |
| | | @click='editMemo' |
| | | >編輯</div> |
| | | </div> |
| | | <div class="mt-30 text--center" v-if="isReserved"> |
| | | <el-button @click="markAppointment">標註為已連絡</el-button> |
| | | |
| | | <el-input |
| | | class="mt-10 pam-appointment-textarea" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="100" |
| | | placeholder="請輸入,限100字。" |
| | | :disabled="!isEdit" |
| | | v-model="memo" |
| | | > |
| | | </el-input> |
| | | |
| | | <div class="mt-10 smTxt text--bold text--primary text--right fixed-Height"> |
| | | <template v-if="isEdit"> |
| | | <span class="cursor--pointer" @click="cancelEditMemo">取消</span> |
| | | <span class="pl-20 cursor--pointer" @click="saveMemo">儲存</span> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | <div class="mt-30 text--center" v-if="isReserved"> |
| | | <el-button @click="markAppointment">標註為已連絡</el-button> |
| | | </div> |
| | | </Ui-Dialog> |
| | | |
| | | <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> |
| | | |
| | | <script lang="ts"> |
| | | import { Vue, Component, Prop, Action } from 'nuxt-property-decorator'; |
| | | import { isMobileDevice } from '~/assets/ts/device'; |
| | | import { ClientInfo, markAsContact, recordRead } from '~/assets/ts/api/appointment'; |
| | | import { Vue, Component, Prop, 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 { ElRow } from 'element-ui/types/row'; |
| | | import { Appointment, AppointmentMemoInfo } from '~/shared/models/appointment.model'; |
| | | import { ContactStatus } from '~/shared/models/enum/contact-status'; |
| | | import reviewsService from '~/shared/services/reviews.service'; |
| | | |
| | | const appointmentStore = namespace('appointment.store'); |
| | | const localStorage = namespace('localStorage'); |
| | | |
| | | @Component({ |
| | | filters: { |
| | |
| | | } |
| | | }) |
| | | export default class ClientList extends Vue { |
| | | @Action updateMyAppointment!: (data: ClientInfo) => void |
| | | @Action storeConsultantList!: () => void; |
| | | @Prop() |
| | | client!: Appointment; |
| | | |
| | | @Prop() client!: ClientInfo; |
| | | isVisibleDialog = false; |
| | | width = ''; |
| | | @appointmentStore.Action |
| | | updateMyAppointmentList!: (data: Appointment) => void; |
| | | |
| | | @appointmentStore.Action |
| | | getAppointmentDetail!: (appointmentId: number) => Promise<Appointment>; |
| | | |
| | | @appointmentStore.Action |
| | | updateAppointmentDetail!: (id: number) => Appointment; |
| | | |
| | | @appointmentStore.Getter |
| | | appointmentProgress!: ContactStatus; |
| | | |
| | | @localStorage.Mutation |
| | | storageClearAppointmentIdFromMsg!: () => void; |
| | | |
| | | contactStatus = ContactStatus; |
| | | |
| | | dialogWidth = ''; |
| | | hideReviews = hideReviews; |
| | | isEdit = false; |
| | | isShowAddInterviewDialog = false; |
| | | isShowInformDialog = false; |
| | | isShowInviteReviewDialog = false; |
| | | memo = ''; |
| | | |
| | | memoInfo: AppointmentMemoInfo = { |
| | | appointmentId: 0, |
| | | content : '', |
| | | id : 0 |
| | | }; |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | @Watch('$route', {immediate: true}) |
| | | onRouteChange() { |
| | | const appointmentIdFromMsg = this.$route.query.appointmentId; |
| | | if (appointmentIdFromMsg && +appointmentIdFromMsg === this.client.id) { |
| | | this.openDetail(); |
| | | } |
| | | } |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | mounted() { |
| | | this.memoInfo = this.client.appointmentMemoList.length > 0 |
| | | ? JSON.parse(JSON.stringify(this.client.appointmentMemoList[0])) |
| | | : {appointmentId: 0, content: '', id: 0}; |
| | | this.memo = this.memoInfo.content; |
| | | } |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | viewAppointmentDetail(): void { |
| | | this.getAppointmentDetail(this.client.id).then((_) => { |
| | | const unread = !this.client.consultantReadTime; |
| | | if (unread) { |
| | | this.readAppointment(); |
| | | } |
| | | this.$router.push(`/appointment/${this.client.id}`); |
| | | }); |
| | | } |
| | | |
| | | showAddInterviewDialog(): void { |
| | | this.isShowAddInterviewDialog = true; |
| | | } |
| | | |
| | | navigateToCloseAppointment(): void { |
| | | this.getAppointmentDetail(this.client.id).then((_) => { |
| | | this.$router.push(`/appointment/${this.client.id}/close`); |
| | | }); |
| | | } |
| | | |
| | | inviteReview(): void { |
| | | reviewsService.sendSatisfactionToClient(this.client.id).then(res => { |
| | | this.isShowInviteReviewDialog = true ; |
| | | }) |
| | | } |
| | | |
| | | openDetail() { |
| | | setTimeout(() => { |
| | | (this.$refs.clientCardRef as any).$el.classList.add('currentShowStyle'); |
| | | }, 0) |
| | | this.dialogWidth = UtilsService.isMobileDevice() ? '80%' : ''; |
| | | this.isShowInformDialog = true; |
| | | } |
| | | |
| | | markAppointment() { |
| | | myConsultantService.markAsContact(this.client.id).then(data => { |
| | | this.updateMyAppointmentList(data); |
| | | this.isShowInformDialog = false; |
| | | }) |
| | | } |
| | | |
| | | closeInformDialog(): void { |
| | | this.readAppointment(); |
| | | this.isEdit = false; |
| | | this.clearAppointmentIdFromMsg(); |
| | | } |
| | | |
| | | private readAppointment(): void { |
| | | 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() { |
| | | this.storageClearAppointmentIdFromMsg(); |
| | | this.$router.push({query: {}}); |
| | | setTimeout(() => { |
| | | (this.$refs.clientCardRef as ElRow).$el.classList.remove('currentShowStyle') |
| | | },1000) |
| | | } |
| | | |
| | | saveMemo() { |
| | | if (this.client.appointmentMemoList.length > 0) { |
| | | const params = { |
| | | content: this.memo, |
| | | id: this.client.appointmentMemoList[0].id |
| | | }; |
| | | this.updateMemo(params); |
| | | return; |
| | | } |
| | | |
| | | const params = { |
| | | content: this.memo, |
| | | appointmentId: this.client.id, |
| | | } |
| | | this.createMemo(params); |
| | | } |
| | | |
| | | private createMemo(params) { |
| | | appointmentService.createMemo(params).then(memoRes => { |
| | | this.storeMemo(memoRes); |
| | | }); |
| | | } |
| | | |
| | | private updateMemo(params) { |
| | | appointmentService.updateMemo(params).then(memoRes => { |
| | | this.storeMemo(memoRes); |
| | | }); |
| | | } |
| | | |
| | | private storeMemo(memoRes) { |
| | | this.memoInfo = memoRes; |
| | | this.memo = this.memoInfo.content; |
| | | this.client.appointmentMemoList[0] = this.memoInfo; |
| | | this.isEdit = false; |
| | | } |
| | | |
| | | editMemo() { |
| | | this.isEdit = !this.isEdit; |
| | | this.memo = this.memoInfo.content; |
| | | } |
| | | |
| | | cancelEditMemo() { |
| | | this.isEdit = false; |
| | | 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(','); |
| | |
| | | if (this.client.gender) { |
| | | return this.client.gender === 'male' ? '男性' : '女性'; |
| | | } |
| | | return '' |
| | | return ''; |
| | | } |
| | | |
| | | get hopeContactTime() { |
| | |
| | | return contactList.filter(item => !!item && item !== ",") |
| | | } |
| | | |
| | | get isReserved() { |
| | | return this.client.communicateStatus === 'reserved'; |
| | | } |
| | | |
| | | get isRead() { |
| | | return !!this.client.consultantReadTime; |
| | | } |
| | | |
| | | get isView() { |
| | | return !!this.client.consultantViewTime; |
| | | } |
| | | |
| | | get reservedTxt() { |
| | | get reservedTxt(): string { |
| | | if (this.isReserved) { |
| | | return this.client.consultantReadTime ? '已讀' : '未讀'; |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | get latestUpdateTime() { |
| | | get displayTime(): string { |
| | | if (this.isReserved) { |
| | | return this.client.consultantReadTime ? this.client.consultantReadTime : this.client.appointmentDate; |
| | | return this.client.appointmentDate; |
| | | } else { |
| | | return this.client.contactTime; |
| | | return this.client.lastModifiedDate; |
| | | } |
| | | } |
| | | |
| | | get time() { |
| | | const formatDate = (this.$options.filters as any).formatDate(this.latestUpdateTime); |
| | | return formatDate.split(' ')[1] |
| | | } |
| | | |
| | | get date() { |
| | | const formatDate = (this.$options.filters as any).formatDate(this.latestUpdateTime); |
| | | const formatDate = (this.$options.filters as any).formatDate(this.displayTime); |
| | | return formatDate.split(' ')[0]; |
| | | } |
| | | |
| | | openDetail() { |
| | | this.width = isMobileDevice() ? '80%' : ''; |
| | | this.isVisibleDialog = true; |
| | | } |
| | | |
| | | markAppointment() { |
| | | markAsContact(this.client.id).then(data => { |
| | | // TODO: 要接後台傳回的 updated client 資料 - Ben 2021/11/16 |
| | | |
| | | const updatedClient = {...this.client}; |
| | | updatedClient.communicateStatus = 'contacted'; |
| | | updatedClient.contactTime = new Date(); |
| | | |
| | | this.updateMyAppointment(updatedClient); |
| | | this.isVisibleDialog = false; |
| | | |
| | | }) |
| | | } |
| | | |
| | | close() { |
| | | if (!this.client.consultantReadTime) { |
| | | recordRead(this.client.id).then(res => { |
| | | const updatedClient = {...this.client}; |
| | | updatedClient.consultantReadTime = new Date(); |
| | | |
| | | this.updateMyAppointment(updatedClient); |
| | | }); |
| | | } |
| | | get time() { |
| | | const formatDate = (this.$options.filters as any).formatDate(this.displayTime); |
| | | return formatDate.split(' ')[1] |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | <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; |
| | | } |
| | | .unread { |
| | | align-self: center; |
| | | |
| | | .circle { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50px; |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | .message { |
| | | margin:10px 0; |
| | | } |
| | | |
| | | .professionals { |
| | | overflow: hidden; |
| | | white-space: nowrap; |
| | | overflow : hidden; |
| | | text-overflow: ellipsis; |
| | | |
| | | display: -webkit-box; |
| | | -webkit-box-orient: vertical; |
| | | -webkit-line-clamp: 1; |
| | | .professionalsTxt { |
| | | font-size: 12px; |
| | | font-weight: bold; |
| | | font-size : 12px; |
| | | margin-right: 5px; |
| | | } |
| | | |
| | | |
| | | } |
| | | .noProfessionalsTxt { |
| | | color: $PRUDENTIAL_GREY; |
| | | color : $PRUDENTIAL_GREY; |
| | | font-weight: lighter; |
| | | } |
| | | } |
| | | |
| | | .contactInfo { |
| | | text-align: right; |
| | | .date { |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .unread-txt { |
| | | @extend .text--primary; |
| | | } |
| | | |
| | | .read-txt { |
| | | color: $SKY_BLUE; |
| | | } |
| | | |
| | | } |
| | | |
| | | .flex-column { |
| | | display: flex; |
| | | flex-direction: column; |
| | | display : flex; |
| | | flex-direction : column; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .dialogTxt { |
| | | font-size: 20px; |
| | | overflow-y:scroll; |
| | | max-height: 45vh; |
| | | font-size : 20px; |
| | | max-height: 25vh; |
| | | overflow-y: scroll; |
| | | @include desktop { |
| | | height: 400px; |
| | | } |
| | | } |
| | | |
| | | .text-right { |
| | | text-align: right; |
| | | .memoTitleStyle { |
| | | display : flex; |
| | | flex-direction : row; |
| | | justify-content: space-between; |
| | | .edit { |
| | | align-self: flex-end; |
| | | } |
| | | } |
| | | .fixed-Height { |
| | | height: 16px; |
| | | } |
| | | .test{ |
| | | display: flex; |
| | | } |
| | | .invite-msg{ |
| | | width: 96px; |
| | | color: $PRIMARY_RED; |
| | | @extend .text--underline; |
| | | } |
| | | .invite-review{ |
| | | align-items : center; |
| | | display : flex; |
| | | flex-direction: column; |
| | | } |
| | | </style> |