保誠-保戶業務員媒合平台
Tomas
2023-08-21 ccd7179b7f9f0c685e1dc51187a82d203922e928
PAMapp/pages/index.vue
@@ -1,188 +1,604 @@
<template>
    <div>
        <Ui-Carousel></Ui-Carousel>
        <div class="page-container">
            <h5 class="mdTxt mb-30">預約保險顧問</h5>
            <el-button
                class="reserveBtn recommendConsultant"
                @click="routerPush('/recommendConsultant')"
            >
                <p>嚴選配對</p>
            </el-button>
            <el-button
                class="reserveBtn quickFilter"
                @click="routerPush('/quickFilter')"
            >
                <p>快速篩選</p>
            </el-button>
            <el-row class="mb-30 rowStyle">
                <el-col :span="16">
                    <span class="mdTxt">我的顧問清單</span>
                    <span class="smTxt_bold amount">共 {{consultantList.length}} 筆</span>
                </el-col>
                <el-col
                    :span="8"
                    class="mdTxt readMore"
                    v-if="consultantList.length > 3"
                    @click.native="routerPush('/myConsultantList/consultantList')">查看更多</el-col>
            </el-row>
            <ConsultantList
                :agents="consultantList.slice(0, 3)"
                @removeAgent="removeAgent"
            ></ConsultantList>
            <div class='pam-recommend pb-30 pt-30'>
                <h5 class="mdTxt">推薦保險顧問</h5>
                <img class="absulate img" src="~/assets/images/index_recommend.svg" alt="">
            </div>
            <ConsultantSwiper :agents="recommendList"></ConsultantSwiper>
  <div>
    <Ui-Carousel></Ui-Carousel>
    <div class="page-container">
      <div>
        <h5 class="mdTxt">預約保險顧問</h5>
        <div class="mt-10 pam-reserveBtn--block">
          <el-button class="reserveBtn recommendConsultant"
            @click="navigateToRoute('/recommendConsultant')">
            <p>嚴選配對</p>
          </el-button>
          <el-button class="reserveBtn quickFilter"
            @click="navigateToRoute('/quickFilter')">
            <p>快速篩選</p>
          </el-button>
        </div>
      </div>
      <div class="pam-paragraph">
        <el-row class="rowStyle">
          <el-col :span="16">
            <span class="mdTxt">我的顧問清單</span>
            <span class="smTxt_bold amount">共 {{ consultantList.length }} 筆</span>
          </el-col>
          <el-col :span="8"
            class="mdTxt readMore fix-chrome-click--issue"
            v-if="consultantList.length > 3"
            @click.native="navigateToRoute('/myConsultantList/consultantList')">查看更多</el-col>
        </el-row>
        <ConsultantList class="mt-10"
          :agents="consultantList.slice(0, 3)"></ConsultantList>
      </div>
      <div class='pam-paragraph'>
        <div class="pam-recommend">
          <h5 class="mdTxt">推薦保險顧問</h5>
          <img class="img"
            src="~/assets/images/index_recommend.svg"
            alt="">
        </div>
        <ConsultantSwiper :agents="recommendList"></ConsultantSwiper>
      </div>
    </div>
    <Ui-Dialog
        :isVisible.sync="isShowAppointmentDialog"
        :width="appointmentDialogWidth"
        class="pam-myDemand-dialog pam-dialog-reserved"
      >
          <div v-if="appointmentDetail">
              <h5 class="subTitle text--center mb-30">預約成功</h5>
              <p class="smTxt">{{appointmentDetail.appointmentDate | formatDate}}</p>
              <div class="reserved-info">
                  <p>姓名:{{appointmentDetail.name}}</p>
                  <p>電話:{{appointmentDetail.phone}}</p>
                  <p>Email:{{appointmentDetail.email}}</p>
                  <p>性別:{{gender}}</p>
                  <p>年齡:{{appointmentDetail.age | toAgeLabel }}</p>
                  <p>職業:{{appointmentDetail.job}}</p>
                  <p>諮詢方式:{{appointmentDetail.consultationMethod | toConsultationMethod }}</p>
                  <p>需求:{{ appointmentDetail.requirement ? appointmentDetail.requirement.split(',').join('、') : '--'}}</p>
                  <p
                      v-for="(item, index) in hopeContactTime"
                      :key="index"
                  >連絡時段{{index + 1 | formatNumber}}:{{ item | formatHopeContactTime }}</p>
                  <div v-if="appointmentDetail.satisfactionScore">
                      <div class="mdTxt mt-10 mb-10">滿意度</div>
                      <el-rate
                      :value="appointmentDetail.satisfactionScore"
                      class="pam-myDemand-dialog__rate"
                      disabled>
                      </el-rate>
                  </div>
              </div>
              <div v-if="notScoreAppointmentYet" class="reserved-btn">
                  <el-button type="primary"
                      @click.native="reviewsBtn = true">給予滿意度評分</el-button>
              </div>
              <div v-if="appointmentDetail.communicateStatus === 'reserved'" class="text--center mt-10">
                  <el-button @click="isCancelPopup = true">取消預約</el-button>
                  <el-button @click="edit" type="primary">編輯</el-button>
              </div>
          </div>
      </Ui-Dialog>
      <PopUpFrame
        :isOpen.sync="isShowReAppointmentDialog"
        @closePopUp="removeUrlQueryParameter('notContactAppointmentId')"
      >
          <div class="pam-dialog-review">
              <div class="mt-30 text--middle" v-if="agentInfo">
                很抱歉!您預約的<span class="text--bold">{{ consultantName }}</span>顧問正忙碌中,請您取消預約並改選其他顧問
              </div>
                <el-row
                  type="flex"
                  class="mt-50"
                  justify="center">
                  <el-button
                      type="primary"
                      @click="reAppointment">取消預約再改選其他顧問</el-button>
                </el-row>
                <el-row
                  type="flex"
                  class="mt-20"
                  justify="center">
                  <el-button
                      class="outline_btn"
                      @click="cancelAppointment">取消預約</el-button>
                </el-row>
          </div>
      </PopUpFrame>
      <PopUpFrame
        :isOpen.sync="isShowReviewDialog"
        @closePopUp="removeUrlQueryParameter('appointmentId')"
      >
          <div class="mdTxt pam-dialog-review">
              保險顧問滿意度
              <span class="hint">選取星星</span>
              <div class="mt-30 review-content" v-if="agentInfo">
                  <UiAvatar :size="80" :agentNo="agentInfo.agentNo"></UiAvatar>
                  <div class="review-text">對於顧問
                      <span class="text--primary">{{agentInfo.name}}</span>
                      的整體服務,您給予幾顆星評價?
                  </div>
              </div>
              <div class="review-score">
                  <el-rate v-model="inputScore" class="pam-rate mt-30"></el-rate>
              </div>
              <div class="review-btn">
                  <el-button
                      type="primary"
                      :disabled="!inputScore"
                      @click="userReviewsConsultants">送出</el-button>
              </div>
          </div>
      </PopUpFrame>
    <div class="video-container"
         v-if="isShowFilmPlayer"
         style="position: fixed; bottom: 30px; right: 30px; z-index: 9999; display: flex; justify-content: flex-end;"
    >
      <iframe
        id="ytplayer" type="text/html" width="360" height="270"
        frameborder="0"
        allowfullscreen
        sandbox="allow-scripts allow-same-origin allow-popups allow-presentation fullscreen"
        :src="filmVideoSrc"
      ></iframe>
      <div class="close-btn" @click="closeVideo()">X</div>
    </div>
  </div>
