From 21d2b51910f1e1e338beed76d53effcfccb1ef7a Mon Sep 17 00:00:00 2001
From: Jack <jack.su@pollex.com.tw>
Date: 星期一, 24 一月 2022 16:47:57 +0800
Subject: [PATCH] Merge branch 'Phase3' of ssh://dev.pollex.com.tw:29418/pcalife/PAM into Phase3

---
 pamapi/src/main/java/com/pollex/pam/config/Constants.java             |    2 
 pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java |    7 
 PAMapp/shared/services/httpClient.ts                                  |   13 
 pamapi/src/doc/sql/淨空整個系統資料(除顧問).sql                                  |   12 
 PAMapp/assets/scss/utilities/_utilities.scss                          |    5 
 pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java     |    6 
 PAMapp/components/DateTimePicker.vue                                  |    5 
 pamapi/src/doc/預約單/客戶取得最新預約的未處理預約單.txt                                |    2 
 pamapi/src/main/resources/config/application-sit.yml                  |    4 
 PAMapp/pages/index.vue                                                |  231 +++++++++++++----
 pamapi/src/doc/sql/20220122_w.sql                                     |    2 
 pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java |   37 ++
 pamapi/src/main/resources/config/application-dev.yml                  |   18 +
 PAMapp/components/Ui/UiTimePicker.vue                                 |   33 ++
 PAMapp/pages/questionnaire/_agentNo.vue                               |   22 +
 PAMapp/components/Ui/UiDatePicker.vue                                 |   20 +
 PAMapp/middleware/getUrlQuery.ts                                      |   12 
 PAMapp/pages/myAppointmentList.vue                                    |    1 
 PAMapp/pages/satisfactionList.vue                                     |   49 +++
 pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java  |   25 +
 PAMapp/pages/appointment/_appointmentId/close/index.vue               |    1 
 PAMapp/components/BackActionBar.vue                                   |    3 
 PAMapp/components/Consultant/ConsultantCard.vue                       |   25 +
 /dev/null                                                             |   58 ----
 pamapi/src/main/resources/config/application-pollex.yml               |   18 +
 pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java   |    3 
 PAMapp/components/Client/ClientCard.vue                               |   23 +
 PAMapp/shared/services/reviews.service.ts                             |   10 
 PAMapp/store/localStorage.ts                                          |   21 +
 pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java       |   63 +++-
 PAMapp/shared/models/strict-query.model.ts                            |   18 
 pamapi/src/main/resources/config/application-uat.yml                  |    4 
 PAMapp/assets/scss/vendors/elementUI/_rate.scss                       |    1 
 PAMapp/pages/notification/index.vue                                   |    8 
 PAMapp/assets/scss/vendors/elementUI/_select.scss                     |    3 
 pamapi/src/main/resources/config/application-prod.yml                 |    4 
 36 files changed, 540 insertions(+), 229 deletions(-)

diff --git a/PAMapp/assets/scss/utilities/_utilities.scss b/PAMapp/assets/scss/utilities/_utilities.scss
index 04b3932..ebf13d7 100644
--- a/PAMapp/assets/scss/utilities/_utilities.scss
+++ b/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;
 }
diff --git a/PAMapp/assets/scss/vendors/elementUI/_rate.scss b/PAMapp/assets/scss/vendors/elementUI/_rate.scss
index 48909a5..571b574 100644
--- a/PAMapp/assets/scss/vendors/elementUI/_rate.scss
+++ b/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;
diff --git a/PAMapp/assets/scss/vendors/elementUI/_select.scss b/PAMapp/assets/scss/vendors/elementUI/_select.scss
index 515da4a..815b184 100644
--- a/PAMapp/assets/scss/vendors/elementUI/_select.scss
+++ b/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";
+    }
   }
 }
 
diff --git a/PAMapp/components/BackActionBar.vue b/PAMapp/components/BackActionBar.vue
index f7981a0..29fc5ee 100644
--- a/PAMapp/components/BackActionBar.vue
+++ b/PAMapp/components/BackActionBar.vue
@@ -74,9 +74,6 @@
         case 'accountSetting':
           featureLabel = '�犖撣唾�身摰�';
           break;
