保誠-保戶業務員媒合平台
Jack
2022-01-24 f8ab133a7dc20562c25a092a402266f5e7b0b296
Merge branch 'Phase3' of ssh://dev.pollex.com.tw:29418/pcalife/PAM into Phase3
修改46個檔案
新增10個檔案
1558 ■■■■ 已變更過的檔案
PAMapp/assets/icon/style.css 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/appointment/avatar_bg.svg 9 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/_common.scss 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/_variable.scss 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_heading.scss 2 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentInterviewList.vue 13 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentProgress.vue 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/DateTimePicker.vue 16 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewAdd.vue 39 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewCard.vue 101 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewMsg.vue 31 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewRecordCard.vue 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/interviewNotification.vue 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/NavBar.vue 48 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/ReviewRecords/ReviewRecords.vue 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiDatePicker.vue 20 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiTimePicker.vue 69 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/_agentNo.vue 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/close/index.vue 7 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/index.vue 37 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/interview/new/index.vue 9 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointmentAgenda/index.vue 92 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/closedList.vue 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList.vue 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/notification/index.vue 43 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/_agentNo.vue 29 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/index.vue 15 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/satisfactionList.vue 53 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviewsRecord/index.vue 13 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/reviews.model.ts 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/reviews.service.ts 7 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/index.ts 33 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220122_w.sql 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/客戶取得最新預約的未處理預約單.txt 61 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/標記為已聯絡API.txt 67 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/顧問取得所有自己的預約單API.txt 89 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/顧問取得未處理預約單數量通知.txt 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/config/Constants.java 19 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentCustomerViewRepository.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/SatisfactionRepository.java 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java 58 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ConsultantService.java 29 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/PersonalNotificationService.java 24 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/SatisfactionService.java 14 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java 158 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java 40 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/NotFoundExpiringAppointmentException.java 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-dev.yml 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages.properties 8 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages_zh_TW.properties 9 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/style.css
@@ -76,8 +76,8 @@
.icon-circle-right:before {
  content: "\e90b";
}
.icon-down:before, .el-icon-arrow-up:before {
  content: "\e910";
.icon-close:before {
  content: "\e90c";
}
.icon-comment:before {
  content: "\e90d";
PAMapp/assets/images/appointment/avatar_bg.svg
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,9 @@
<svg id="Component_29" data-name="Component 29" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
  <circle id="Ellipse_726" data-name="Ellipse 726" cx="50" cy="50" r="50" fill="#feecdc"/>
  <g id="Group_3685" data-name="Group 3685" transform="translate(-4269 -1812)">
    <circle id="Ellipse_723" data-name="Ellipse 723" cx="50" cy="50" r="50" transform="translate(4269 1812)" fill="#feecdc"/>
    <path id="Intersection_4" data-name="Intersection 4" d="M8376.456,8861.851v-2.594c0-14.428,24.235-18.738,38.724-18.738s38.367,4.313,38.367,18.738v2.59A49.9,49.9,0,0,1,8415.013,8880H8415A49.892,49.892,0,0,1,8376.456,8861.851Zm17.021-48.454a21.7,21.7,0,0,1,21.7-21.686h.014a21.693,21.693,0,1,1-21.716,21.686Z" transform="translate(-4095.456 -6968)" fill="#d0d0ce" opacity="0.789"/>
    <circle id="Ellipse_724" data-name="Ellipse 724" cx="15" cy="15" r="15" transform="translate(4334 1882)" fill="none"/>
    <path id="female" d="M14.817,13.279l1.116,1.11c.457.457.92.914,1.377,1.383a.856.856,0,0,1-1.235,1.187c-.665-.653-1.324-1.318-1.983-1.977l-.516-.522a.938.938,0,0,1-.107.16l-2.339,2.339a.837.837,0,0,1-.867.231.813.813,0,0,1-.594-.641.843.843,0,0,1,.261-.819l2.321-2.315a.771.771,0,0,1,.125-.095l-1.395-1.4a5.9,5.9,0,0,1-5.028.968,5.734,5.734,0,0,1-3.383-2.333,5.936,5.936,0,1,1,9.669.107l1.431,1.4c.071-.083.142-.184.231-.273.736-.736,1.466-1.472,2.208-2.2A.859.859,0,1,1,17.3,10.822l-1.6,1.6C15.4,12.7,15.114,12.965,14.817,13.279ZM11.683,7.124a4.244,4.244,0,1,0-4.256,4.232A4.244,4.244,0,0,0,11.683,7.124Z" transform="matrix(0.719, 0.695, -0.695, 0.719, 4348.543, 1882.728)" fill="none"/>
  </g>
</svg>
PAMapp/assets/scss/_common.scss
@@ -91,4 +91,48 @@
        line-height: 40px;
        padding-right: 10px;
    }
}
}
.remind-container{
  margin-top: 13px;
  margin-bottom: 20px;
  display: flex;
      .remind-date{
          display: flex;
          flex-direction: column;
          align-items: center;
          font-weight: bold;
          width: 70px;
          border-radius: 6px;
          border-bottom: 1px solid #CCCCCC;
          border-right: 1px solid #CCCCCC;
          border-left: 1px solid #CCCCCC;
      }
      .remind-content-txt{
        display: flex;
        flex-direction: column;
        border: 1px solid #CCCCCC;
        flex:1;
        border-radius: 5px;
        padding: 10px;
      }
      .mb-3{
        margin-bottom: 3px;
      }
      .mt-2{
        margin-top:2px;
      }
      .date-year{
        color: #fff;
        align-items: center;
        display: flex;
        justify-content: center;
      }
      .bgc-primary-red{
        background-color:$PRIMARY_RED;
        width: 70.5px;
        border-top-left-radius:6px;
        border-top-right-radius:6px;
        border: 1px solid #CCCCCC;
      }
}
PAMapp/assets/scss/_variable.scss
@@ -10,6 +10,7 @@
$SKY_BLUE: #009CBD;
$LIGHT_BLUE: #8DB9CA;
$DARK_BLUE: #1B365D;
$LIGHT_RED: #DA3738;
$BEIGE: #A89968;
$PRUDENTIAL_GREY: #68737A;
$LIGHT_GREY: #D0D0CE;
PAMapp/assets/scss/utilities/_heading.scss
@@ -125,7 +125,6 @@
  @extend .text--bold;
  @extend .text--primary;
  @extend .cursor--pointer;
  @extend .text--underline;
}
.pam-link-button--lg {
@@ -134,5 +133,4 @@
  @extend .text--bold;
  @extend .text--primary;
  @extend .cursor--pointer;
  @extend .text--underline;
}
PAMapp/components/Appointment/AppointmentInterviewList.vue
@@ -2,13 +2,16 @@
    <div>
        <div class="interview__header">
            <div class="mdTxt">約訪紀錄</div>
            <div class="pam-link-button--lg"
            <div class="pam-link-button"
            @click="addInterview">+新增</div>
        </div>
        <InterviewCard :interviewList="displayList.slice(0, 3)"></InterviewCard>
        <section class="text--right mt-30" v-if="interviewList.length > 3">
                <div class="pam-link-button--lg" @click="readMoreBtn">展開看更多</div>
        <section class="text--right mt-30 interview-check-more" v-if="interviewList.length > 3">
                <div class="pam-link-button" @click="readMoreBtn">
                  å±•開看更多
                  <i class="icon-expand"></i>
                  </div>
        </section>
    </div>
</template>
@@ -61,4 +64,8 @@
  justify-content: space-between;
  margin-bottom  : 10px;
}
.interview-check-more{
  display: flex;
  justify-content: center;
}
</style>
PAMapp/components/Appointment/AppointmentProgress.vue
@@ -96,14 +96,14 @@
    position       : relative;
    .circle {
      background-color: white;
      border          : 1px solid #707070;
      border          : 1px solid #CCCCCC;
      border-radius   : 50%;
      height          : 8px;
      margin          : 0;
      width           : 8px;
      z-index         : 5;
      &.activate {
        background-color: $PRIMARY_BLACK;
        background-color: $BEIGE;
      }
    }
    .line {
PAMapp/components/DateTimePicker.vue
@@ -4,25 +4,31 @@
    <div class="dateTime">
        <UiDatePicker
            @changeDate="changeDateTime($event, 'date')"
            :isPastDateDisabled="isPastDateDisabled"
            :defaultValue="defaultValue"
        ></UiDatePicker>
        <UiTimePicker
            @changeTime="changeDateTime($event, 'time')"
            :defaultValue="defaultValue"
            :isPastDateDisabled="isPastDateDisabled"
            :changeDate="changeDate"
        ></UiTimePicker>
    </div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue } from "nuxt-property-decorator";
import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator";
@Component
export default class DateTimePicker extends Vue {
    changeDate!: Date;
    changeDate: Date | string = '';
    changeTime!: string;
    @Prop()
    defaultValue!: string;
    @Prop()
    isPastDateDisabled!: boolean;
    @Emit('changeDateTime')
    changeDateTime(event, type) {
@@ -33,12 +39,14 @@
            this.changeTime = event;
        }
        if (this.changeDate && this.changeTime) {
            const timeArray = this.changeTime.split(':');
            const interViewTime = this.changeDate.setHours(+timeArray[0], +timeArray[1]);
            const hour = this.changeTime.split(':')[0];
            const minute = this.changeTime.split(':')[1];
            const interViewTime = new Date(this.changeDate).setHours(+hour, +minute);
            return new Date(interViewTime);
        }
        return '';
    }
}
</script>
PAMapp/components/Interview/InterviewAdd.vue
@@ -5,18 +5,20 @@
          <span>{{interviewRecord.lastModifiedDate | formatDate}} æ›´æ–°</span>
      </div>
      <el-row class="mdTxt mb-10">
          <el-col :xs="16" :sm="20" class="required">約訪時間</el-col>
          <el-col :xs="16" :sm="20">
            <span :class="{'required': !interviewId || isEdit}">約訪時間</span>
          </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"
                class="mr-10 text--primary  cursor--pointer"
                @click="showCancelPopUp = true"
              >刪除</span>
              ><i class="icon-delet"></i></span>
              <span
                v-if="!isEdit"
                class="text--primary text--underline cursor--pointer"
                class="text--primary  cursor--pointer"
                @click="isEdit = !isEdit"
              >編輯</span>
              ><i class="icon-edit"></i></span>
          </el-col>
      </el-row>
      <template v-if="!interviewId || isEdit">