</template>
<script lang="ts">
import { Vue, Component, State, Action } from 'nuxt-property-decorator';
import { getFavoriteFromStorage, setFavoriteToStorage } from '~/assets/ts/storageConsultant';
import { addFavoriteConsultant, Consultants, deleteConsultant } from '~/assets/ts/api/consultant';
import { login, getFavoriteConsultant } from '~/assets/ts/api/consultant';
import { isLogin } from '~/assets/ts/auth';
import {Action, Component, Getter, Mutation, namespace, State, Vue, Watch} from 'nuxt-property-decorator';
@Component({
import appointmentService from '~/shared/services/appointment.service';
import utilService, {AccessFroms} from '~/shared/services/utils.service';
import reviewsService from '~/shared/services/reviews.service';
import myConsultantService from '~/shared/services/my-consultant.service';
import {Appointment, AppointmentClosedInfo} from '~/shared/models/appointment.model';
import {Consultant} from '~/shared/models/consultant.model';
import {UserReviewParams} from '~/shared/models/reviews.model';
import {StrictQueryParams} from '~/shared/models/strict-query.model';
import {AgentInfo} from '~/shared/models/agent-info.model';
import {ContactStatus} from '~/shared/models/enum/contact-status';
import {SatisfactionType} from '~/shared/models/enum/satisfaction-type';
const localStorage = namespace('localStorage');
  const roleStorage = namespace('localStorage');
  @Component({
    layout: 'home'
})
export default class MainComponent extends Vue {
    consultantList: Consultants[] = [];
    agents: Consultants[] = [];
    @State('recommendList') recommendList!: Consultants[];
    @Action storeRecommendList!: any;
  })
  export default class MainComponent extends Vue {
    @State('recommendList')
    recommendList!: Consultant[];
    @State('myConsultantList')
    myConsultantList!: Consultant[];
    @roleStorage.Getter
    isAdminLogin!: boolean;
    @roleStorage.Getter
    isUserLogin!: boolean;
    @Action
    storeRecommendList!: any;
    @Action
    storeConsultantList!: any;
    @Mutation
    setAccessSource!: (accessSource: AccessFroms) => void;
    @localStorage.Mutation
    storageClearQuickFilter!: () => void;
    @localStorage.Mutation
    storageClearRecommendConsultant!: () => void;
    @localStorage.Getter
    currentSatisfactionIdFromMsg!: string;
    @localStorage.Getter
    currentNotContactAppointmentIdFromMsg!: string;
    @localStorage.Mutation
    storageClearSatisfactionIdFromMsg!: () => void;
    @localStorage.Mutation
    storageClearNotContactAppointmentIdFromMsg!: () => void;
    @localStorage.Mutation
    storageStrickQueryItem!: (strictQueryDto: StrictQueryParams) => void;
    consultantList: Consultant[] = [];
    appointmentDialogWidth    = '';
    inputScore                = 0;
    isShowAppointmentDialog   = false;
    isShowReAppointmentDialog = false;
    isShowReviewDialog        = false;
    consultantName = '';
    contactStatus = ContactStatus;
    appointmentDetail: Appointment = {
      age               : '',
      agentNo           : '',
      appointmentClosedInfo: {} as AppointmentClosedInfo,
      appointmentDate   : '',
      appointmentMemoList: [],
      appointmentNoticeLogs: [],
      communicateStatus: this.contactStatus.PICKED,
      consultantReadTime: '',
      consultantViewTime: '',
      contactTime       : '',
      contactType       : '',
      customerId        : 0,
      email             : '',
      gender            : '',
      hopeContactTime   : '',
      interviewRecordDTOs: [],
      id                : 0,
      job               : '',
      lastModifiedDate  : '',
      name              : '',
      otherRequirement  : '',
      phone             : '',
      requirement       : '',
      satisfactionScore : 0,
      consultationMethod    : ''
    };
    agentInfo: Consultant = {
      agentNo            : '',
      name               : '',
      img                : '',
      expertise          : [],
      avgScore           : 0,
      contactStatus      : '',
      createTime         : '',
      updateTime         : '',
      customerViewTime   : '',
      role               : '',
      seniority          : '',
      appointments       : []
    };
    isCloseVideo = false;
    @Getter
    fromAccess!: AccessFroms;
    get filmVideoSrc() {
      if (!this.fromAccess) return '';
      return this.fromAccess === AccessFroms.FILM_1
        ? 'https://www.youtube.com/embed/zVBLW9hWD0g?autoplay=1&mute=1&loop=1&playlist=zVBLW9hWD0g'
        : 'https://www.youtube.com/embed/655JnwbuRGA?autoplay=1&mute=1&loop=1&playlist=655JnwbuRGA';
    }
    get isShowFilmPlayer() {
      return !!this.fromAccess && !this.isCloseVideo;
    }
    //////////////////////////////////////////////////////////////////////
    mounted() {
        if (!this.recommendList) {
            this.storeRecommendList();
      if (this.$route.query.from) {
        const fromSource = this.$route.query.from as AccessFroms;
        this.setAccessSource(fromSource);
        utilService.insertAccessFrom(fromSource);
      }
      if (this.isAdminLogin) {
        this.$router.push('/myAppointmentList/appointmentList');
      } else {
        if (!this.recommendList?.length) {
          this.storeRecommendList();
        }
        if (isLogin()) {
            this.addFavoriteFromStorageToApi();
            getFavoriteConsultant().then((response) => this.consultantList = response.data);
        } else {
            this.consultantList = getFavoriteFromStorage();
        this.storeConsultantList();
        this.storageClearQuickFilter();
        this.storageClearRecommendConsultant();
        if (this.isUserLogin) {
          appointmentService.getNotContactAppointment().then((appointment) => {
            if (appointment) {
              this.$router.push({ query: { notContactAppointmentId: appointment.id + ''}});
              this.autoOpenAppointmentBy('askReAppointment', appointment.id);
            }
          });
        }
      }
    }
    addFavoriteFromStorageToApi() {
        const agentNoList = getFavoriteFromStorage().map(i => i.agentNo)
        if (agentNoList.length > 0) {
            addFavoriteConsultant(agentNoList).then(res => res);
            localStorage.removeItem('favoriteConsultant');
    destroyed() {
      this.removeUrlQueryParameter();
    }
    //////////////////////////////////////////////////////////////////////
    @Watch('myConsultantList')
    onMyConsultantListChange() {
      this.consultantList = (this.myConsultantList || [])
        .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)}))
        .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate);
      if (this.currentNotContactAppointmentIdFromMsg) {
        this.autoOpenAppointmentBy('askReAppointment', +this.currentNotContactAppointmentIdFromMsg);
        return;
      }
      if (this.currentSatisfactionIdFromMsg) {
        this.autoOpenAppointmentBy('inviteReviewConsultant',+this.currentSatisfactionIdFromMsg);
        this.storageClearSatisfactionIdFromMsg();
        return;
      }
    }
    private autoOpenAppointmentBy(reason: string, targetAppointmentId: number): void {
        const setAgentInfo = new Promise((resolve, reject) => {
          this.agentInfo = this.myConsultantList.filter(item => {
            const appointmentIndex = item.appointments?.findIndex(i => i.id === targetAppointmentId);
            return appointmentIndex !== undefined && appointmentIndex > -1;
          })[0];
          if (this.agentInfo) {
            myConsultantService.getConsultantDetail(this.agentInfo.agentNo).then((res) => resolve(res));
          }
        });
        const setAppointment = new Promise((resolve, reject) => {
           appointmentService.getAppointmentDetail(targetAppointmentId).then((res) => resolve(res));
        });
        Promise.all([setAgentInfo, setAppointment]).then((values) => {
          const agentInfo = values[0] as AgentInfo;
          const appointmentInfo = values[1] as Appointment;
          this.consultantName = agentInfo.name;
          this.appointmentDetail = appointmentInfo;
          this.appointmentDialogWidth = utilService.isMobileDevice() ? '80%' : '';
          this.isShowAppointmentDialog = true;
          switch (reason) {
            case 'inviteReviewConsultant':
              if (this.notScoreAppointmentYet) {
                setTimeout(() => {
                  this.isShowReviewDialog = true;
                }, 500);
              }
              break;
            case 'askReAppointment':
              setTimeout(() => {
                this.isShowReAppointmentDialog = true;
              }, 500);
              break;
          }
        });
    }
    //////////////////////////////////////////////////////////////////////
    navigateToRoute(path: string): void {
      this.$router.push(path);
    }
    edit() {
        this.isShowAppointmentDialog = false;
        this.$router.push({path: `/questionnaire/${this.agentInfo.agentNo}`, query: {'edit': 'true'}});
    }
    reAppointment(): void {
      appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => {
        const requirements = this.appointmentDetail && this.appointmentDetail.requirement
                            ? this.appointmentDetail.requirement.split(',')
                            : [];
        this.storeConsultantList();
        this.storageStrickQueryItem({ requirements: requirements });
        this.storageClearNotContactAppointmentIdFromMsg();
        this.removeUrlQueryParameter('notContactAppointmentId');
        this.$router.push('/recommendConsultant');
      });
    }
    cancelAppointment(): void {
      appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => {
        this.storeConsultantList();
        this.storageClearNotContactAppointmentIdFromMsg();
        this.removeUrlQueryParameter('notContactAppointmentId');
        this.isShowReAppointmentDialog = false;
        this.isShowAppointmentDialog = false;
        this.$router.push('/');
      });
    }
    userReviewsConsultants() {
      const reviewParams: UserReviewParams = {
            appointmentId: this.appointmentDetail.id,
            score: this.inputScore,
            type: SatisfactionType.APPOINTMENT
        }
        this.appointmentDetail.satisfactionScore = this.inputScore;
        reviewsService.userReviewsConsultants(reviewParams).then((res) => {
            this.isShowReviewDialog = false;
        });
    }
    routerPush(path: string) {
        this.$router.push(path);
    }
    removeAgent(agentNo: string) {
        if (!isLogin()) {
            const findIndex = this.consultantList.findIndex((item, i) => {
                return item.agentNo === agentNo;
            })
            this.consultantList.splice(findIndex, 1);
            setFavoriteToStorage(this.consultantList)
        } else {
            deleteConsultant(agentNo).then(res => this.$router.go(0))
    removeUrlQueryParameter(targetKey?: string): void {
        // NOTE: 刪除特定的 query parameter [Tomas, 2022/1/24 11:36]
        // [REF] How to remove a parameter from this.$router.query Nuxt.js? https://reurl.cc/X45aMD
        let newRouteQuery = {};
        if (targetKey) {
          Object.keys(this.$route.query).forEach((key) => {
            if (key !== targetKey) {
              newRouteQuery[key] = this.$route.query[key]
            }
          })
        }
        this.$router.push(newRouteQuery);
        this.storageClearSatisfactionIdFromMsg();
        this.storageClearNotContactAppointmentIdFromMsg();
    }
}
    closeVideo(): void {
      this.isCloseVideo = true;
    }
    ///////////////////////////////////////////////////////////////////////////////
    get gender(): string {
        if (this.appointmentDetail.gender) {
            return this.appointmentDetail.gender === 'male' ? '男性' : '女性';
        }
        return ''
    }
    get hopeContactTime(): string[] {
        const contactList = this.appointmentDetail.hopeContactTime
            .split("'").map((item: any) => item.slice(0, item.length));
        return contactList.filter((item: any) => !!item && item !== ",")
    }
    get notScoreAppointmentYet(): boolean {
      if (this.appointmentDetail.communicateStatus === 'closed' || this.appointmentDetail.communicateStatus === 'done') {
        return !this.appointmentDetail.satisfactionScore;
      };
      return false;
    }
  }