-        case 'appointmentAgenda':
-          featureLabel = '�撠�赤����';
-          break;
         case 'consultantAccountSetting':
           featureLabel = '���董�����';
           break;
diff --git a/PAMapp/components/Client/ClientCard.vue b/PAMapp/components/Client/ClientCard.vue
index 294eb5f..0ff9748 100644
--- a/PAMapp/components/Client/ClientCard.vue
+++ b/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() {
diff --git a/PAMapp/components/Consultant/ConsultantCard.vue b/PAMapp/components/Consultant/ConsultantCard.vue
index f7198f6..8ea5a64 100644
--- a/PAMapp/components/Consultant/ConsultantCard.vue
+++ b/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;
diff --git a/PAMapp/components/DateTimePicker.vue b/PAMapp/components/DateTimePicker.vue
index f2a8763..584df86 100644
--- a/PAMapp/components/DateTimePicker.vue
+++ b/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') {
diff --git a/PAMapp/components/Interview/interviewNotification.vue b/PAMapp/components/Interview/interviewNotification.vue
deleted file mode 100644
index 9bb6253..0000000
--- a/PAMapp/components/Interview/interviewNotification.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-    <div class="remind-card">
-        <div class="remind-card-header">
-            <div class="mdTxt text--black">�撠�赤����</div>
-            <div class="smTxt_bold text--primary sub-title cursor--pointer" @click="toAgendaPage">����</div>
-        </div>
-
-        <div class="remind-container">
-            <div class="remind-date mr-10">
-              <div class="mb-3 smTxt bgc-primary-red date-year">
-                <div class="mb-3 mt-2">2021</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>
-
-<script lang="ts">
-import { Vue, Component, Prop, Emit, Action, State, namespace } from 'nuxt-property-decorator';
-
-@Component
-export default class ConsultantAppointmentRemind extends Vue {
-
-  toAgendaPage(){
-    this.$router.push('/appointmentAgenda')
-  }
-    //////////////////////////////////////////////////////////////////////
-}
-</script>
-<style lang="scss" scoped>
-.remind-card{
-    display: flex;
-    flex-direction:column;
-    justify-content: center;
-    border: 1px solid #CCCCCC;
-    height: 131px;
-    padding: 10px 20px;
-    background-color: #fff;
-    .remind-card-header{
-        display: flex;
-        justify-content: space-between;
-        align-items: baseline;
-        .sub-title{
-            border-bottom: 1px solid $PRIMARY_RED;
-        }
-    }
-}
-
-</style>
diff --git a/PAMapp/components/Ui/UiDatePicker.vue b/PAMapp/components/Ui/UiDatePicker.vue
index 20132bc..025594a 100644
--- a/PAMapp/components/Ui/UiDatePicker.vue
+++ b/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();
+                }
+            }
+        }
     }
 
 }
diff --git a/PAMapp/components/Ui/UiTimePicker.vue b/PAMapp/components/Ui/UiTimePicker.vue
index b661e00..291fbcd 100644
--- a/PAMapp/components/Ui/UiTimePicker.vue
+++ b/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();
         }
diff --git a/PAMapp/middleware/getUrlQuery.ts b/PAMapp/middleware/getUrlQuery.ts
index 0a6c0ec..5fb2841 100644
--- a/PAMapp/middleware/getUrlQuery.ts
+++ b/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
\ No newline at end of file
+export default getUrlQuery
diff --git a/PAMapp/pages/appointment/_appointmentId/close/index.vue b/PAMapp/pages/appointment/_appointmentId/close/index.vue
index d10476c..a267a72 100644
--- a/PAMapp/pages/appointment/_appointmentId/close/index.vue
+++ b/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>
diff --git a/PAMapp/pages/index.vue b/PAMapp/pages/index.vue
index 93084d4..286c1ff 100644
--- a/PAMapp/pages/index.vue
+++ b/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>
diff --git a/PAMapp/pages/myAppointmentList.vue b/PAMapp/pages/myAppointmentList.vue
index 5045e5f..c013cd9 100644
--- a/PAMapp/pages/myAppointmentList.vue
+++ b/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
diff --git a/PAMapp/pages/notification/index.vue b/PAMapp/pages/notification/index.vue
index 1dcdba1..f79907c 100644
--- a/PAMapp/pages/notification/index.vue
+++ b/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>
diff --git a/PAMapp/pages/questionnaire/_agentNo.vue b/PAMapp/pages/questionnaire/_agentNo.vue
index cc1ccc5..120c844 100644
--- a/PAMapp/pages/questionnaire/_agentNo.vue
+++ b/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;
     }
 