@@ -26,12 +28,12 @@
          ></DateTimePicker>
      </template>
      <template v-else>
          <div class="mdTxt lighter mt-20">
          <div class="fs-20 mt-20">
              {{formatInterviewDate}}
          </div>
      </template>
      <div class="mdTxt mb-10 mt-30">約訪紀錄</div>
      <div class="mdTxt mb-10 mt-30" :class="{'required': !interviewId || isEdit}">約訪紀錄</div>
      <template v-if="!interviewId || isEdit">
          <el-input
            type="textarea"
@@ -43,7 +45,7 @@
        </el-input>
      </template>
      <template v-else>
          <div class="mdTxt lighter mt-20">
          <div class="fs-20 mt-20">
              {{content}}
          </div>
      </template>
@@ -86,6 +88,7 @@
      <InterviewMsg
        :isVisible.sync="showInterviewMsgPopup"
        :client="appointmentDetail"
        :defaultValue="interviewTime"
        @closeDialog="closePopup"
      ></InterviewMsg>
  </div>
@@ -249,17 +252,15 @@
    }
}
.required {
    position: relative;
    transform: translateX(10px);
      position: relative;
    &::before {
      content: '*';
      font-size: 15px;
      font-weight: bold;
      position: absolute;
      color: #FF0000;
      transform: translateX(-10px);
      z-index: 5;
      &::before {
        content: '*';
        position: absolute;
        color: #FF0000;
        transform: translate(-12px, 0);
        z-index: 5;
      }
    }
}
</style>
PAMapp/components/Interview/InterviewCard.vue
@@ -1,35 +1,46 @@
<template>
    <div>
       <template v-if="!interviewList.length">
          <div class="record-card record-card--empty">
      <template v-if="!interviewList.length">
          <span class="record-card record-card--empty" style="display:flex">
            ç„¡ç´„訪紀錄
          </div>
          </span>
      </template>
      <template v-else>
        <div class="interview--future">
            <div class="record-card mb-10"
          <div class="record-card mb-10"
                v-for="(item, index) in futureList"
                :key="index + 'feature'"
                @click="editInterview(item)"
            >
                <div class="record-card-date">
                    <div>
                        <UiDateFormat
          >
            <div class="remind-container">
              <div class="remind-date mr-10">
                <div class="mb-3 smTxt bgc-primary-red date-year">
                  <div class="mb-3 mt-2">
                    <UiDateFormat
                            class="date bold"
                            :date="item.interviewDate"
                            onlyShowSection="DAY" />
                    </div>
                    <div>
                        <UiDateFormat
                            class="time mt-5 line-space"
                            onlyShowSection="YEAR" />
                  </div>
                </div>
              <div class="p mt-5">
                <UiDateFormat
                            class="date bold"
                            :date="item.interviewDate"
                            onlyShowSection="DATE" />
              </div>
            </div>
            <div class="remind-content-txt">
              <div style="display:flex">
                <UiDateFormat
                            class="date bold "
                            :date="item.interviewDate"
                            onlyShowSection="TIME" />
                    </div>
                </div>
                <div class="record-card-content">
                    <span>{{item.content}}</span>
                </div>
              </div>
                <div class="interview-card-content">{{item.content}}</div>
            </div>
          </div>
            </div>
        </div>
@@ -39,23 +50,34 @@
                :key="index + 'past'"
                @click="editInterview(item)"
            >
                <div class="record-card-date">
                    <div>
                        <UiDateFormat
                            class="date bold"
              <div class="remind-container">
                <div class="remind-date mr-10">
                  <div class="mb-3 smTxt bgc-primary-red date-year">
                    <div class="mb-3 mt-2">
                      <UiDateFormat
                            class="bold date"
                            :date="item.interviewDate"
                            onlyShowSection="DAY" />
                            onlyShowSection="YEAR" />
                    </div>
                    <div>
                        <UiDateFormat
                            class="time mt-5 line-space"
                    </div>
                  <div class="p mt-5">
                    <UiDateFormat
                            class="mt-5 line-space time"
                            :date="item.interviewDate"
                            onlyShowSection="DATE" />
                  </div>
                </div>
                <div class="remind-content-txt">
                  <div style="display:flex">
                    <UiDateFormat
                            class="mt-5 line-space mb-3 time"
                            :date="item.interviewDate"
                            onlyShowSection="TIME" />
                    </div>
                  </div>
                    <div class="interview-card-content">{{item.content}}</div>
                </div>
                <div class="record-card-content">
                    <span>{{item.content}}</span>
                </div>
              </div>
            </div>
        </section>
      </template>
@@ -109,16 +131,14 @@
    }
}
.interview--past {
    margin-top: 10px;
    border-top: 1px solid #CCCCCC;
    padding-top: 17px;
    margin-top: 17px;
}
.record-card {
    height: 62px;
    border: 1px solid #707070;
    border-radius: 5px;
    display: flex;
    border-bottom: 1px solid #000;
    height: 64px;
    margin-bottom: 20px;
    .record-card-date{
        display: flex;
        flex-direction: column;
@@ -126,11 +146,13 @@
        margin-right: 10px;
        margin-top: 10px;
    }
    .record-card-content{
        height: 42px;
        margin-top: 10px;
        margin-right: 10px;
        line-height: 1.2;
    }
  &.record-card--empty {
    align-items     : center;
@@ -139,6 +161,15 @@
    justify-content : center;
  }
}
.interview-card-content{
 overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-box-orient: vertical;
        word-break: break-all;
        word-wrap: break-word;
        -webkit-line-clamp: 2;
}
.line-space{
    letter-spacing: 1px;
}
PAMapp/components/Interview/InterviewMsg.vue
@@ -20,9 +20,11 @@
        </el-input>
      <div v-if="client.phone">
        <div class="mdTxt mt-30 mb-10">預計約訪時段</div>
        <div class="mdTxt mt-30 mb-10 required">預計約訪時段</div>
        <DateTimePicker
          @changeDateTime="interviewTime = $event"
          :isPastDateDisabled="true"
          :defaultValue="defaultValue"
        ></DateTimePicker>
      </div>
@@ -33,7 +35,8 @@
        </el-dialog>
        <PopUpFrame
        :isOpen.sync="isShowSuccessAlert">
          :isOpen.sync="isShowSuccessAlert"
          @closePopUp="closeAllDialog">
        <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>
@@ -42,7 +45,7 @@
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, PropSync, Emit, Action, namespace } from 'nuxt-property-decorator';
import { Vue, Component, Prop, PropSync, Emit, namespace } from 'nuxt-property-decorator';
import appointmentService from '~/shared/services/appointment.service';
import { Appointment, ToInformAppointment } from '~/shared/models/appointment.model';
@@ -54,11 +57,11 @@
@Component
export default class InterviewMsg extends Vue {
    @Action
    storeMyAppointmentList!: () => Promise<number>;
    @appointmentStore.Action
    updateAppointmentDetail!: (id: number) => Appointment;
    @appointmentStore.Action
    getMyAppointmentList!: () => Promise<Appointment[]>;
    @PropSync('isVisible')
    dialogVisible!: boolean;
@@ -68,6 +71,9 @@
    @Prop()
    client!: Appointment;
    @Prop()
    defaultValue!: string;
    @Emit('closeDialog')
    closeDialog() {
@@ -97,14 +103,14 @@
      };
      appointmentService.informAppointment(appointmentInformation).then((_) => {
        this.isShowSuccessAlert = true ;
        this.updateAppointmentDetail(this.client.id);
      });
    }
    closeAllDialog() {
      this.isShowSuccessAlert = false ;
      this.dialogVisible = false;
      this.storeMyAppointmentList();
      this.updateAppointmentDetail(this.client.id);
      this.getMyAppointmentList();
    }
    get isBtnDisabled() :Boolean {
@@ -118,6 +124,15 @@
<style lang="scss" >
.interview-msg-component{
  .required {
  position: relative;
  &::before {
      content: '*';
      position: absolute;
      color: #FF0000;
      transform: translate(-12px, 0);
    }
  }
  .msg-dialog-title{
    display: flex;
    justify-content: center;
PAMapp/components/Interview/InterviewRecordCard.vue
@@ -33,7 +33,7 @@
                                <span v-else-if="item.email">(Email)</span>
                                <span v-else>(手機簡訊)</span>
                            </div>
                            <div class="mt-10">預約{{item.interviewDate | formatDate}}</div>
                            <div v-if="item.phone" class="mt-10">預約{{item.interviewDate | formatDate}}</div>
                        </div>
                </section>
                <div class="time-line"></div>
@@ -70,6 +70,8 @@
                justify-content: center;
                align-items: center;
                align-content: center;
                background-color:#1B365D;
                color: #fff;
            }
        }
    }
@@ -93,4 +95,4 @@
    margin-left: 13px;
    margin-top: 10px;
}
</style>
</style>
PAMapp/components/Interview/interviewNotification.vue
@@ -4,17 +4,22 @@
            <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 class="remind-date mr-10">
              <div class="mb-3 smTxt bgc-primary-red date-year">
                <div class="mb-3 mt-2">2021</div>
                </div>
                <div class="remind-content-txt">
                  <div class="txt-margin">09:00</div>
                  <div>約訪林志零</div>
                </div>
              <div class="p mt-5">
                <div>12/25</div>
              </div>
            </div>
            <div class="remind-content-txt">
                <div class="mb-3">09:00</div>
                <div>約訪林志零</div>
            </div>
        </div>
    </div>
