保誠-保戶業務員媒合平台
Jack
2022-01-24 21d2b51910f1e1e338beed76d53effcfccb1ef7a
Merge branch 'Phase3' of ssh://dev.pollex.com.tw:29418/pcalife/PAM into Phase3
刪除1個檔案
修改33個檔案
新增2個檔案
769 ■■■■ 已變更過的檔案
PAMapp/assets/scss/utilities/_utilities.scss 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_rate.scss 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_select.scss 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/BackActionBar.vue 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Client/ClientCard.vue 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Consultant/ConsultantCard.vue 25 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/DateTimePicker.vue 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/interviewNotification.vue 58 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiDatePicker.vue 20 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiTimePicker.vue 33 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/middleware/getUrlQuery.ts 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/close/index.vue 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/index.vue 231 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/notification/index.vue 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/_agentNo.vue 22 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/satisfactionList.vue 49 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/strict-query.model.ts 18 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/httpClient.ts 13 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/reviews.service.ts 10 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/localStorage.ts 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220122_w.sql 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/淨空整個系統資料(除顧問).sql 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/客戶取得最新預約的未處理預約單.txt 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java 37 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/config/Constants.java 2 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java 25 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java 63 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java 7 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-dev.yml 18 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-pollex.yml 18 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-prod.yml 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-sit.yml 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-uat.yml 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_utilities.scss
@@ -5,6 +5,10 @@
  margin-bottom: 50px;
}
.mt-50 {
  margin-top: 50px;
}
.mt-30 {
  margin-top: 30px;
}
@@ -21,6 +25,7 @@
  margin-bottom: 20px;
}
.mt-10 {
  margin-top: 10px;
}
PAMapp/assets/scss/vendors/elementUI/_rate.scss
@@ -45,6 +45,7 @@
  display: flex;
  justify-content: center;
  margin-top: 10px;
  @extend .fix-chrome-click--issue;
  .el-rate__item {
    .el-rate__icon {
      font-size: 30px;
PAMapp/assets/scss/vendors/elementUI/_select.scss
@@ -31,6 +31,9 @@
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    &:before {
      content: "\e910";
    }
  }
}
PAMapp/components/BackActionBar.vue
@@ -74,9 +74,6 @@
        case 'accountSetting':
          featureLabel = '個人帳號設定';
          break;
        case 'appointmentAgenda':
          featureLabel = '即將約訪排程';
          break;
        case 'consultantAccountSetting':
          featureLabel = '查看帳號資訊';
          break;
PAMapp/components/Client/ClientCard.vue
@@ -213,6 +213,9 @@
    @appointmentStore.Action
    getAppointmentDetail!: (appointmentId: number) => Promise<Appointment>;
    @appointmentStore.Action
    updateAppointmentDetail!: (id: number) => Appointment;
    @appointmentStore.Getter
    appointmentProgress!: ContactStatus;
@@ -258,7 +261,10 @@
    viewAppointmentDetail(): void {
      this.getAppointmentDetail(this.client.id).then((_) => {
        this.readAppointment();
        const unread = !this.client.consultantReadTime;
        if (unread) {
          this.readAppointment();
        }
        this.$router.push(`/appointment/${this.client.id}`);
      });
    }
@@ -277,7 +283,6 @@
        reviewsService.sendSatisfactionToClient(this.client.id).then(res => {
            this.isShowInviteReviewDialog = true ;
        })
    }
    openDetail() {
@@ -302,14 +307,12 @@
    }
    private readAppointment(): void {
        const unread = !this.client.consultantReadTime;
        if (unread) {
            appointmentService.recordRead(this.client.id).then((_) => {
                const updatedClient = {...this.client};
                updatedClient.consultantReadTime = new Date().toString();
                this.updateMyAppointmentList(updatedClient);
            });
        };
      appointmentService.recordRead(this.client.id).then((_) => {
          const updatedClient = {...this.client};
          updatedClient.consultantReadTime = new Date().toString();
          this.updateMyAppointmentList(updatedClient);
          this.updateAppointmentDetail(this.client.id);
      });
    }
    private clearAppointmentIdFromMsg() {
PAMapp/components/Consultant/ConsultantCard.vue
@@ -31,7 +31,7 @@
                <div
                    class="delete"
                    v-if="showRemoveBtn"
                    @click="removeAgent"
                    @click="isRemoveAgentPopup = true"
                >移除</div>
                <div
                    v-if="notScoreAppointmentYet"
@@ -125,10 +125,18 @@
            </div>
        </PopUpFrame>
        <PopUpFrame :isOpen.sync="isConfirmPopup">
            <div class="text--center mdTxt">已成功取消此筆預約</div>
        <PopUpFrame  :isOpen.sync="isConfirmPopup">
            <div class="text--center mdTxt">已成功{{confirmTxt}}</div>
            <div class="text--center mt-30">
                <el-button @click="isConfirmPopup = false" type="primary">確定</el-button>
            </div>
        </PopUpFrame>
        <PopUpFrame :isOpen.sync="isRemoveAgentPopup">
            <div class="text--center mdTxt">是否移除顧問 <span class="text--primary">{{agentInfo.name}}</span>?</div>
            <div class="text--center mt-30">
                <el-button @click="isRemoveAgentPopup = false">否</el-button>
                <el-button @click="removeAgent" type="primary">是</el-button>
            </div>
        </PopUpFrame>
    </div>
@@ -181,6 +189,8 @@
    isCancelPopup = false;
    hideReviews = hideReviews;
    isConfirmPopup = false;
    isRemoveAgentPopup = false;
    confirmTxt = '';
    appointmentDetail: any = {
        age               : '',
@@ -339,6 +349,12 @@
    removeAgent() {
        this.removeFromMyConsultantList(this.agentInfo.agentNo).then((removeOk) => {
            this.isRemoveAgentPopup = false;
            setTimeout(() => {
                this.confirmTxt = '移除顧問';
                this.isConfirmPopup = true;
            }, 300);
        });
    }
@@ -370,7 +386,9 @@
            this.isVisibleDialog = false;
            this.isCancelPopup = false;
            setTimeout(() => {
                this.confirmTxt = '取消此筆預約';
                this.isConfirmPopup = true;
            }, 300);
        });
    }