diff --git a/PAMapp/pages/satisfactionList.vue b/PAMapp/pages/satisfactionList.vue
index a6bae69..8d52771 100644
--- a/PAMapp/pages/satisfactionList.vue
+++ b/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 {
diff --git a/PAMapp/shared/models/strict-query.model.ts b/PAMapp/shared/models/strict-query.model.ts
index 59892c1..45a5f31 100644
--- a/PAMapp/shared/models/strict-query.model.ts
+++ b/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 {
diff --git a/PAMapp/shared/services/httpClient.ts b/PAMapp/shared/services/httpClient.ts
index e524ddf..bafed6e 100644
--- a/PAMapp/shared/services/httpClient.ts
+++ b/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);
   }
diff --git a/PAMapp/shared/services/reviews.service.ts b/PAMapp/shared/services/reviews.service.ts
index 44c51d9..4bd81df 100644
--- a/PAMapp/shared/services/reviews.service.ts
+++ b/PAMapp/shared/services/reviews.service.ts
@@ -5,11 +5,17 @@
 
 class ReviewsService {
 
-  //摰X�脰�遛��漲閰��
+  //摰X�脰�遛��漲閰��(�蝑�)
   userReviewsConsultants(data: UserReviewsConsultantsParams) {
-    return http.post('/satisfaction/score', data );
+    return http.post('/satisfaction/score', data);
   }
 
+  // 摰X�脰�遛��漲(憭��)
+  allUserReviewsConsultants(data: UserReviewsConsultantsParams[]) {
+    return http.post('/satisfaction/score/all', data);
+  }
+
+
   //������������
   async getMyReviewLog(): Promise<AppointmentLog[]> {
     return http.get('/satisfaction/getMySatisfaction').then(res => res.data);
diff --git a/PAMapp/store/localStorage.ts b/PAMapp/store/localStorage.ts
index 9ad38e9..a08b69a 100644
--- a/PAMapp/store/localStorage.ts
+++ b/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");
   }
diff --git a/pamapi/src/doc/sql/20220122_w.sql b/pamapi/src/doc/sql/20220122_w.sql
index 1acd577..39a28b8 100644
--- a/pamapi/src/doc/sql/20220122_w.sql
+++ b/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)
 );