</script>
<style lang="scss" scoped>
  .page-container {
    margin: 0px 20px 30px 20px;
  }
  .reserveBtn.el-button--default {
    width              : 100%;
    height             : 110px;
    border-radius      : 10px;
    margin-bottom      : 15px;
    font-size          : 32px;
    font-weight        : 700;
    background-position: right;
    background-size    : cover;
    color              : #68737A;
    text-align         : left;
    background-repeat  : no-repeat;
    box-shadow         : 0px 0px 6px #a79b9b29;
    border-width       : 0;
    p {
      text-shadow: 1px 1px 5px $PRIMARY_WHITE;
    }
  }
  .el-button+.el-button {
    margin-left: 0px;
  }
  .quickFilter {
    background-image: url('~/assets/images/quickFilter/banner_mob.svg');
  }
  .recommendConsultant {
    background-image: url('~/assets/images/recommendConsultant/banner_mob.svg');
  }
  .rowStyle {
    .amount {
      color: $PRUDENTIAL_GREY;
    }
    .readMore {
      color: $PRIMARY_RED;
      cursor: pointer;
      text-align: right;
    }
  }
  .pam-recommend {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  @media (min-width: 576px) and (max-width: 767px) {
    .quickFilter.el-button--default {
      background-image: url('~/assets/images/quickFilter/banner_web.svg');
    }
    .recommendConsultant.el-button--default {
      background-image: url('~/assets/images/recommendConsultant/banner_web.svg');
    }
  }
  @include desktop {
    .page-container {
        padding: 0px 20px 30px 20px;
      width: 700px;
      margin: 0 auto;
    }
    .reserveBtn.el-button--default {
        width: 100%;
        height: 110px;
        border-radius: 10px;
        margin: 0 auto 17px auto;
        font-size: 32px;
        font-weight: 700;
        background-position: right;
        background-size: cover;
        color: #68737A;
        text-align: left;
        background-repeat: no-repeat;
        box-shadow: 0px 0px 6px #a79b9b29;
        border-width: 0;
      .landing-container {
    border: 2px solid blue;
    border-radius: 10px;
    padding: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
        p {
            text-shadow: 1px 1px 5px $PRIMARY_WHITE;
        }
        &:nth-child(3) {
            margin-bottom: 42px;
        }
    .pam-reserveBtn--block {
      display: flex;
      justify-content: space-between;
    }
    .reserveBtn+.reserveBtn {
        margin-left: 0px;
    .reserveBtn {
      max-width: 335px;
    }
  }
    .quickFilter {
        background-image: url('~/assets/images/quickFilter/banner_mob.svg');
    }
  /* 確保 .video-container 有相對/絕對定位或固定寬度 */
  .video-container {
    position: relative;
    width: auto; /* 根據你的影片尺寸調整 */
  }
    .recommendConsultant {
        background-image: url('~/assets/images/recommendConsultant/banner_mob.svg');
    }
  /* 關閉按鈕樣式 */
  .close-btn {
    position: absolute;
    top: -10px;
    right: -10px;
    cursor: pointer;
    font-size: 20px;
    color: #fff;
    background-color: #000;
    width: 30px;
    height: 30px;
    text-align: center;
    line-height: 30px;
    border-radius: 50%;
    opacity: 0.7;
  }
    @media (min-width: 576px) and (max-width: 1023px) {
        .quickFilter.el-button--default {
            background-image: url('~/assets/images/quickFilter/banner_web.svg');
        }
        .recommendConsultant.el-button--default {
            background-image: url('~/assets/images/recommendConsultant/banner_web.svg');
        }
    }
    .rowStyle {
        .amount {
            color: $PRUDENTIAL_GREY;
        }
        .readMore {
            color: $PRIMARY_RED;
            cursor: pointer;
            text-align: right;
        }
    }
    @include desktop {
        .page-container {
            width: 700px;
            margin: 0 auto;
        }
        .reserveBtn {
            max-width: 340px;
            &:nth-child(2) {
                margin-right: 15px;
            }
        }
    }
    .pam-recommend {
        position: relative;
        .img {
            position: absolute;
            right: 20px;
            bottom: 0px;
        }
    }
  .close-btn:hover {
    opacity: 1;
  }
</style>