</template>
@@ -48,29 +53,6 @@
            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/NavBar.vue
@@ -8,7 +8,7 @@
    </div>
    <div class="pam-header__action-bar">
      <i
        v-if="currentRole"
        v-if="isShowNotification"
        class="icon-bell text--dark-blue cursor--pointer fix-chrome-click--issue"
        @click="$router.push('/notification')"
      ></i>
@@ -49,9 +49,11 @@
<script lang="ts">
  import { Vue, Component } from 'vue-property-decorator';
  import { namespace } from 'nuxt-property-decorator';
  import { Action, namespace, State, Watch } from 'nuxt-property-decorator';
  import { Role } from '~/shared/models/enum/Role';
  import * as _ from 'lodash';
  import { NotificationList } from '~/shared/models/reviews.model';
  import { AppointmentLog } from '~/shared/models/appointment.model';
  const roleStorage = namespace('localStorage');
  @Component
@@ -71,6 +73,21 @@
    @roleStorage.Getter
    isAdminLogin!: boolean;
    @roleStorage.Getter
    isUserLogin!: boolean;
    @Action
    storeMyPersonalNotification!: () => void;
    @State
    notificationList!: NotificationList[];
    @Action
    storeMyAppointmentReviewLog!: () => void;
    @State
    unReviewLogList!: AppointmentLog[];
    isOpenDropdown = false;
@@ -123,6 +140,24 @@
    //////////////////////////////////////////////////////////////////////
    @Watch('$route', {immediate: true})
    onRouterChange() {
        this.getNotificationAndReviewLog();
    }
    private getNotificationAndReviewLog() {
      if (this.isUserLogin) {
        this.storeMyPersonalNotification();
        this.storeMyAppointmentReviewLog();
      }
      if (this.isAdminLogin) {
        this.storeMyPersonalNotification();
      }
    }
    //////////////////////////////////////////////////////////////////////
    routerNavigateTo(url: string): void {
      (this.$refs.dropdown as any).hide();
      _.isEqual(url,'')
@@ -144,6 +179,15 @@
      return this.idToken && this.currentRole ? (this.currentRole as Role): Role.NOT_LOGIN;
    }
    get isShowNotification() {
      if (this.isUserLogin) {
        return this.notificationList.length || this.unReviewLogList.length;
      }
      if (this.isAdminLogin) {
        return this.notificationList.length
      }
    }
  }
</script>
PAMapp/components/ReviewRecords/ReviewRecords.vue
@@ -11,11 +11,11 @@
    </section>
    <div class="user-reviews-page">
      <template v-if="myAppointmentReviewLogList.length">
      <template v-if="reviewLogList.length">
        <section class="user-reviews-content">
            <div
                class="user-reviews-card"
                v-for="(appointmentLog, index) in myAppointmentReviewLogList"
                v-for="(appointmentLog, index) in reviewLogList"
                :key="index">
                <div class="user-reviews-card-content" v-if="isUserLogin">
                    æ‚¨å°<span class="mdTxt">{{ ` ${appointmentLog.agentName} ` }}</span>做了 <UiReviewScore :score="appointmentLog.score" /> è©•價!