diff --git "a/pamapi/src/doc/sql/\346\267\250\347\251\272\346\225\264\345\200\213\347\263\273\347\265\261\350\263\207\346\226\231\050\351\231\244\351\241\247\345\225\217\051.sql" "b/pamapi/src/doc/sql/\346\267\250\347\251\272\346\225\264\345\200\213\347\263\273\347\265\261\350\263\207\346\226\231\050\351\231\244\351\241\247\345\225\217\051.sql"
new file mode 100644
index 0000000..963ed9f
--- /dev/null
+++ "b/pamapi/src/doc/sql/\346\267\250\347\251\272\346\225\264\345\200\213\347\263\273\347\265\261\350\263\207\346\226\231\050\351\231\244\351\241\247\345\225\217\051.sql"
@@ -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;
diff --git "a/pamapi/src/doc/\351\240\220\347\264\204\345\226\256/\345\256\242\346\210\266\345\217\226\345\276\227\346\234\200\346\226\260\351\240\220\347\264\204\347\232\204\346\234\252\350\231\225\347\220\206\351\240\220\347\264\204\345\226\256.txt" "b/pamapi/src/doc/\351\240\220\347\264\204\345\226\256/\345\256\242\346\210\266\345\217\226\345\276\227\346\234\200\346\226\260\351\240\220\347\264\204\347\232\204\346\234\252\350\231\225\347\220\206\351\240\220\347\264\204\345\226\256.txt"
index d31a4ea..f4e02f9 100644
--- "a/pamapi/src/doc/\351\240\220\347\264\204\345\226\256/\345\256\242\346\210\266\345\217\226\345\276\227\346\234\200\346\226\260\351\240\220\347\264\204\347\232\204\346\234\252\350\231\225\347\220\206\351\240\220\347\264\204\345\226\256.txt"
+++ "b/pamapi/src/doc/\351\240\220\347\264\204\345\226\256/\345\256\242\346\210\266\345\217\226\345\276\227\346\234\200\346\226\260\351\240\220\347\264\204\347\232\204\346\234\252\350\231\225\347\220\206\351\240\220\347\264\204\345\226\256.txt"
@@ -4,7 +4,7 @@
 
 蝪∟��mail��誑閰脩雯���脣擐�� -> http://localhost:3000?notContactAppointmentId={���銝�蝑�����}
 
-response body: ����200銝衣策隞乩�����(���遙雿�暹������嚗����404)
+response body: ����200銝衣策隞乩�����(���遙雿�暹������嚗���ull)
 {
     "id": 385,
     "phone": "0911223344",
diff --git a/pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java b/pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java
index d4b2b36..60b33ca 100644
--- a/pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java
+++ b/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;
diff --git a/pamapi/src/main/java/com/pollex/pam/config/Constants.java b/pamapi/src/main/java/com/pollex/pam/config/Constants.java
index aae9b67..87dd25b 100644
--- a/pamapi/src/main/java/com/pollex/pam/config/Constants.java
+++ b/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() {}
 }
diff --git a/pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java b/pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java
new file mode 100644
index 0000000..42a10ce
--- /dev/null
+++ b/pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java
@@ -0,0 +1,6 @@
+package com.pollex.pam.enums;
+
+public enum SendEmailMsgMethod {
+    PAM_EMAIL_SERVICE,
+    POLLEX_GMAIL
+}
diff --git a/pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java b/pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
index 6d5ded6..3ae99c4 100644
--- a/pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
+++ b/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);
diff --git a/pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java b/pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
index 604479e..57324b5 100644
--- a/pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
+++ b/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;
     }
 
diff --git a/pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java b/pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java
index 05d8d73..a5be71e 100644
--- a/pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java
+++ b/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);
diff --git a/pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java b/pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
index ac19b72..6ac1d5f 100644
--- a/pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
+++ b/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")
diff --git a/pamapi/src/main/resources/config/application-dev.yml b/pamapi/src/main/resources/config/application-dev.yml
index ced80df..e612913 100644
--- a/pamapi/src/main/resources/config/application-dev.yml
+++ b/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
diff --git a/pamapi/src/main/resources/config/application-pollex.yml b/pamapi/src/main/resources/config/application-pollex.yml
index e1b1955..e6a2ae8 100644
--- a/pamapi/src/main/resources/config/application-pollex.yml
+++ b/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
diff --git a/pamapi/src/main/resources/config/application-prod.yml b/pamapi/src/main/resources/config/application-prod.yml
index 71773a9..90ff8b7 100644
--- a/pamapi/src/main/resources/config/application-prod.yml
+++ b/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
diff --git a/pamapi/src/main/resources/config/application-sit.yml b/pamapi/src/main/resources/config/application-sit.yml
index 4f4c25c..402dcef 100644
--- a/pamapi/src/main/resources/config/application-sit.yml
+++ b/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
diff --git a/pamapi/src/main/resources/config/application-uat.yml b/pamapi/src/main/resources/config/application-uat.yml
index 15f5390..28d8c16 100644
--- a/pamapi/src/main/resources/config/application-uat.yml
+++ b/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

--
Gitblit v1.8.0