保誠-保戶業務員媒合平台
Jack
2022-01-18 dc0e7366e96ce773ae1690f6db28e03a684bb45e
Merge branch 'Phase3' of ssh://dev.pollex.com.tw:29418/pcalife/PAM into Phase3
修改16個檔案
新增2個檔案
1121 ■■■■ 已變更過的檔案
PAMapp/assets/scss/vendors/elementUI/_dateTimePicker.scss 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentInterviewList.vue 85 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/BackActionBar.vue 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Client/ClientCard.vue 249 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/DateTimePicker.vue 15 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewAdd.vue 234 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewMsg.vue 50 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/interviewNotification.vue 76 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiDatePicker.vue 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiTimePicker.vue 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/close/index.vue 54 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/index.vue 48 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointmentAgenda/index.vue 78 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/closedList.vue 75 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/appointment.model.ts 53 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/appointment.service.ts 24 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/index.ts 33 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
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 => {