@@ -55,17 +55,19 @@
</template>
<script lang="ts">
import { AppointmentLog } from '~/shared/models/appointment.model';
import { Vue, Component, Prop } from 'nuxt-property-decorator';
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator';
import authService from '~/shared/services/auth.service';
@Component
export default  class ReviewRecords extends Vue{
    @Prop()
    myAppointmentReviewLogList!: AppointmentLog[];
    reviewLogList!: AppointmentLog[];
    isUserLogin = false;
    //////////////////////////////////////////////////////////////////////
    mounted() {
      this.isUserLogin = authService.isUserLogin();
    }
PAMapp/components/Ui/UiDatePicker.vue
@@ -8,13 +8,14 @@
        format="yyyy/MM/dd"
        placeholder="選擇日期"
        prefix-icon="icon-down down-icon"
        :picker-options="pickerOptions"
        @change="changeDate"
    >
    </el-date-picker>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator";
import { Component, Emit, Prop, PropSync, Vue, Watch } from "nuxt-property-decorator";
@Component
export default class UiDatePicker extends Vue {
@@ -22,6 +23,9 @@
    @Prop()
    defaultValue!: string;
    @Prop({default: false})
    isPastDateDisabled!: boolean;
    @Emit('changeDate')
    changeDate() {
@@ -35,5 +39,19 @@
            this.changeDate();
        }
    }
    get pickerOptions() {
        if (this.isPastDateDisabled) {
            return {
                disabledDate(time: Date) {
                    const date = new Date();
                    const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
                    const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`
                    return new Date(pickedDate).getTime() < new Date(currentDate).getTime();
                }
            }
        }
    }
}
</script>
PAMapp/components/Ui/UiTimePicker.vue
@@ -19,14 +19,17 @@
@Component
export default class UiTimePicker extends Vue {
    timeValue = '';
    pickerOptions = {
        start: '09:00',
        step: '00:15',
        end: '21:00'
    }
    @Prop()
    defaultValue!: string;
    @Prop({default: ''})
    changeDate!: Date | string;
    @Prop()
    isPastDateDisabled!: boolean;
    ///////////////////////////////////////////////////////////////////////
    @Emit('changeTime')
    changeTime() {
@@ -36,11 +39,61 @@
    @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}`;
            const defaultDate = new Date(this.defaultValue);
            this.timeValue = this.formatTimeString(defaultDate);
            this.changeTime();
        }
    }
    ///////////////////////////////////////////////////////////////////////
    get pickerOptions() {
        let minTime = '';
        const currentDate = new Date();
        if (this.isPastDateDisabled && this.changeDate && this.isPickedToday(currentDate)) {
            minTime = this.formatTimeString(currentDate);
            this.isPickedDisableTime(currentDate, minTime);
        }
        return {
            start: '09:00',
            step: '00:15',
            end: '21:00',
            minTime: minTime
        }
    }
    private isPickedDisableTime(currentDate: Date, minTime: string) {
        const currentTime = this.getTimeValue(currentDate, minTime);
        const pickedTime = this.getTimeValue(currentDate, this.timeValue);
        if (pickedTime < currentTime) {
            this.timeValue = '';
            this.changeTime();
        }
    }
    private isPickedToday(currentDate: Date) {
        const pickedDate = new Date(this.changeDate);
        const today = this.formatDateString(currentDate);
        const picked = this.formatDateString(pickedDate);
        return today === picked;
    }
    private getTimeValue(date: Date, time: string) {
        const hour = time.split(':')[0];
        const minute = time.split(':')[1];
        return date.setHours(+hour, +minute);
    }
    private formatDateString(date: Date) {
        return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
    }
    private formatTimeString(date: Date) {
        const hours = date.getHours();
        const minutes = date.getMinutes();
        return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`;
    }
}
</script>
PAMapp/pages/agentInfo/_agentNo.vue
@@ -146,7 +146,7 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="comment" label="個人理念">
        <UiField icon="comment" label="個人理念" class="agent-info-textarea">
          {{ agentInfo.concept }}
        </UiField>
      </el-row>
@@ -154,7 +154,7 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="school" label="個人背景">
        <UiField icon="school" label="個人背景" class="agent-info-textarea">
          <span>
            {{ agentInfo.experiences }}
          </span>
@@ -164,7 +164,7 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="trophy" label="得獎經歷">
        <UiField icon="trophy" label="得獎經歷" class="agent-info-textarea">
          {{ agentInfo.awards }}
        </UiField>
      </el-row>
@@ -332,5 +332,8 @@
.pam-field{
  display: flex;
}
.agent-info-textarea{
  word-break: break-all;
  word-wrap: break-word;
}
</style>
PAMapp/pages/appointment/_appointmentId/close/index.vue
@@ -196,8 +196,11 @@
        contactStatus    : this.contactStatus.CLOSE,
        remark           : this.appointmentCloseInfo.remark,
      }
      appointmentService.closeAppointment(toCloseAppointment).then((_) => this.updateAppointmentDetail(appointmentId));
      this.isShowSuccessAlert = true;
      appointmentService.closeAppointment(toCloseAppointment).then((_) => {
        this.updateAppointmentDetail(appointmentId);
        this.isShowSuccessAlert = true;
      });
    }
  }
PAMapp/pages/appointment/_appointmentId/index.vue
@@ -14,12 +14,15 @@
    <section class="client-detail">
      <div class="client-detail-info">
      <div class="client-detail-info mb-30">
        <div class="client-detail-info__avatar">
          <div class="circle">
            {{ appointmentDetail.name || 'NO NAME' }}
            <div class="sm-circle">
              {{ appointmentDetail.gender === 'male' ? '男' : '女'}}
            <div class="sm-circle sm-circle-male" v-if="appointmentDetail.gender === 'male'">
              <i class="icon-sex-male sex-icon"></i>
            </div>
            <div class="sm-circle sm-circle-female" v-if="appointmentDetail.gender === 'female'">
              <i class="icon-sex-female sex-icon"></i>
            </div>
          </div>
        </div>
@@ -156,13 +159,14 @@
  }
  getHopeContactTimeContent(hopeContactTimeString: string): string[] {
    const result = hopeContactTimeString.replace("'", '').split('、');
    const result = hopeContactTimeString.replaceAll("'", '').split('、');
    return result;
  }
  inviteReview(): void {
    reviewsService.sendSatisfactionToClient(this.appointmentDetail.id).then(res => res);
    this.isShowInviteReviewDialog = true ;
    reviewsService.sendSatisfactionToClient(this.appointmentDetail.id).then(res => {
        this.isShowInviteReviewDialog = true;
    });
  }
}
</script>
@@ -189,8 +193,7 @@
          height: 100px;
          width: 100px;
          border-radius: 50%;
          background-color: #fff;
          border: 1px solid $PRIMARY_BLACK;
          background-image: url('~/assets/images/appointment/avatar_bg.svg');
          position: relative;
          display: flex;
          justify-content: center;
@@ -201,12 +204,26 @@
            width: 30px;
            border-radius: 50%;
            background-color: #fff;
            border: 1px solid $PRIMARY_BLACK;
            bottom: 0;
            right: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            .sex-icon {
              font-size: 20px;
              &.icon-sex-male{
                color: $SKY_BLUE;
              }
              &.icon-sex-female{
                color: $CORAL;
              }
            }
            &-male {
              border: 1px solid $SKY_BLUE;
            }
            &-female {
              border: 1px solid $LIGHT_RED;
            }
          }
        }
      }
@@ -230,12 +247,14 @@
        @extend .mb-10;
        @extend .mdTxt;
        @extend .mr-10;
        line-height: 1.3;
        color     : $DARK_BLUE;
        flex-basis: auto;
        min-width : 40px;
      }
      .client-detail-demand__demand-list-content {
        text-align: justify;
        line-height: 1.3;
        text-justify: auto;
        word-break: break-all;
      }
PAMapp/pages/appointment/_appointmentId/interview/new/index.vue
@@ -43,4 +43,13 @@
    display: flex;
    justify-content: center;
}
.required {
  position: relative;
  &::before {
      content: '*';
      position: absolute;
      color: #FF0000;
      transform: translate(-12px, 0);
  }
}
</style>
PAMapp/pages/appointmentAgenda/index.vue
@@ -1,41 +1,56 @@
<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 class="remind-date mr-10">
              <div class="mb-3 smTxt bgc-primary-red date-year">
                <div class="mb-3 mt-2">2021</div>
                </div>
                <div class="remind-content-txt">
                  <div class="txt-margin">09:00</div>
                  <div>約訪林志零</div>
                </div>
              <div class="p mt-5">
                <div>12/25</div>
              </div>
            </div>
            <div class="remind-content-txt">
                <div class="mb-3">09:00</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 class="remind-container">
            <div class="remind-date mr-10">
              <div class="mb-3 smTxt bgc-primary-red date-year">
                <div class="mb-3 mt-2">2021</div>
                </div>
                <div class="remind-content-txt">
                  <div class="txt-margin">13:00</div>
                  <div>約訪王聰明</div>
                </div>
              <div class="p mt-5">
                <div>12/25</div>
              </div>
            </div>
            <div class="remind-content-txt">
                <div class="mb-3">09:00</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 class="remind-container">
            <div class="remind-date mr-10">
              <div class="mb-3 smTxt bgc-primary-red date-year">
                <div class="mb-3 mt-2">2021</div>
                </div>
                <div class="remind-content-txt">
                  <div class="txt-margin">09:00</div>
                  <div>約訪李伯伯</div>
                </div>
              <div class="p mt-5">
                <div>12/25</div>
              </div>
            </div>
            <div class="remind-content-txt">
                <div class="mb-3">09:00</div>
                <div>約訪林志零</div>
            </div>
        </div>
  </div>
</template>
@@ -50,29 +65,4 @@
}
</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,8 +1,9 @@
<template>
    <div>
      <div class="pam-myAppointment-banner"></div>
    <InterviewNotification></InterviewNotification>
        <div class="pam-container">
            <div class="pam-cus-tabs mb-30">
            <div class="pam-cus-tabs mb-10">
                <div
                    class="cus-tab-item"
                    :class="{'is-active': activeTabName === 'appointmentList'}"
PAMapp/pages/myAppointmentList/closedList.vue
@@ -14,7 +14,7 @@
            ></i>
        </el-input>
        <div class="closed-appointment__tag-filter">
        <div class="closed-appointment__tag-filter mb-10">
          <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>
@@ -126,15 +126,19 @@
  .closed-appointment__tag-filter {
    display: flex;
    .el-radio {
      border-color: $PRIMARY_BLACK;
      border-width: 2px;
      border-color: $LIGHT_GREY;
      border-radius: 30px;
      border-width: 1px;
      font-size   : 16px;
      margin-left : 0 !important;
      margin-right: 10px;
      padding     : 10px;
      @extend .fix-chrome-click--issue;
      &.is-checked {
        background-color: #D0D0CE;
        background-color: $CORAL;
        .el-radio__label {
          color  : $PRIMARY_WHITE !important;
        }
      }
      .el-radio__input {
        display: none;
PAMapp/pages/myAppointmentList/contactedList.vue
@@ -14,15 +14,18 @@
            ></i>
        </el-input>
        <div class="mb-10">
          <a class="sort-indicator text--primary text--underline cursor--pointer" @click="changeSortType">
        <div class="mb-10 sort-indicator-container">
          <a class="sort-indicator cursor--pointer" @click="changeSortType">
            {{ this.sortType === 'DESC' ? '新->舊' : '舊->新' }}
            <i v-if="isSortType" class="icon-sort-add"></i>
            <i v-else class="icon-sort-decrease"></i>
          </a>
        </div>
        <ClientList
            :clients="pageList"
            :title="'contactedList'"
            class="mt-10"
        ></ClientList>
        <UiPagination
@@ -123,5 +126,21 @@
      }
    }
    get isSortType () :boolean {
      return this.sortType === 'DESC';
    }
}
</script>
<style lang="scss" scoped>
.sort-indicator-container{
  margin-bottom: 20px;
}
.sort-indicator{
  border-radius:30px;
  border: 1px solid #D0D0CE;
  background-color:#fff;
  padding: 10px 20px;
  color: #68737A;
}
</style>
PAMapp/pages/notification/index.vue
@@ -3,7 +3,11 @@
        <div class="text--right mb-10" @click="showNotificationHint = true">
            <i class="satisfaction-icon icon-edit"></i>
        </div>
        <div class="satisfaction-banner my-10 cursor--pointer" @click="$router.push('/satisfactionList')">
        <div
            v-if="isUserLogin && unReviewLogList.length"
            class="satisfaction-banner my-10 cursor--pointer"
            @click="$router.push('/satisfactionList')"
        >
            <p class="satisfaction-text text--center">請填寫滿意度調查</p>
        </div>
        <el-row
@@ -14,7 +18,7 @@
            align="middle"
            class="notification-card"
        >
            <el-col class="unRead" :span="3"></el-col>
            <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col>
            <el-col :span="18">
                <p class="text">{{item.content}}</p>
            </el-col>
@@ -22,13 +26,13 @@
                <div>
                    <UiDateFormat
                        class="date"
                        :date="item.date"
                        :date="item.createdDate"
                        onlyShowSection="DAY" />
                </div>
                <div>
                    <UiDateFormat
                        class="time"
                        :date="item.date"
                        :date="item.createdDate"
                        onlyShowSection="TIME" />
                </div>
@@ -54,22 +58,29 @@
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
import { Component, State, Vue } from "nuxt-property-decorator";
import { AppointmentLog } from "~/shared/models/appointment.model";
import { NotificationList } from "~/shared/models/reviews.model";
import authService from "~/shared/services/auth.service";
@Component
export default class Notification extends Vue {
    showNotificationHint = false;
    notificationList = [
        {
            content: '系統停機公告:10/19(五)22:30至10/21(日)20:00進行系統更新',
            date: '2022-01-05T04:18:05.249Z'
        },
        {
            content: '系統停機公告:10/19(五)22:30至10/21(日)20:00進行系統更新',
            date: '2022-01-05T04:18:05.249Z'
        }
    ]
    @State
    unReviewLogList!: AppointmentLog[];
    @State
    notificationList!: NotificationList[];
    showNotificationHint = false;
    isUserLogin = false;
    ////////////////////////////////////////////////////////////
    mounted() {
        this.isUserLogin = authService.isUserLogin();
    }
}
</script>
PAMapp/pages/questionnaire/_agentNo.vue
@@ -115,12 +115,21 @@
    </PopUpFrame>
    <PopUpFrame :isOpen.sync="sendReserve" @update:isOpen="closeReservePopUp">
        <div class="text--middle  mt-30 sendReserve-txt">預約成功!</div>
        <div class="text--middle sendReserve-txt">您預約的顧問會儘速與您聯絡!</div>
        <div class="mdTxt mt-30 sendReserve-txt">預約成功!</div>
        <div class="mdTxt sendReserve-txt mb-30">您預約的顧問會儘速與您聯絡!</div>
        <div class="pam-app-review mb-10">
          <div class="mdTxt mb-10">對於
                <span class="mdTxt text--primary text--bold ">服務媒合</span>
                å¹³å°çš„æ•´é«”服務,
            </div>
          <div class="mdTxt">您給予幾顆星評價?</div>
        </div>
        <el-rate v-model="score" class="pam-satisfaction-rate fix-chrome-click--issue"></el-rate>
        <div class="text--center mdTxt">
          <el-button @click="closeReservePopUp">略過</el-button>
          <el-button type="primary"
            @click="closeReservePopUp">
            æˆ‘知道了
            é€å‡º
          </el-button>
        </div>
    </PopUpFrame>
@@ -166,6 +175,8 @@
    @roleStorage.State
    recommendConsultantItem!:string;
    score ="" ;
    genderOptions=[
      {
@@ -478,7 +489,6 @@
  display: flex;
  justify-content: center;
  margin-top: 10px;
  margin-bottom: 26px;
}
//drawer最底下文字樣式
@@ -603,6 +613,16 @@
    margin: 0px 20px;
  }
.pam-app-review{
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.pam-satisfaction-rate{
  margin-bottom: 45px;
}
  @include desktop{
  .ques-header{
@@ -630,5 +650,6 @@
  }
}
</style>
PAMapp/pages/record/index.vue
@@ -1,7 +1,7 @@
<template>
<div>
    <ReviewRecords
      :myAppointmentReviewLogList="myAppointmentReviewLogList"
      :reviewLogList="reviewLogList"
    ></ReviewRecords>
</div>
</template>
@@ -13,17 +13,8 @@
@Component
export default  class Reviews extends Vue{
    @State('myAppointmentReviewLogList')
    myAppointmentReviewLogList!: AppointmentLog[];
    @Action
    storeMyAppointmentReviewLog!: any;
    //////////////////////////////////////////////////////////////////////
    mounted() {
        this.storeMyAppointmentReviewLog();
    }
    @State('reviewLogList')
    reviewLogList!: AppointmentLog[];
}
</script>
PAMapp/pages/satisfactionList.vue
@@ -4,17 +4,17 @@
    <div class="pam-container">
      <div class="satisfaction-title">
        <span class="mdTxt">滿意度調查</span>
        <span class="ml-10 text--prudential_grey smTxt_bold">共 {{satisfactionList.length}} ç­†</span>
        <span class="ml-10 text--prudential_grey smTxt_bold">共 {{mapUnReviewLogList.length}} ç­†</span>
      </div>
      <div class="satisfaction-card" v-for="(item, index) in satisfactionList" :key="index">
      <div class="satisfaction-card" v-for="(item, index) in mapUnReviewLogList" :key="index">
        <div class="satisfaction-card-content">
          <UiAvatar :size="80" :agentNo="''"></UiAvatar>
          <UiAvatar :size="80" :agentNo="item.agentNo"></UiAvatar>
          <div class="satisfaction-card-text">對於顧問
              <span class="text--primary text--bold">{{item.agentName}}</span>
              çš„æ•´é«”服務,您給予幾顆星評價?
          </div>
        </div>
        <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10"></el-rate>
        <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"></el-rate>
      </div>
      <div class="text--center mt-30">
        <el-button type="primary" :disabled="isBtnDisabled">送出</el-button>
@@ -23,27 +23,46 @@
  </div>
</template>
<script>
import { Vue, Component } from 'nuxt-property-decorator';
<script lang="ts">
import { Vue, Component, Action, State, Watch } from 'nuxt-property-decorator';
import { AppointmentLog } from '~/shared/models/appointment.model';
@Component({
  layout: 'home'
})
export default class MySatisfactionList extends Vue {
  satisfactionList = [
    {
      agentName: '蔡美眉',
      score: 0
    },
    {
      agentName: '蔡美眉',
      score: 0
  @State
  unReviewLogList!: AppointmentLog[];
  mapUnReviewLogList: AppointmentReviewLog[] = [];
  ///////////////////////////////////////////////////////
  @Watch('unReviewLogList')
  onUnReviewLogListChange() {
    if (this.unReviewLogList.length) {
      this.mapUnReviewLogList = this.unReviewLogList.map(item => {
        return {
          ...item,
          satisfaction: 0
        }
      })
    }
  ]
  }
  ///////////////////////////////////////////////////////
  get isBtnDisabled() {
    return this.satisfactionList.findIndex(item => item.score > 0) === -1;
    if (this.mapUnReviewLogList.length) {
      return this.mapUnReviewLogList.findIndex(item => item.satisfaction > 0) === -1;
    }
    return false;
  }
}
interface AppointmentReviewLog extends AppointmentLog {
  satisfaction: number;
}
</script>
@@ -99,4 +118,4 @@
  }
}
</style>
</style>
PAMapp/pages/userReviewsRecord/index.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <ReviewRecords
      :myAppointmentReviewLogList="myAppointmentReviewLogList"
      :reviewLogList="reviewLogList"
    ></ReviewRecords>
  </div>
</template>
@@ -11,16 +11,9 @@
@Component
export default  class UserReviewsRecord extends Vue{
    @State('myAppointmentReviewLogList')
    myAppointmentReviewLogList!: AppointmentLog[];
    @Action
    storeMyAppointmentReviewLog!: any;
    //////////////////////////////////////////////////////////////////////
    mounted() {
      this.storeMyAppointmentReviewLog();
    }
    @State('reviewLogList')
    reviewLogList!: AppointmentLog[];
}
</script>
PAMapp/shared/models/reviews.model.ts
@@ -2,3 +2,20 @@
  appointmentId: number,
  score        : number,
}
export interface NotificationList {
  id: number;
  /** æ¨™é¡Œ */
  title: string;
  /** é€šçŸ¥å…§å®¹ */
  content: string;
  /** ACTIVITY(個人活動通知)、SYSTEM(系統通知) */
  notificationType: string;
  /** CUSTOMER(客戶)、CONSULTANT(顧問) */
  ownerRole: string;
  /** ç™»å…¥è€…id */
  ownerId: number
  createdDate: string;
  /** å·²è®€æ™‚é–“ */
  readDate: string;
}
PAMapp/shared/services/reviews.service.ts
@@ -1,6 +1,6 @@
import { http } from "./httpClient";
import { UserReviewsConsultantsParams } from "../models/reviews.model";
import { NotificationList, UserReviewsConsultantsParams } from "../models/reviews.model";
import { AppointmentLog } from "../models/appointment.model";
class ReviewsService {
@@ -19,6 +19,11 @@
  sendSatisfactionToClient(appointmentId: number) {
    return http.post(`/consultant/sendSatisfactionToClient/${appointmentId}`).then((res) => res);
  }
  // é€šçŸ¥å°éˆ´éº
  getMyPersonalNotification(): Promise<NotificationList[]> {
    return http.get('/personal_notification/getMyPersonalNotification').then(res => res.data);
  }
}
export default new ReviewsService();
PAMapp/store/index.ts
@@ -7,6 +7,7 @@
import { getFavoriteFromStorage, setFavoriteToStorage } from '~/shared/storageConsultant';
import { AppointmentLog } from '~/shared/models/appointment.model';
import { AgentOfStrictQuery, StrictQueryParams } from '~/shared/models/strict-query.model';
import { NotificationList } from '~/shared/models/reviews.model';
@Module
export default class Store extends VuexModule {
@@ -14,7 +15,9 @@
    strictQueryList: AgentOfStrictQuery[] = [];
    myConsultantList: Consultant[] = [];
    myAppointmentReviewLogList: AppointmentLog[] = [];
    reviewLogList: AppointmentLog[] = [];
    unReviewLogList: AppointmentLog[] = [];
    notificationList: NotificationList[] = [];
    get isUserLogin() {
        return this.context.getters['localStorage/isUserLogin'];
@@ -36,8 +39,18 @@
    }
    @Mutation
    updateMyAppointmentReviewLog(data: AppointmentLog[]) {
        this.myAppointmentReviewLogList = data;
    updateReviewLog(data: AppointmentLog[]) {
        this.reviewLogList = data;
    }
    @Mutation
    updateUnReviewLog(data: AppointmentLog[]) {
        this.unReviewLogList = data;
    }
    @Mutation
    updateNotification(data: NotificationList[]) {
        this.notificationList = data;
    }
    @Action
@@ -118,7 +131,10 @@
                }
            });
            const sortedData = dataWithLatestDate.sort((a, b) => +b.compareDate - +a.compareDate);
            this.context.commit('updateMyAppointmentReviewLog', sortedData);
            const reviewLog = sortedData.filter(item => item.score);
            const unReviewLog = sortedData.filter(item => !item.score);
            this.context.commit('updateReviewLog', reviewLog);
            this.context.commit('updateUnReviewLog', unReviewLog);
        });
    }
@@ -131,4 +147,13 @@
        });
    }
    @Action
    storeMyPersonalNotification() {
        reviewsService.getMyPersonalNotification().then(data => {
            const sortData = data
                .sort((preItem, nextItem) => +new Date(nextItem.createdDate) - +new Date(preItem.createdDate))
            this.context.commit('updateNotification', sortData);
        })
    }
}
pamapi/src/doc/sql/20220122_w.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
CREATE TABLE public.appointment_expiring_notify_record (
   id bigserial NOT NULL,
   appointment_id bigserial NOT NULL,
   send_time timestamp NULL,
   CONSTRAINT appointment_pending_notify_record_pk PRIMARY KEY (id)
);
pamapi/src/doc/¹w¬ù³æ/«È¤á¨ú±o³Ì·s¹w¬ùªº¥¼³B²z¹w¬ù³æ.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,61 @@
http get :
http://localhost:8080/api/appointment/customer/expiring/newest
簡訊及email會以該網址進入首頁 -> http://localhost:3000?notContactAppointmentId={最新一筆未處理預約單}
response body: è‹¥æœ‰æœƒå‚³200並給以下資料,若無(未有任何逾期未處理預約單,則會回404)
{
    "id": 385,
    "phone": "0911223344",
    "email": "SDD",
    "contactType": "phone",
    "gender": "female",
    "age": "21-30",
    "job": "內勤",
    "requirement": "健康與保障",
    "communicateStatus": "done",
    "hopeContactTime": "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
    "otherRequirement": null,
    "appointmentDate": "2021-12-16T07:11:05.400Z",
    "lastModifiedDate": "2022-01-19T10:57:51.380Z",
    "agentNo": "A568420",
    "customerId": 139,
    "name": "Angula-test",
    "consultantViewTime": "2021-12-27T02:02:18.711Z",
    "consultantReadTime": "2021-12-28T07:16:01.295Z",
    "contactTime": "2021-12-28T07:16:37.004Z",
    "satisfactionScore": null,
    "appointmentMemoList": [],
    "interviewRecordDTOs": [],
    "appointmentNoticeLogs": [
        {
            "id": 4,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-11T09:33:57.754Z",
            "interviewDate": null
        },
        {
            "id": 6,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-19T10:38:42.187Z",
            "interviewDate": "2022-11-01T08:00:00.000+00:00"
        }
    ],
    "appointmentClosedInfo": {
        "id": 9,
        "policyholderIdentityId": "A123456789",
        "planCode": "ATMdd",
        "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
        "remark": "test remark",
        "closedReason": "other2",
        "closedOtherReason": "心情不好不想買2",
        "appointmentId": 385
    }
}
pamapi/src/doc/¹w¬ù³æ/¼Ð°O¬°¤wÁpµ¸API.txt
@@ -3,25 +3,56 @@
response body:
{
    "id": 401,
    "phone": "0912345678",
    "email": "wayne@pollex.com.tw",
    "contactType": "EMAIL",
    "gender": "male",
    "age": "under_20",
    "job": "123",
    "requirement": "健康與保障,保單健檢/規劃",
    "communicateStatus": "contacted",
    "id": 385,
    "phone": "0911223344",
    "email": "SDD",
    "contactType": "phone",
    "gender": "female",
    "age": "21-30",
    "job": "內勤",
    "requirement": "健康與保障",
    "communicateStatus": "done",
    "hopeContactTime": "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
    "otherRequirement": null,
    "appointmentDate": "2021-12-21T08:13:50.154Z",
    "lastModifiedDate": "2022-01-04T09:40:13.715Z",
    "agentNo": "J149388015",
    "customerId": 155,
    "name": "123",
    "consultantViewTime": "2021-12-24T07:27:48.681Z",
    "consultantReadTime": null,
    "contactTime": "2022-01-04T09:40:13.715Z",
    "appointmentDate": "2021-12-16T07:11:05.400Z",
    "lastModifiedDate": "2022-01-19T10:57:51.380Z",
    "agentNo": "A568420",
    "customerId": 139,
    "name": "Angula-test",
    "consultantViewTime": "2021-12-27T02:02:18.711Z",
    "consultantReadTime": "2021-12-28T07:16:01.295Z",
    "contactTime": "2021-12-28T07:16:37.004Z",
    "satisfactionScore": null,
    "appointmentMemoList": []
    "appointmentMemoList": [],
    "interviewRecordDTOs": [],
    "appointmentNoticeLogs": [
        {
            "id": 4,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-11T09:33:57.754Z",
            "interviewDate": null
        },
        {
            "id": 6,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-19T10:38:42.187Z",
            "interviewDate": "2022-11-01T08:00:00.000+00:00"
        }
    ],
    "appointmentClosedInfo": {
        "id": 9,
        "policyholderIdentityId": "A123456789",
        "planCode": "ATMdd",
        "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
        "remark": "test remark",
        "closedReason": "other2",
        "closedOtherReason": "心情不好不想買2",
        "appointmentId": 385
    }
}
pamapi/src/doc/¹w¬ù³æ/ÅU°Ý¨ú±o©Ò¦³¦Û¤vªº¹w¬ù³æAPI.txt
@@ -5,36 +5,59 @@
appointmentMemoList : å‚™è¨»è³‡æ–™
interviewRecordDTOs : ç´„訪紀錄
[ {
  "id" : 385,
  "phone" : "0911223344",
  "email" : "SDD",
  "contactType" : "phone",
  "gender" : "female",
  "age" : "21-30",
  "job" : "內勤",
  "requirement" : "健康與保障",
  "communicateStatus" : "contacted",
  "hopeContactTime" : "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
  "otherRequirement" : null,
  "appointmentDate" : "2021-12-16T07:11:05.400Z",
  "lastModifiedDate" : "2021-12-28T07:16:37.004Z",
  "agentNo" : "A568420",
  "customerId" : 139,
  "name" : "Angula-test",
  "consultantViewTime" : "2021-12-27T02:02:18.711Z",
  "consultantReadTime" : "2021-12-28T07:16:01.295Z",
  "contactTime" : "2021-12-28T07:16:37.004Z",
  "satisfactionScore" : null,
  "appointmentMemoList" : [ ],
  "interviewRecordDTOs" : [ {
    "id" : 6,
    "content" : "test record content",
    "createdDate" : "2022-01-07T07:38:05.976Z",
    "lastModifiedDate" : "2022-01-07T07:38:05.976Z",
    "createdBy" : "A568420",
    "lastModifiedBy" : "A568420",
    "interviewDate" : "2021-01-01T08:00:00.000+00:00",
    "appointmentId" : 385
  } ]
} ]
[
    {
        "id": 385,
        "phone": "0911223344",
        "email": "SDD",
        "contactType": "phone",
        "gender": "female",
        "age": "21-30",
        "job": "內勤",
        "requirement": "健康與保障",
        "communicateStatus": "done",
        "hopeContactTime": "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
        "otherRequirement": null,
        "appointmentDate": "2021-12-16T07:11:05.400Z",
        "lastModifiedDate": "2022-01-19T10:57:51.380Z",
        "agentNo": "A568420",
        "customerId": 139,
        "name": "Angula-test",
        "consultantViewTime": "2021-12-27T02:02:18.711Z",
        "consultantReadTime": "2021-12-28T07:16:01.295Z",
        "contactTime": "2021-12-28T07:16:37.004Z",
        "satisfactionScore": null,
        "appointmentMemoList": [],
        "interviewRecordDTOs": [],
        "appointmentNoticeLogs": [
            {
                "id": 4,
                "phone": "0912345678",
                "email": "pollex@gmail.com",
                "appointmentId": 385,
                "content": "notice customer invterview time",
                "createdDate": "2022-01-11T09:33:57.754Z",
                "interviewDate": null
            },
            {
                "id": 6,
                "phone": "0912345678",
                "email": "pollex@gmail.com",
                "appointmentId": 385,
                "content": "notice customer invterview time",
                "createdDate": "2022-01-19T10:38:42.187Z",
                "interviewDate": "2022-11-01T08:00:00.000+00:00"
            }
        ],
        "appointmentClosedInfo": {
            "id": 9,
            "policyholderIdentityId": "A123456789",
            "planCode": "ATMdd",
            "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
            "remark": "test remark",
            "closedReason": "other2",
            "closedOtherReason": "心情不好不想買2",
            "appointmentId": 385
        }
    }
]
pamapi/src/doc/¹w¬ù³æ/ÅU°Ý¨ú±o¥¼³B²z¹w¬ù³æ¼Æ¶q³qª¾.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,5 @@
http get :
http://localhost:8080/api/appointment/consultant/pending/sum
response body:
2
pamapi/src/main/java/com/pollex/pam/config/Constants.java
@@ -11,5 +11,24 @@
    public static final String SYSTEM = "system";
    public static final String DEFAULT_LANGUAGE = "zh-tw";
    /**
     * é›»è©±åŠemail在建立預約單(T)後的N天視為未處理預約單
     * ç›®å‰N皆暫定為2
     */
    public static final int APPOINTMENT_PENDING_PHONE_INTERVAL = 2;
    public static final int APPOINTMENT_PENDING_EMAIL_INTERVAL = 2;
    /**
     * é›»è©±åŠemail在建立預約單(T)後的N天會被視為未處理預約單,當天批次會發送提醒給顧問
     * è€Œåœ¨å¾Œä¸€å¤©(T+N+1),就會發送批次給客戶告知 è©²é¡§å•å¯èƒ½å¿™ç¢Œç„¡æ³•處理,是否需要取消
     */
    public static final int APPOINTMENT_EXPIRING_PHONE_INTERVAL = APPOINTMENT_PENDING_PHONE_INTERVAL + 1;
    public static final int APPOINTMENT_EXPIRING_EMAIL_INTERVAL = APPOINTMENT_PENDING_EMAIL_INTERVAL + 1;
    /**
     * é€šçŸ¥å®¢æˆ¶çš„æ¬¡æ•¸é™åˆ¶
     */
    public static final int SEND_EXPIRING_NOTIFY_LIMIT = 1;
    private Constants() {}
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,46 @@
package com.pollex.pam.domain;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@Entity
@Table(name = "appointment_expiring_notify_record")
public class AppointmentExpiringNotifyRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "appointment_id")
    private Long appointmentId;
    @Column(name = "send_time")
    private Instant sendTime;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
    public Instant getSendTime() {
        return sendTime;
    }
    public void setSendTime(Instant sendTime) {
        this.sendTime = sendTime;
    }
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentCustomerViewRepository.java
@@ -2,6 +2,9 @@
import java.util.List;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -11,4 +14,5 @@
public interface AppointmentCustomerViewRepository extends JpaRepository<AppointmentCustomerView, Long>{
    List<AppointmentCustomerView> findByAgentNo(String agentNo);
    List<AppointmentCustomerView> findByAgentNoAndCustomerId(String agentNo, Long customerId);
    List<AppointmentCustomerView> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
package com.pollex.pam.repository;
import com.pollex.pam.domain.AppointmentExpiringNotifyRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AppointmentExpiringNotifyRecordRepository extends JpaRepository<AppointmentExpiringNotifyRecord, Long> {
    List<AppointmentExpiringNotifyRecord> findAllByAppointmentId(Long appointmentId);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java
@@ -3,6 +3,8 @@
import java.util.List;
import java.util.Optional;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -16,4 +18,6 @@
    List<Appointment> findByAgentNoAndCustomerId(String agentNo, Long customerId);
    Optional<Appointment> findTopByAgentNoAndCustomerIdOrderByAppointmentDateDesc(String agentNo, Long customerId);
    List<Appointment> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/repository/SatisfactionRepository.java
@@ -3,6 +3,7 @@
import java.util.List;
import java.util.Optional;
import com.pollex.pam.enums.SatisfactionStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -19,9 +20,10 @@
    Optional<Satisfaction> findOneByAppointmentId(Long appointmentId);
    List<Satisfaction> findAllByStatus(SatisfactionStatusEnum status);
    @Query(value = "SELECT avg(score) FROM satisfaction where agent_no=:agent_no"
            + " and score is not null"
            , nativeQuery = true)
    Float getAgentScoreAvg(@Param("agent_no") String agentNo);
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
@@ -1,6 +1,9 @@
package com.pollex.pam.service;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -8,10 +11,9 @@
import com.pollex.pam.appointment.process.AppointmentProcess;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.service.dto.AppointmentUpdateDTO;
import com.pollex.pam.service.dto.ClosedProcessDTO;
import com.pollex.pam.service.dto.DoneProcessDTO;
import com.pollex.pam.service.dto.InterviewRecordDTO;
import com.pollex.pam.config.Constants;
import com.pollex.pam.service.dto.*;
import com.pollex.pam.web.rest.errors.NotFoundExpiringAppointmentException;
import com.pollex.pam.web.rest.errors.SendEmailFailException;
import com.pollex.pam.web.rest.errors.SendSMSFailException;
import io.jsonwebtoken.lang.Assert;
@@ -24,15 +26,11 @@
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentCustomerView;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.enums.InterviewRecordStatusEnum;
import com.pollex.pam.repository.AppointmentCustomerViewRepository;
import com.pollex.pam.repository.AppointmentRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.AppointmentCloseDTO;
import com.pollex.pam.service.dto.AppointmentCreateDTO;
import com.pollex.pam.service.dto.AppointmentCustomerViewDTO;
import com.pollex.pam.service.mapper.AppointmentCustomerViewMapper;
import com.pollex.pam.service.mapper.AppointmentDTOMapper;
import com.pollex.pam.web.rest.errors.AppointmentNotFoundException;
@@ -77,13 +75,13 @@
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    InterviewRecordService interviewRecordService;
    @Autowired
    AppointmentProcess abstractAppointmentProcess;
    @Autowired
    PersonalNotificationService personalNotificationService;
@@ -119,7 +117,7 @@
        appointment.setCommunicateStatus(ContactStatusEnum.CANCEL);
        appointmentRepository.save(appointment);
        personalNotificationService.createMarkAppointmentDeletedToConsultant(appointment);
    }
    public List<Appointment> findByAgentNo(String agentNo) {
@@ -203,7 +201,7 @@
        Assert.notNull(appointment, "appointment entity cannot be null");
        log.debug("is need send appointment notify msg? = {}", applicationProperties.isSendNotifyMsg());
        log.debug("sending appointment notify, appointmentId = {}", appointment.getId());
        sendAppointmentNotifyBySMS(appointment);
        sendAppointmentNotifyByHtmlEmail(appointment);
@@ -268,7 +266,7 @@
    public String getAppointmentDetailUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "/myAppointmentList/contactedList?appointmentId=" + appointmentId;
    }
    public Appointment findById(Long id) {
        return appointmentRepository.findById(id)
                .orElseThrow(AppointmentNotFoundException::new);
@@ -285,4 +283,36 @@
            abstractAppointmentProcess.process(dto);
        }
    }
    public Long getConsultantPendingAppointmentSum(String agentNo) {
        return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE)
                .stream()
                .filter(appointment -> agentNo.equals(appointment.getAgentNo()))
                .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_PENDING_PHONE_INTERVAL, Constants.APPOINTMENT_PENDING_EMAIL_INTERVAL))
                .count();
    }
    public AppointmentCustomerViewDTO getCustomerNewestExpiringAppointment(Long customerId) {
        return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE)
                .stream()
                .filter(appointment -> customerId.equals(appointment.getCustomerId()))
                .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL))
                .max(Comparator.comparing(AppointmentCustomerView::getAppointmentDate))
                .map(appointmentCustomerView -> appointmentCustomerViewMapper.toAppointmentCustomerViewDTO(appointmentCustomerView))
                .orElse(null);
    }
    public boolean isAppointmentDateNotInIntervalFromNow(AppointmentCustomerView appointment, int phoneInterval, int emailInterval) {
        final boolean isHavePhone = StringUtils.hasText(appointment.getPhone());
        final boolean isHaveEmail = StringUtils.hasText(appointment.getEmail());
        LocalDate appointmentDate = appointment.getAppointmentDate().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate nowDate = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate();
        long intervalDays = nowDate.toEpochDay() - appointmentDate.toEpochDay();
        final boolean isAppointmentExpiringByPhone = isHavePhone && intervalDays >= phoneInterval;
        final boolean isAppointmentExpiringByEmail = isHaveEmail && intervalDays >= emailInterval;
        return isAppointmentExpiringByPhone || isAppointmentExpiringByEmail;
    }
}
pamapi/src/main/java/com/pollex/pam/service/ConsultantService.java
@@ -73,22 +73,22 @@
    @Autowired
    ApplicationProperties applicationProperty;
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    ApplicationProperties applicationProperties;
    @Autowired
    ConsultantService consultantService;
    @Autowired
    SatisfactionRepository satisfactionRepository;
    @Autowired
    PersonalNotificationService personalNotificationService;
@@ -108,7 +108,7 @@
                    dto,
                    appointmentService.findAvailableByAgentNoAndCustomerId(consultant.getAgentNo(), customerId)
                );
                setFavoriteConsultantUpdatedTime(relation, dto);
                return dto;
@@ -140,8 +140,7 @@
        if (!appointments.isEmpty()) {
            AppointmentCustomerView latestAvailableAppointment = appointments.get(0);
            ContactStatusEnum latestStatus = latestAvailableAppointment.getCommunicateStatus();
            if( latestStatus != ContactStatusEnum.DONE
                    || latestStatus != ContactStatusEnum.CLOSED)
            if(latestStatus != ContactStatusEnum.DONE && latestStatus != ContactStatusEnum.CLOSED)
                customerFavoriteConsultantDTO.setContactStatus(latestStatus);
            else
                customerFavoriteConsultantDTO.setContactStatus(PICKED);
@@ -271,19 +270,19 @@
    public void sendSatisfactionToClient(Appointment appointment) {
        String subject = "滿意度填寫通知";
        if(StringUtils.hasText(appointment.getEmail())) {
            String content = genSendSatisfactionEmailContent(appointment);
            sendMsgService.sendMsgByEmail(appointment.getEmail(), subject, content, true);
        }if(StringUtils.hasText(appointment.getPhone())) {
            String content = genSendSatisfactionSMSContent(appointment);
            sendMsgService.sendMsgBySMS(appointment.getPhone(), content);
        }
        personalNotificationService.createSendSatisfactionToClientToCustomer(appointment);
    }
    private String genSendSatisfactionSMSContent(Appointment appointment) {
        String agentNo = appointment.getAgentNo();
        Consultant consultant = consultantService.findByAgentNo(agentNo);
@@ -305,7 +304,7 @@
    public String getSendSatisfactionToClientUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "/?appointmentId=" + appointmentId;
    }
    public void setConsultantAvgScore(Satisfaction satisfaction) {
        float avgScore = getAgentAvgScore(satisfaction.getAgentNo());
        Consultant consultant = consultantRepository.findOneByAgentNo(satisfaction.getAgentNo())
@@ -317,7 +316,7 @@
    public float getAgentAvgScore(String agentNo) {
        Float avgScore = satisfactionRepository.getAgentScoreAvg(agentNo);
        if(avgScore==null)return 0;
        BigDecimal bigDecimal = new BigDecimal(avgScore);
        BigDecimal bigDecimal = new BigDecimal(avgScore);
        return avgScore = bigDecimal.setScale(1,BigDecimal.ROUND_HALF_UP).floatValue();
    }
}
pamapi/src/main/java/com/pollex/pam/service/PersonalNotificationService.java
@@ -20,22 +20,22 @@
@Service
@Transactional
public class PersonalNotificationService {
    @Autowired
    PersonalNotificationRepository personalNotificationRepository;
    @Autowired
    ConsultantService consultantService;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    CustomerService customerService;
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    SatisfactionService satisfactionService;
@@ -68,6 +68,18 @@
        personalNotificationRepository.save(entity);
    }
    public void createNotFillSatisfactionSumToCustomer(Long customerId, int notFillSatisfactionSum) {
        PersonalNotification entity = new PersonalNotification();
        String content = "您有 "+notFillSatisfactionSum+" ç­†é¡§å•çš„æ»¿æ„åº¦éœ€è¦å¡«å¯«";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(customerId);
        entity.setOwnerRole(PersonalNotificationRoleEnum.CUSTOMER);
        entity.setTitle("客戶滿意度");
        personalNotificationRepository.save(entity);
    }
    public void createEditConsultantToConsultant(Consultant consultant) {
        PersonalNotification entity = new PersonalNotification();
        String content = "您的個人帳號設定已進行更新";
pamapi/src/main/java/com/pollex/pam/service/SatisfactionService.java
@@ -40,13 +40,13 @@
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    ConsultantRepository consultantRepository;
    @Autowired
    ConsultantService consultantService;
    @Autowired
    PersonalNotificationService personalNotificationService;
@@ -55,7 +55,7 @@
        consultantService.setConsultantAvgScore(satisfaction);
        return satisfaction;
    }
    public Satisfaction scorefaction(SatisfactionCustomerScoreDTO scoreDTO) {
        Optional<Satisfaction> satisfactionOP = getByAppointmentId(scoreDTO.getAppointmentId());
        Satisfaction satisfaction = satisfactionOP.orElseThrow(SatisfactionNotFoundException::new);
@@ -65,7 +65,7 @@
        personalNotificationService.createScorefactionToConsultant(satisfaction);
        return satisfaction;
    }
    public Satisfaction createSatisfaction(Appointment appointment) {
        boolean isexist = getByAppointmentId(appointment.getId()).isPresent();
        if(isexist) {
@@ -94,6 +94,10 @@
        return satisfactionRepository.findOneByAppointmentId(appointmentId);
    }
    public List<Satisfaction> getByStatus(SatisfactionStatusEnum status) {
        return satisfactionRepository.findAllByStatus(status);
    }
    public List<Satisfaction> scoreAllfaction(List<SatisfactionCustomerScoreDTO> scoreDTO) {
        List<Satisfaction> satisfactionList = new ArrayList<>();
        scoreDTO.stream().forEach(dto ->{
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,158 @@
package com.pollex.pam.service;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.config.Constants;
import com.pollex.pam.domain.*;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.enums.SatisfactionStatusEnum;
import com.pollex.pam.repository.AppointmentCustomerViewRepository;
import com.pollex.pam.repository.AppointmentExpiringNotifyRecordRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional
public class ScheduleTaskService {
    private static final String NOT_CONTACTED_NOTIFY_SUBJECT = "預約單未進行聯繫通知";
    private static final Logger log = LoggerFactory.getLogger(ScheduleTaskService.class);
    @Autowired
    ConsultantService consultantService;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    AppointmentCustomerViewRepository appointmentCustomerViewRepository;
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    ApplicationProperties applicationProperties;
    @Autowired
    AppointmentExpiringNotifyRecordRepository appointmentExpiringNotifyRecordRepository;
    @Autowired
    SatisfactionService satisfactionService;
    @Autowired
    PersonalNotificationService personalNotificationService;
    @Scheduled(cron = "0 30 8 * * *")
    public void sendAppointmentPendingNotifyToConsultant() {
        log.info("Starting send appointment pending notify to consultant");
        Map<String, List<AppointmentCustomerView>> consultantWithPendingAppointments =
            appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE)
                .stream()
                .filter(appointment ->
                    appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_PENDING_PHONE_INTERVAL, Constants.APPOINTMENT_PENDING_EMAIL_INTERVAL)
                )
                .collect(Collectors.groupingBy(AppointmentCustomerView::getAgentNo));
        consultantWithPendingAppointments.forEach((agentNo, pendingAppointments) -> {
            int pendingAppointmentsSum = pendingAppointments.size();
            Consultant consultant = consultantService.findByAgentNo(agentNo);
            String consultantPhoneNumber = consultant.getPhoneNumber();
            String consultantEmail = consultant.getEmail();
            String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum);
            sendMsgService.sendMsgBySMS(consultantPhoneNumber, String.format("您有%s則預約單未進行聯繫,請盡速處理", pendingAppointmentsSum));
            sendMsgService.sendMsgByEmail(consultantEmail, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true);
        });
        log.info("Sending appointment pending notify to consultant finish");
    }
    @Scheduled(cron = "0 30 8 * * *")
    public void sendAppointmentExpiringNotifyToCustomer() {
        log.info("Starting send appointment expiring notify to customer");
        List<AppointmentCustomerView> allByCommunicateStatus =
            appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE)
                .stream()
                .filter(appointment ->
                    appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL)
                )
                .filter(this::isAppointmentNotifyNotOnLimit)
                .collect(Collectors.toList());
        allByCommunicateStatus.forEach(appointment -> {
            Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
            Optional<String> optionalPhone = Optional.ofNullable(appointment.getPhone()).filter(StringUtils::hasText);
            Optional<String> optionalEmail = Optional.ofNullable(appointment.getEmail()).filter(StringUtils::hasText);
            optionalPhone.ifPresent(phone ->
                sendMsgService.sendMsgBySMS(phone, String.format("很抱歉!您預約%s顧問正忙碌中,請您取消預約並改選其他顧問,請點擊網址:%s"
                    , consultant.getName(), getAppointmentUrl(appointment.getId())))
            );
            optionalEmail.ifPresent(email ->
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentUrl(appointment.getId())), true)
            );
            AppointmentExpiringNotifyRecord record = new AppointmentExpiringNotifyRecord();
            record.setAppointmentId(appointment.getId());
            record.setSendTime(Instant.now());
            appointmentExpiringNotifyRecordRepository.save(record);
        });
        log.info("Sending appointment expiring notify to customer finish");
    }
    // todo éœ€ç¢ºèªè©²æ™‚é–“, otis todo=134497
    @Scheduled(cron = "0 0 9 * * *")
    public void sendNotFillSatisfactionToPersonalNotification() {
        Map<Long, List<Satisfaction>> customerNotFillSatisfactions = satisfactionService.getByStatus(SatisfactionStatusEnum.UNFILLED)
                .stream()
                .collect(Collectors.groupingBy(Satisfaction::getCustomerId));
        customerNotFillSatisfactions.forEach((customerId, notFillSatisfactions) ->
            personalNotificationService.createNotFillSatisfactionSumToCustomer(customerId, notFillSatisfactions.size())
        );
    }
    private boolean isAppointmentNotifyNotOnLimit(AppointmentCustomerView appointment) {
        int sendNotifyToCustomerRecordSum =
            appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size();
        return sendNotifyToCustomerRecordSum < Constants.SEND_EXPIRING_NOTIFY_LIMIT;
    }
    private String getAppointmentUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId;
    }
    private String getAppointmentPendingNotifyEmailContent(int sum) {
        Context context = new Context();
        context.setVariable("pendingAppointmentSum", sum);
        return springTemplateEngine.process("mail/appointmentPendingNotifyEmail", context);
    }
    private String getAppointmentExpiringNotifyEmail(String consultantName, String notifyUrl) {
        Context context = new Context();
        context.setVariable("consultantName", consultantName);
        context.setVariable("notifyUrl", notifyUrl);
        return springTemplateEngine.process("mail/appointmentExpiringNotifyEmail", context);
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
@@ -2,16 +2,13 @@
import com.pollex.pam.appointment.process.AppointmentProcess;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.SendMsgService;
import com.pollex.pam.service.dto.AppointmentUpdateDTO;
import com.pollex.pam.service.dto.ClosedProcessDTO;
import com.pollex.pam.service.dto.DoneProcessDTO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import com.pollex.pam.service.AppointmentService;
@@ -20,6 +17,8 @@
import com.pollex.pam.service.dto.AppointmentCloseDTO;
import com.pollex.pam.service.dto.AppointmentCreateDTO;
import com.pollex.pam.service.dto.AppointmentCustomerViewDTO;
import java.util.Objects;
@RestController
@RequestMapping("/api/appointment")
@@ -33,10 +32,10 @@
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    AppointmentProcess abstractAppointmentProcess;
    @Autowired
    PersonalNotificationService personalNotificationService;
@@ -75,16 +74,35 @@
        appointmentService.recordConsultantReadTime(appointmentId);
        return ResponseEntity.noContent().build();
    }
    @PostMapping("/close")
    public ResponseEntity<Void> closeAppointment(@RequestBody AppointmentCloseDTO closeDTO) {
        appointmentService.closeAppointment(closeDTO);
        return ResponseEntity.noContent().build();
    }
    @GetMapping("/customer/expiring/newest")
    public ResponseEntity<AppointmentCustomerViewDTO> getNewestExpiringAppointment() {
        Long customerId = SecurityUtils.getCustomerDBId();
        AppointmentCustomerViewDTO customerNewestExpiringAppointment = appointmentService.getCustomerNewestExpiringAppointment(customerId);
        if(Objects.nonNull(customerNewestExpiringAppointment)) {
            return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK);
        }
        else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
    @GetMapping("/consultant/pending/sum")
    public ResponseEntity<Long> getConsultantPendingAppointmentSum() {
        String agentNo = SecurityUtils.getAgentNo();
        return new ResponseEntity<>(appointmentService.getConsultantPendingAppointmentSum(agentNo), HttpStatus.OK);
    }
//    @PostMapping("/close/info/edit")
//    public ResponseEntity<Void> editAppointmentClosedInfo(@RequestBody AppointmentCloseDTO closeDTO) {
//
//
//        if(closeDTO.getContactStatus() == ContactStatusEnum.DONE) {
//            DoneProcessDTO dto = new DoneProcessDTO();
//            BeanUtils.copyProperties(closeDTO, dto);
@@ -96,7 +114,7 @@
//        }else {
//            return ResponseEntity.notFound().build();
//        }
//
//
//        return ResponseEntity.noContent().build();
//    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/NotFoundExpiringAppointmentException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,8 @@
package com.pollex.pam.web.rest.errors;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "not found any expiring appointment")
public class NotFoundExpiringAppointmentException extends RuntimeException {
}
pamapi/src/main/resources/config/application-dev.yml
@@ -33,7 +33,7 @@
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:postgresql://dev.pollex.com.tw:5433/pam_p2
    #url: jdbc:postgresql://localhost:5432/omo?currentSchema=omo
    #url: jdbc:postgresql://localhost:5432/omo?currentSchema=public
    username: pamadmin
    password: pamadmin
    hikari:
pamapi/src/main/resources/i18n/messages.properties
@@ -20,5 +20,11 @@
email.reset.text1=For your pamapi account a password reset was requested, please click on the URL below to reset it:
email.reset.text2=Regards,
# satisfaction write email
# satisfaction write email
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/i18n/messages_zh_TW.properties
@@ -20,5 +20,12 @@
email.reset.text1=\u60A8\u7684 pamapi \u5E33\u865F\u88AB\u8981\u6C42\u91CD\u65B0\u8A2D\u5B9A\u5BC6\u78BC\uFF0C\u8ACB\u9EDE\u4E0B\u5217\u7DB2\u5740\u8A2D\u5B9A:
email.reset.text2=\u795D\u60A8\u4F7F\u7528\u6109\u5FEB\uFF0C
# satisfaction write email
# satisfaction write email
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.expiring.content(${consultantName}, ${notifyUrl})}">很抱歉!您預約xxx顧問正忙碌中 , è«‹æ‚¨å–消預約並改選其他顧問</p>
</body>
</html>
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.pending.content(${pendingAppointmentSum})}">您有x則預約單未進行聯繫,請盡速處理</p>
</body>
</html>