@@ -422,6 +440,7 @@
        }
        .delete {
            display: inline-block;
            color: $PRIMARY_RED;
            font-size: 14px;
            font-weight: bold;
PAMapp/components/DateTimePicker.vue
@@ -5,12 +5,14 @@
        <UiDatePicker
            @changeDate="changeDateTime($event, 'date')"
            :isPastDateDisabled="isPastDateDisabled"
            :isFutureDateDisabled="isFutureDateDisabled"
            :defaultValue="defaultValue"
        ></UiDatePicker>
        <UiTimePicker
            @changeTime="changeDateTime($event, 'time')"
            :defaultValue="defaultValue"
            :isPastDateDisabled="isPastDateDisabled"
            :isFutureDateDisabled="isFutureDateDisabled"
            :changeDate="changeDate"
        ></UiTimePicker>
    </div>
@@ -30,6 +32,9 @@
    @Prop()
    isPastDateDisabled!: boolean;
    @Prop()
    isFutureDateDisabled!: boolean;
    @Emit('changeDateTime')
    changeDateTime(event, type) {
        if (type === 'date') {
PAMapp/components/Interview/interviewNotification.vue
Àɮפw§R°£
PAMapp/components/Ui/UiDatePicker.vue
@@ -5,6 +5,7 @@
        v-model="dateValue"
        :clearable="false"
        type="date"
        :editable="false"
        format="yyyy/MM/dd"
        placeholder="選擇日期"
        prefix-icon="icon-down down-icon"
@@ -27,6 +28,9 @@
    @Prop({default: false})
    isPastDateDisabled!: boolean;
    @Prop({default: false})
    isFutureDateDisabled!: boolean;
    @Emit('changeDate')
    changeDate() {
        return this.dateValue;
@@ -41,16 +45,26 @@
    }
    get pickerOptions() {
        const date = new Date();
        const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
        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()}`
                    const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`;
                    return new Date(pickedDate).getTime() < new Date(currentDate).getTime();
                }
            }
        }
        if (this.isFutureDateDisabled) {
            return {
                disabledDate(time: Date) {
                    const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}`;
                    return new Date(pickedDate).getTime() > new Date(currentDate).getTime();
                }
            }
        }
    }
}
PAMapp/components/Ui/UiTimePicker.vue
@@ -4,6 +4,7 @@
        popper-class="pam-time-popper"
        v-model="timeValue"
        :clearable="false"
        :editable="false"
        :picker-options="pickerOptions"
        placeholder="選擇時間"
        prefix-icon="icon-down down-icon"
@@ -29,6 +30,9 @@
    @Prop()
    isPastDateDisabled!: boolean;
    @Prop()
    isFutureDateDisabled!: boolean;
    ///////////////////////////////////////////////////////////////////////
    @Emit('changeTime')
@@ -49,25 +53,40 @@
    get pickerOptions() {
        let minTime = '';
        let maxTime = '';
        const currentDate = new Date();
        if (this.isPastDateDisabled && this.changeDate && this.isPickedToday(currentDate)) {
            minTime = this.formatTimeString(currentDate);
            this.isPickedDisableTime(currentDate, minTime);
        if (this.changeDate && this.isPickedToday(currentDate)) {
            if (this.isPastDateDisabled) {
                minTime = this.formatTimeString(currentDate);
                this.isPickedDisableTime(currentDate, minTime);
            }
            if (this.isFutureDateDisabled) {
                maxTime = this.formatTimeString(currentDate);
                this.isPickedDisableTime(currentDate, maxTime);
            }
        }
        return {
            start: '09:00',
            step: '00:15',
            end: '21:00',
            minTime: minTime
            minTime: minTime,
            maxTime: maxTime
        }
    }
    private isPickedDisableTime(currentDate: Date, minTime: string) {
        const currentTime = this.getTimeValue(currentDate, minTime);
    private isPickedDisableTime(currentDate: Date, minMaxTime: string) {
        const currentTime = this.getTimeValue(currentDate, minMaxTime);
        const pickedTime = this.getTimeValue(currentDate, this.timeValue);
        if (pickedTime < currentTime) {
        if (this.isPastDateDisabled && pickedTime < currentTime) {
            this.timeValue = '';
            this.changeTime();
        }
        if (this.isFutureDateDisabled && currentTime < pickedTime) {
            this.timeValue = '';
            this.changeTime();
        }
PAMapp/middleware/getUrlQuery.ts
@@ -3,7 +3,15 @@
const getUrlQuery: Middleware = (context) => {
  const currentRouteName = context.route.name;
  const satisfactionIdFromMsg = context.route.query.appointmentId;
  const queryNotContactAppointmentId = context.route.query.notContactAppointmentId;
  const isUserLogin = context.store.getters['localStorage/isUserLogin'];
  if (currentRouteName === 'index' && queryNotContactAppointmentId) {
    context.store.commit('localStorage/storageNotContactAppointmentIdFromMsg', queryNotContactAppointmentId);
    if (!isUserLogin) {
      context.redirect('/login');
    }
  }
  if (currentRouteName === 'index' && satisfactionIdFromMsg) {
    context.store.commit('localStorage/storageSatisfactionIdFromMsg', satisfactionIdFromMsg);
@@ -11,6 +19,8 @@
      context.redirect('/login');
    }
  }
}
export default getUrlQuery
export default getUrlQuery
PAMapp/pages/appointment/_appointmentId/close/index.vue
@@ -46,6 +46,7 @@
          <UiField label="進件時間" :labelSize="20" class="required">
            <DateTimePicker
              :defaultValue="appointmentCloseInfo.policyEntryDate"
              :isFutureDateDisabled="true"
              @changeDateTime="appointmentCloseDate = $event"></DateTimePicker>
          </UiField>
        </el-row>
PAMapp/pages/index.vue
@@ -41,10 +41,9 @@
    </div>
    <Ui-Dialog
        :isVisible.sync="isVisibleDialog"
        :width="width"
        :isVisible.sync="isShowAppointmentDialog"
        :width="appointmentDialogWidth"
        class="pam-myDemand-dialog pam-dialog-reserved"
        @closeDialog="clearSatisfactionId"
      >
        <div v-if="appointmentDetail">
            <h5 class="subTitle text--center mb-30">預約成功</h5>
@@ -71,16 +70,44 @@
                </div>
            </div>
            <div v-if="!appointmentDetail.satisfactionScore" class="reserved-btn">
            <div v-if="notScoreAppointmentYet" class="reserved-btn">
                <el-button type="primary"
                    @click.native="reviewsBtn = true">給予滿意度評分</el-button>
                    @click.native="isShowReviewDialog = true">給予滿意度評分</el-button>
            </div>
        </div>
      </Ui-Dialog>
      <PopUpFrame
        :isOpen.sync="reviewsBtn"
        @closePopUp="clearSatisfactionId"
        :isOpen.sync="isShowReAppointmentDialog"
        @closePopUp="removeUrlQueryParameter('notContactAppointmentId')"
      >
          <div class="pam-dialog-review">
              <div class="mt-30 text--middle" v-if="agentInfo">
                å¾ˆæŠ±æ­‰ï¼æ‚¨é ç´„çš„<span class="text--bold">{{ consultantName }}</span>顧問正忙碌中,請您取消預約並改選其他顧問
              </div>
                <el-row
                  type="flex"
                  class="mt-50"
                  justify="center">
                  <el-button
                      type="primary"
                      @click="reAppointment">取消預約再改選其他顧問</el-button>
                </el-row>
                <el-row
                  type="flex"
                  class="mt-20"
                  justify="center">
                  <el-button
                      class="outline_btn"
                      @click="cancelAppointment">取消預約</el-button>
                </el-row>
          </div>
      </PopUpFrame>
      <PopUpFrame
        :isOpen.sync="isShowReviewDialog"
        @closePopUp="removeUrlQueryParameter('appointmentId')"
      >
          <div class="mdTxt pam-dialog-review">
              ä¿éšªé¡§å•æ»¿æ„åº¦
@@ -110,11 +137,16 @@
<script lang="ts">
  import { Vue, Component, State, Action, Watch, namespace } from 'nuxt-property-decorator';
  import { Appointment, AppointmentClosedInfo } from '~/shared/models/appointment.model';
  import { Consultant } from '~/shared/models/consultant.model';
import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model';
  import { ContactStatus } from '~/shared/models/enum/contact-status';
  import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model';
  import { StrictQueryParams } from '~/shared/models/strict-query.model';
  import appointmentService from '~/shared/services/appointment.service';
import reviewsService from '~/shared/services/reviews.service';
  import reviewsService from '~/shared/services/reviews.service';
  import UtilsService from '~/shared/services/utils.service';
  import myConsultantService from '~/shared/services/my-consultant.service';
import { AgentInfo } from '~/shared/models/agent-info.model';
  const localStorage = namespace('localStorage');
  const roleStorage = namespace('localStorage');
@@ -136,7 +168,8 @@
    @Action
    storeRecommendList!: any;
    @Action storeConsultantList!: any;
    @Action
    storeConsultantList!: any;
    @localStorage.Mutation
    storageClearQuickFilter!: () => void;
@@ -147,37 +180,55 @@
    @localStorage.Getter
    currentSatisfactionIdFromMsg!: string;
    @localStorage.Getter
    currentNotContactAppointmentIdFromMsg!: string;
    @localStorage.Mutation
    storageClearSatisfactionIdFromMsg!: () => void;
    @localStorage.Mutation
    storageClearNotContactAppointmentIdFromMsg!: () => void;
    @localStorage.Mutation
    storageStrickQueryItem!: (strictQueryDto: StrictQueryParams) => void;
    consultantList: Consultant[] = [];
    appointmentDetail: any = {
        age               : '',
        agentNo           : '',
        appointmentDate   : '',
        communicateStatus : '',
        consultantReadTime: null,
        consultantViewTime: null,
        contactTime       : '',
        contactType       : '',
        customerId        : 0,
        email             : '',
        gender            : '',
        hopeContactTime   : "",
        id                : 0,
        job               : "",
        lastModifiedDate  : '',
        name              : '',
        otherRequirement  : null,
        phone             : "",
        requirement       : '',
        satisfactionScore : 0,
    appointmentDialogWidth    = '';
    inputScore                = 0;
    isShowAppointmentDialog   = false;
    isShowReAppointmentDialog = false;
    isShowReviewDialog        = false;
    consultantName = '';
    contactStatus = ContactStatus;
    appointmentDetail: Appointment = {
      age               : '',
      agentNo           : '',
      appointmentClosedInfo: {} as AppointmentClosedInfo,
      appointmentDate   : '',
      appointmentMemoList: [],
      appointmentNoticeLogs: [],
      communicateStatus : this.contactStatus.PICKED,
      consultantReadTime: '',
      consultantViewTime: '',
      contactTime       : '',
      contactType       : '',
      customerId        : 0,
      email             : '',
      gender            : '',
      hopeContactTime   : '',
      interviewRecordDTOs: [],
      id                : 0,
      job               : '',
      lastModifiedDate  : '',
      name              : '',
      otherRequirement  : '',
      phone             : '',
      requirement       : '',
      satisfactionScore : 0,
    };
    isVisibleDialog = false;
    width = '';
    reviewsBtn = false;
    inputScore = 0;
    agentInfo: Consultant = {
      agentNo            : '',
      name               : '',
@@ -211,7 +262,7 @@
    }
    destroyed() {
      this.clearSatisfactionId();
      this.removeUrlQueryParameter();
    }
    //////////////////////////////////////////////////////////////////////
@@ -223,36 +274,81 @@
        .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)}))
        .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate);
      if (this.currentSatisfactionIdFromMsg) {
        this.agentInfo = this.myConsultantList.filter(item => {
          const satisfactionIdIndex = item.appointments?.findIndex(i => i.id === +this.currentSatisfactionIdFromMsg);
          return satisfactionIdIndex !== undefined && satisfactionIdIndex > -1;
        })[0];
        if (this.agentInfo) {
          this.openAppointmentInfo();
        }
      if (this.currentNotContactAppointmentIdFromMsg) {
        this.autoOpenAppointmentBy('askReAppointment', +this.currentNotContactAppointmentIdFromMsg);
        return;
      }
      if (this.currentSatisfactionIdFromMsg) {
        this.autoOpenAppointmentBy('inviteReviewConsultant',+this.currentSatisfactionIdFromMsg);
        this.storageClearSatisfactionIdFromMsg();
        return;
      }
    }
    private openAppointmentInfo() {
        appointmentService.getAppointmentDetail(+this.currentSatisfactionIdFromMsg).then(res => {
            this.appointmentDetail = res;
            this.width = UtilsService.isMobileDevice() ? '80%' : '';
            this.isVisibleDialog = true;
            if (!this.appointmentDetail.satisfactionScore) {
              setTimeout(() => {
                this.reviewsBtn = true;
              }, 500)
            }
    private autoOpenAppointmentBy(reason: string, targetAppointmentId: number): void {
        const setAgentInfo = new Promise((resolve, reject) => {
          this.agentInfo = this.myConsultantList.filter(item => {
            const appointmentIndex = item.appointments?.findIndex(i => i.id === targetAppointmentId);
            return appointmentIndex !== undefined && appointmentIndex > -1;
          })[0];
          if (this.agentInfo) {
            myConsultantService.getConsultantDetail(this.agentInfo.agentNo).then((res) => resolve(res));
          }
        });
        const setAppointment = new Promise((resolve, reject) => {
           appointmentService.getAppointmentDetail(targetAppointmentId).then((res) => resolve(res));
        });
        Promise.all([setAgentInfo, setAppointment]).then((values) => {
          const agentInfo = values[0] as AgentInfo;
          const appointmentInfo = values[1] as Appointment;
          this.consultantName = agentInfo.name;
          this.appointmentDetail = appointmentInfo;
          this.appointmentDialogWidth = UtilsService.isMobileDevice() ? '80%' : '';
          this.isShowAppointmentDialog = true;
          switch (reason) {
            case 'inviteReviewConsultant':
              if (this.notScoreAppointmentYet) {
                setTimeout(() => {
                  this.isShowReviewDialog = true;
                }, 500);
              }
              break;
            case 'askReAppointment':
              setTimeout(() => {
                this.isShowReAppointmentDialog = true;
              }, 500);
              break;
          }
        });
    }
    //////////////////////////////////////////////////////////////////////
    navigateToRoute(path: string): void {
      this.$router.push(path);
    }
    reAppointment(): void {
      appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => {
        const requirements = this.appointmentDetail.requirement.split(',');
        console.log('requirements', requirements)
        this.storeConsultantList();
        this.storageStrickQueryItem({ requirements: requirements });
        this.storageClearNotContactAppointmentIdFromMsg();
        this.$router.push('/recommendConsultant');
      });
    }
    cancelAppointment(): void {
      appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => {
        this.storeConsultantList();
        this.storageClearNotContactAppointmentIdFromMsg();
        this.$router.push('');
      });
    }
    userReviewsConsultants() {
@@ -263,14 +359,22 @@
        this.appointmentDetail.satisfactionScore = this.inputScore;
        reviewsService.userReviewsConsultants(reviewParams).then((res) => {
            this.reviewsBtn = false;
            this.isShowReviewDialog = false;
        });
    }
    clearSatisfactionId() {
        console.log('close');
        this.$router.push({query: {}});
        this.storageClearSatisfactionIdFromMsg();
    removeUrlQueryParameter(targetKey?: string): void {
        // NOTE: åˆªé™¤ç‰¹å®šçš„ query parameter [Tomas, 2022/1/24 11:36]
        // [REF] How to remove a parameter from this.$router.query Nuxt.js? https://reurl.cc/X45aMD
        let newRouteQuery = {};
        if (targetKey) {
          Object.keys(this.$route.query).forEach((key) => {
            if (key !== targetKey) {
              newRouteQuery[key] = this.$route.query[key]
            }
          })
        }
        this.$router.push(newRouteQuery);
    }
    ///////////////////////////////////////////////////////////////////////////////
@@ -288,6 +392,13 @@
        return contactList.filter((item: any) => !!item && item !== ",")
    }
    get notScoreAppointmentYet(): boolean {
      if (this.appointmentDetail.communicateStatus === 'closed' || this.appointmentDetail.communicateStatus === 'done') {
        return !this.appointmentDetail.satisfactionScore;
      };
      return false;
    }
  }
</script>
PAMapp/pages/myAppointmentList.vue
@@ -1,7 +1,6 @@
<template>
    <div>
      <div class="pam-myAppointment-banner"></div>
    <InterviewNotification></InterviewNotification>
        <div class="pam-container">
            <div class="pam-cus-tabs mb-10">
                <div
PAMapp/pages/notification/index.vue
@@ -1,8 +1,9 @@
<template>
    <div>
        <div class="text--right mb-10" @click="showNotificationHint = true">
        <!-- TODO: å…¨éƒ¨å·²è®€/全部刪除 åŠŸèƒ½æœªå®Œæˆä¸”é ˆéœ€æ±‚å¾…ç¢ºèª å…ˆéš±è— -->
        <!-- <div class="text--right mb-10" @click="showNotificationHint = true">
            <i class="satisfaction-icon icon-edit"></i>
        </div>
        </div> -->
        <div
            v-if="isUserLogin && unReviewLogList.length"
            class="satisfaction-banner my-10 cursor--pointer"
@@ -18,7 +19,8 @@
            align="middle"
            class="notification-card"
        >
            <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col>
            <!-- TODO: å…¨éƒ¨å·²è®€/全部刪除 åŠŸèƒ½æœªå®Œæˆä¸”é ˆéœ€æ±‚å¾…ç¢ºèª å…ˆéš±è— -->
            <!-- <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col> -->
            <el-col :span="18">
                <p class="text">{{item.content}}</p>
            </el-col>
PAMapp/pages/questionnaire/_agentNo.vue
@@ -24,8 +24,12 @@
              <div class="ques-header__input-block">
                  <span>Email:</span>
                  <input class="ques-header__input"
                    :class="{ 'is-invalid': !emailValid}"
                    placeholder="請輸入"
                    v-model="myRequest.email">
              </div>
              <div class="error mt-5 mb-5" style="margin-left:65px">
                  <span v-show="!emailValid">Email格式有誤</span>
              </div>
          </div>
        </div>
@@ -117,19 +121,24 @@
    <PopUpFrame :isOpen.sync="sendReserve" @update:isOpen="closeReservePopUp">
        <div class="mdTxt mt-30 sendReserve-txt">預約成功!</div>
        <div class="mdTxt sendReserve-txt mb-30">您預約的顧問會儘速與您聯絡!</div>
        <div class="pam-app-review mb-10">
        <!-- TODO: æœªä¸²æŽ¥ api, éš±è—å¹³å°æ»¿æ„åº¦ -->
        <!-- <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>
        <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 @click="closeReservePopUp">略過</el-button>
          <el-button type="primary"
            @click="closeReservePopUp">
            é€å‡º
          </el-button> -->
          <el-button type="primary"
            @click="closeReservePopUp">
            æˆ‘知道了
          </el-button>
        </div>
    </PopUpFrame>
@@ -466,6 +475,11 @@
            : true;
    }
    get emailValid() {
      const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
      return this.myRequest.email ? rule.test(this.myRequest.email) : true;
    }
    get userInfo(): RegisterInfo {
      const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!);
      return initUserInfo;
@@ -473,7 +487,7 @@
    get isDisabledSubmitBtn(): boolean {
           return _.includes(this.myRequest.contactType,ContactType.PHONE)
      ? !this.isHopeContactTimeDone()
      ? !this.isHopeContactTimeDone() || !this.emailValid
      : !this.phoneValid;
    }
PAMapp/pages/satisfactionList.vue
@@ -14,18 +14,32 @@
              çš„æ•´é«”服務,您給予幾顆星評價?
          </div>
        </div>
        <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"></el-rate>
        <el-rate
          v-model="item.satisfaction"
          class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"
          @change="isBtnDisabled = false"
        ></el-rate>
      </div>
      <div class="text--center mt-30">
        <el-button type="primary" :disabled="isBtnDisabled">送出</el-button>
      <div class="text--center mt-30" v-if="mapUnReviewLogList.length">
        <el-button type="primary" :disabled="isBtnDisabled" @click="sent">送出</el-button>
      </div>
    </div>
    <PopUpFrame :isOpen.sync="showConfirmPopup"
        @closePopUp="closePopup">
        <div class="text--center mdTxt">發送成功</div>
        <div class="text--center mt-30">
            <el-button @click="closePopup" type="primary">確定</el-button>
        </div>
      </PopUpFrame>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Action, State, Watch } from 'nuxt-property-decorator';
import { AppointmentLog } from '~/shared/models/appointment.model';
import { UserReviewsConsultantsParams } from '~/shared/models/reviews.model';
import reviewsService from '~/shared/services/reviews.service';
@Component({
  layout: 'home'
@@ -35,7 +49,12 @@
  @State
  unReviewLogList!: AppointmentLog[];
  @Action
  storeMyAppointmentReviewLog!: () => void;
  mapUnReviewLogList: AppointmentReviewLog[] = [];
  showConfirmPopup = false;
  isBtnDisabled = true;
  ///////////////////////////////////////////////////////
@@ -53,12 +72,26 @@
  ///////////////////////////////////////////////////////
  get isBtnDisabled() {
    if (this.mapUnReviewLogList.length) {
      return this.mapUnReviewLogList.findIndex(item => item.satisfaction > 0) === -1;
    }
    return false;
  sent() {
    const reviewParams: UserReviewsConsultantsParams[] = this.mapUnReviewLogList
                .filter(item => item.satisfaction > 0)
                .map(item => {
                  return {
                    appointmentId: item.appointmentId,
                    score: item.satisfaction
                  }
                })
        reviewsService.allUserReviewsConsultants(reviewParams).then((res) => {
            this.showConfirmPopup = true;
        });
  }
  closePopup() {
    this.showConfirmPopup = false;
    this.storeMyAppointmentReviewLog();
  }
}
interface AppointmentReviewLog extends AppointmentLog {
PAMapp/shared/models/strict-query.model.ts
@@ -1,14 +1,14 @@
export interface StrictQueryParams {
  gender          : string;
  avgScore        : number;
  status          : string;    //phase 1 disable
  area            : string;
  requirements    : string[];
  otherRequirement: string;
  seniority       : string;
  popularTags     : string[];
  otherPopularTags: string;
  gender?          : string;
  avgScore?        : number;
  status?          : string;    //phase 1 disable
  area?            : string;
  requirements?    : string[];
  otherRequirement?: string;
  seniority?       : string;
  popularTags?     : string[];
  otherPopularTags?: string;
}
export interface AgentOfStrictQuery {
PAMapp/shared/services/httpClient.ts
@@ -16,8 +16,11 @@
  withCredentials: true
});
let apiNumber = 0;
http.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    apiNumber += 1;
    loadingStart();
    addHttpHeader(config);
    return config;
@@ -26,11 +29,17 @@
http.interceptors.response.use(
  (response: AxiosResponse) => {
    loadingFinish();
    apiNumber -= 1;
    if (apiNumber === 0) {
      loadingFinish();
    }
    return response;
  },
  (error: AxiosError) => {
    loadingFinish();
    apiNumber -= 1;
    if (apiNumber === 0) {
      loadingFinish();
    }
    showErrorMessageBox(error)
    return Promise.reject(error);
  }
PAMapp/shared/services/reviews.service.ts
@@ -5,11 +5,17 @@
class ReviewsService {
  //客戶進行滿意度評分
  //客戶進行滿意度評分(單筆)
  userReviewsConsultants(data: UserReviewsConsultantsParams) {
    return http.post('/satisfaction/score', data );
    return http.post('/satisfaction/score', data);
  }
  // å®¢æˆ¶é€²è¡Œæ»¿æ„åº¦(多筆)
  allUserReviewsConsultants(data: UserReviewsConsultantsParams[]) {
    return http.post('/satisfaction/score/all', data);
  }
  //取得所有評分紀錄
  async getMyReviewLog(): Promise<AppointmentLog[]> {
    return http.get('/satisfaction/getMySatisfaction').then(res => res.data);
PAMapp/store/localStorage.ts
@@ -1,6 +1,7 @@
import { Module, Mutation, VuexModule ,Action } from 'vuex-module-decorators';
import { Role } from '~/shared/models/enum/Role';
import { Selected } from '~/shared/models/quick-filter.model';
import { StrictQueryParams } from '~/shared/models/strict-query.model';
@Module
export default class LocalStorage extends VuexModule {
  id_token = localStorage.getItem('id_token');
@@ -10,6 +11,7 @@
  recommendConsultantItem = localStorage.getItem('recommendConsultantItem');
  appointmentIdFromMsg = localStorage.getItem('appointmentIdFromMsg');
  satisfactionIdFromMsg = localStorage.getItem('satisfactionIdFromMsg');
  notContactAppointmentIdFromMsg = localStorage.getItem('notContactAppointmentIdFromMsg');
  get idToken(): string|null {
    return this.id_token;
@@ -41,6 +43,10 @@
  get currentSatisfactionIdFromMsg(): string|null {
    return this.satisfactionIdFromMsg;
  }
  get currentNotContactAppointmentIdFromMsg(): string|null {
    return this.notContactAppointmentIdFromMsg;
  }
  @Mutation storageIdToken(token: string): void {
@@ -78,6 +84,11 @@
    this.satisfactionIdFromMsg = localStorage.getItem('satisfactionIdFromMsg');
  }
  @Mutation storageNotContactAppointmentIdFromMsg(id: string) {
    localStorage.setItem('notContactAppointmentIdFromMsg', id);
    this.notContactAppointmentIdFromMsg = id;
  }
  @Mutation storageClear(): void {
    localStorage.removeItem('myRequests');
    localStorage.removeItem('userInfo');
@@ -112,6 +123,16 @@
    this.appointmentIdFromMsg = localStorage.getItem('satisfactionIdFromMsg');
  }
  @Mutation storageClearNotContactAppointmentIdFromMsg() {
    localStorage.removeItem('notContactAppointmentIdFromMsg');
    this.appointmentIdFromMsg = localStorage.getItem('notContactAppointmentIdFromMsg');
  }
  @Mutation storageStrickQueryItem(queryItem: StrictQueryParams): void {
    localStorage.setItem('recommendConsultantItem', JSON.stringify(queryItem));
    this.recommendConsultantItem = localStorage.getItem('recommendConsultantItem');
  }
  @Action actionStorageClear(): void {
    this.context.commit("storageClear");
  }
pamapi/src/doc/sql/20220122_w.sql
@@ -2,5 +2,5 @@
   id bigserial NOT NULL,
   appointment_id bigserial NOT NULL,
   send_time timestamp NULL,
   CONSTRAINT appointment_pending_notify_record_pk PRIMARY KEY (id)
   CONSTRAINT appointment_expiring_notify_record_pk PRIMARY KEY (id)
);
pamapi/src/doc/sql/²bªÅ¾ã­Ó¨t²Î¸ê®Æ(°£ÅU°Ý).sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
truncate public.appointment;
truncate public.appointment_closed_info;
truncate public.appointment_expiring_notify_record;
truncate public.appointment_memo;
truncate public.appointment_notice_log;
truncate public.customer;
truncate public.interview_record;
truncate public.login_record;
truncate public.otp_tmp;
truncate public.personal_notification;
truncate public.satisfaction;
truncate public.customer_favorite_consultant;
pamapi/src/doc/¹w¬ù³æ/«È¤á¨ú±o³Ì·s¹w¬ùªº¥¼³B²z¹w¬ù³æ.txt
@@ -4,7 +4,7 @@
簡訊及email會以該網址進入首頁 -> http://localhost:3000?notContactAppointmentId={最新一筆未處理預約單}
response body: è‹¥æœ‰æœƒå‚³200並給以下資料,若無(未有任何逾期未處理預約單,則會回404)
response body: è‹¥æœ‰æœƒå‚³200並給以下資料,若無(未有任何逾期未處理預約單,則會回null)
{
    "id": 385,
    "phone": "0911223344",
pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java
@@ -1,5 +1,6 @@
package com.pollex.pam.config;
import com.pollex.pam.enums.SendEmailMsgMethod;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
@@ -19,7 +20,6 @@
    private String eServiceLoginFunc;
    private String eServiceLoginSys;
    private String frontEndDomain;
    private boolean sendNotifyMsg;
    private SMS sms;
    private Email email;
    private String fileFolderPath;
@@ -88,14 +88,6 @@
        this.frontEndDomain = frontEndDomain;
    }
    public boolean isSendNotifyMsg() {
        return sendNotifyMsg;
    }
    public void setSendNotifyMsg(boolean sendNotifyMsg) {
        this.sendNotifyMsg = sendNotifyMsg;
    }
    public SMS getSms() {
        return sms;
    }
@@ -118,6 +110,7 @@
        private String sender;
        private String smsType;
        private String subject;
        private boolean sendNotifyMsg;
        public String getUrl() {
            return url;
@@ -158,12 +151,22 @@
        public void setSubject(String subject) {
            this.subject = subject;
        }
        public boolean isSendNotifyMsg() {
            return sendNotifyMsg;
        }
        public void setSendNotifyMsg(boolean sendNotifyMsg) {
            this.sendNotifyMsg = sendNotifyMsg;
        }
    }
    public static class Email {
        private String url;
        private String functionId;
        private String senderEmail;
        private boolean sendNotifyMsg;
        private SendEmailMsgMethod method;
        public String getUrl() {
            return url;
@@ -188,6 +191,22 @@
        public void setSenderEmail(String senderEmail) {
            this.senderEmail = senderEmail;
        }
        public boolean isSendNotifyMsg() {
            return sendNotifyMsg;
        }
        public void setSendNotifyMsg(boolean sendNotifyMsg) {
            this.sendNotifyMsg = sendNotifyMsg;
        }
        public SendEmailMsgMethod getMethod() {
            return method;
        }
        public void setMethod(SendEmailMsgMethod method) {
            this.method = method;
        }
    }
    public String getFileFolderPath() {
        return fileFolderPath;
pamapi/src/main/java/com/pollex/pam/config/Constants.java
@@ -30,5 +30,7 @@
     */
    public static final int SEND_EXPIRING_NOTIFY_LIMIT = 1;
    public static final String SPRING_PROFILE_POLLEX_DEVELOPMENT = "pollex";
    private Constants() {}
}
pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum SendEmailMsgMethod {
    PAM_EMAIL_SERVICE,
    POLLEX_GMAIL
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
@@ -200,7 +200,8 @@
    public void sendAppointmentNotify(Appointment appointment) {
        Assert.notNull(appointment, "appointment entity cannot be null");
        log.debug("is need send appointment notify msg? = {}", applicationProperties.isSendNotifyMsg());
        log.debug("is need send appointment notify msg? sms = {}, email = {}",
            applicationProperties.getSms().isSendNotifyMsg(), applicationProperties.getEmail().isSendNotifyMsg());
        log.debug("sending appointment notify, appointmentId = {}", appointment.getId());
        sendAppointmentNotifyBySMS(appointment);
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
@@ -73,12 +73,17 @@
        consultantWithPendingAppointments.forEach((agentNo, pendingAppointments) -> {
            int pendingAppointmentsSum = pendingAppointments.size();
            Consultant consultant = consultantService.findByAgentNo(agentNo);
            String consultantPhoneNumber = consultant.getPhoneNumber();
            String consultantEmail = consultant.getEmail();
            Optional<String> optionalPhone = Optional.ofNullable(consultant.getPhoneNumber()).filter(StringUtils::hasText);
            Optional<String> optionalEmail = Optional.ofNullable(consultant.getEmail()).filter(StringUtils::hasText);
            String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum);
            sendMsgService.sendMsgBySMS(consultantPhoneNumber, String.format("您有%s則預約單未進行聯繫,請盡速處理", pendingAppointmentsSum));
            sendMsgService.sendMsgByEmail(consultantEmail, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true);
            optionalPhone.ifPresent(phone -> {
                sendMsgService.sendMsgBySMS(phone, String.format("您有%s則預約單未進行聯繫,請盡速處理", pendingAppointmentsSum));
            });
            optionalEmail.ifPresent(email -> {
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true);
            });
        });
        log.info("Sending appointment pending notify to consultant finish");
@@ -94,7 +99,7 @@
                .filter(appointment ->
                    appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL)
                )
                .filter(this::isAppointmentNotifyNotOnLimit)
                .filter(this::isAppointmentExpiringNotifyNotOnLimit)
                .collect(Collectors.toList());
        allByCommunicateStatus.forEach(appointment -> {
@@ -104,10 +109,10 @@
            optionalPhone.ifPresent(phone ->
                sendMsgService.sendMsgBySMS(phone, String.format("很抱歉!您預約%s顧問正忙碌中,請您取消預約並改選其他顧問,請點擊網址:%s"
                    , consultant.getName(), getAppointmentUrl(appointment.getId())))
                    , consultant.getName(), getAppointmentExpiringNotifyUrl(appointment.getId())))
            );
            optionalEmail.ifPresent(email ->
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentUrl(appointment.getId())), true)
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentExpiringNotifyUrl(appointment.getId())), true)
            );
            AppointmentExpiringNotifyRecord record = new AppointmentExpiringNotifyRecord();
@@ -121,7 +126,7 @@
    }
    // todo éœ€ç¢ºèªè©²æ™‚é–“, otis todo=134497
    @Scheduled(cron = "0 0 9 * * *")
    @Scheduled(cron = "0 30 8 * * *")
    public void sendNotFillSatisfactionToPersonalNotification() {
        Map<Long, List<Satisfaction>> customerNotFillSatisfactions = satisfactionService.getByStatus(SatisfactionStatusEnum.UNFILLED)
                .stream()
@@ -132,14 +137,14 @@
        );
    }
    private boolean isAppointmentNotifyNotOnLimit(AppointmentCustomerView appointment) {
    private boolean isAppointmentExpiringNotifyNotOnLimit(AppointmentCustomerView appointment) {
        int sendNotifyToCustomerRecordSum =
            appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size();
        return sendNotifyToCustomerRecordSum < Constants.SEND_EXPIRING_NOTIFY_LIMIT;
    }
    private String getAppointmentUrl(Long appointmentId) {
    private String getAppointmentExpiringNotifyUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId;
    }
pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java
@@ -2,7 +2,10 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.config.ApplicationProperties.Email;
import com.pollex.pam.config.ApplicationProperties.SMS;
import com.pollex.pam.config.Constants;
import com.pollex.pam.enums.SendEmailMsgMethod;
import com.pollex.pam.repository.ConsultantRepository;
import com.pollex.pam.service.dto.*;
import com.pollex.pam.service.util.HttpRequestUtil;
@@ -11,9 +14,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.thymeleaf.spring5.SpringTemplateEngine;
import tech.jhipster.config.JHipsterConstants;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
@@ -38,13 +44,19 @@
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    Environment environment;
    @Autowired
    MailService mailService;
    public SendSMSResponse sendMsgBySMS(String toMobile, String content) throws SendSMSFailException {
        if(!applicationProperties.isSendNotifyMsg()) {
        SMS smsProperties = applicationProperties.getSms();
        if(!smsProperties.isSendNotifyMsg()) {
//            return getMockSMSResponse();
            return null;
        }
        SMS smsProperties = applicationProperties.getSms();
            return null;
        }
        SendSMSRequest sendSMSRequest = new SendSMSRequest();
        sendSMSRequest.setpKey(UUID.randomUUID().toString());
@@ -94,7 +106,7 @@
    public String sendMsgByEmail(String toAddress, String subject, String content, boolean htmlFormat, List<String> toCCAddress,
        List<String> attachments) throws SendEmailFailException {
        String fromAddress = applicationProperties.getEmail().getSenderEmail();
        SendMailRequest sendMailRequest = new SendMailRequest();
        sendMailRequest.setSendMailAddresses(Collections.singletonList(toAddress));
        sendMailRequest.setFrom(fromAddress);
@@ -109,25 +121,48 @@
    }
    public String sendMsgByEmail(SendMailRequest sendMailRequest) throws SendEmailFailException{
        if(!applicationProperties.isSendNotifyMsg()) {
            return null;
        }
        try {
        final Email emailProperties = applicationProperties.getEmail();
        if(!emailProperties.isSendNotifyMsg()) {
            return null;
        }
        if(emailProperties.getMethod() == SendEmailMsgMethod.POLLEX_GMAIL) {
            return sendMsgByPollexGmail(sendMailRequest);
        }
        else if(emailProperties.getMethod() == SendEmailMsgMethod.PAM_EMAIL_SERVICE) {
            return sendMsgByPamEmailService(sendMailRequest);
        }
        return null;
    }
    private String sendMsgByPollexGmail(SendMailRequest sendMailRequest) {
        String subject = sendMailRequest.getSubject();
        String content = sendMailRequest.getContent();
        boolean isHtml = sendMailRequest.isHtmlFormat();
        sendMailRequest.getSendMailAddresses().forEach(receiver -> mailService.sendEmail(receiver, subject, content, false, isHtml));
        return null;
    }
    private String sendMsgByPamEmailService(SendMailRequest sendMailRequest) {
        final Email emailProperties = applicationProperties.getEmail();
        try {
            ResponseEntity<String> responseEntity =
                HttpRequestUtil.postWithJson( applicationProperties.getEmail().getUrl(), sendMailRequest, String.class);
                HttpRequestUtil.postWithJson(emailProperties.getUrl(), sendMailRequest, String.class);
            log.debug("responseEntity = {}", responseEntity);
            String rawResponseString = responseEntity.getBody();
            SendMailResponse sendMailResponse = new ObjectMapper().readValue(rawResponseString, SendMailResponse.class);
            log.debug("sendMailResponse = {}", sendMailResponse);
            if(sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) {
            if (sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) {
                throw new SendEmailFailException("send email service return error msg! raw response string= " + rawResponseString);
            }
            return responseEntity.getBody();
        }
        catch (SendEmailFailException e) {
        } catch (SendEmailFailException e) {
            throw e;
        } catch (Exception e) {
            log.warn("send email fail by other reason", e);
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
@@ -86,12 +86,7 @@
        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);
        }
        return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK);
    }
    @GetMapping("/consultant/pending/sum")
pamapi/src/main/resources/config/application-dev.yml
@@ -45,10 +45,16 @@
    # Remove 'faker' if you do not want the sample data to be loaded automatically
    contexts: dev, faker
  mail:
    host: localhost
    port: 25
    username:
    password:
    host: smtp.gmail.com
    port: 587
    username: pollex.testing@gmail.com
    password: ilismmmhtscppxft
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  messages:
    cache-duration: PT1S # 1 second, see the ISO 8601 standard
  thymeleaf:
@@ -120,15 +126,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: http://localhost:3000
  send-notify-msg: false
  sms:
    send-notify-msg: false
    url: https://localhost:8081/testSMS
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: false
    url: https://localhost:8081/testEmail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'POLLEX_GMAIL'
  file-folder-path: C://pam_file
pamapi/src/main/resources/config/application-pollex.yml
@@ -43,10 +43,16 @@
    # Remove 'faker' if you do not want the sample data to be loaded automatically
    contexts: pollex, faker
  mail:
    host: localhost
    port: 25
    username:
    password:
    host: smtp.gmail.com
    port: 587
    username: pollex.testing@gmail.com
    password: ilismmmhtscppxft
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  messages:
    cache-duration: PT1S # 1 second, see the ISO 8601 standard
  thymeleaf:
@@ -118,15 +124,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: http://dev.pollex.com.tw:5566/pam
  send-notify-msg: false
  sms:
    send-notify-msg: false
    url: https://localhost:8081/testSMS
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://localhost:8081/testEmail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'POLLEX_GMAIL'
  file-folder-path: C://pam_file
pamapi/src/main/resources/config/application-prod.yml
@@ -140,15 +140,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: https://vtwlifeopensysuat.pru.intranet.asia/pam
  send-notify-msg: true
  sms:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'PAM_EMAIL_SERVICE'
  file-folder-path: /sfs_omo/vtwlifewpsfs01/SensitiveData4AP$/OMO
pamapi/src/main/resources/config/application-sit.yml
@@ -118,15 +118,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: https://vtwlifeopensyssit.pru.intranet.asia/pam
  send-notify-msg: true
  sms:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'PAM_EMAIL_SERVICE'
  file-folder-path: /sfs_omo/vtwlifewuftp66/sensitivedata4ap$/OMOSIT
pamapi/src/main/resources/config/application-uat.yml
@@ -118,15 +118,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: https://vtwlifeopensysuat.pru.intranet.asia/pam
  send-notify-msg: true
  sms:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'PAM_EMAIL_SERVICE'
  file-folder-path: /sfs_omo/vtwlifewuftp66/sensitivedata4ap$/OMO