Merge branch 'Phase3' of ssh://dev.pollex.com.tw:29418/pcalife/PAM into Phase3
| | |
| | | .icon-circle-right:before { |
| | | content: "\e90b"; |
| | | } |
| | | .icon-down:before, .el-icon-arrow-up:before { |
| | | content: "\e910"; |
| | | .icon-close:before { |
| | | content: "\e90c"; |
| | | } |
| | | .icon-comment:before { |
| | | content: "\e90d"; |
¤ñ¹ï·sÀÉ®× |
| | |
| | | <svg id="Component_29" data-name="Component 29" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"> |
| | | <circle id="Ellipse_726" data-name="Ellipse 726" cx="50" cy="50" r="50" fill="#feecdc"/> |
| | | <g id="Group_3685" data-name="Group 3685" transform="translate(-4269 -1812)"> |
| | | <circle id="Ellipse_723" data-name="Ellipse 723" cx="50" cy="50" r="50" transform="translate(4269 1812)" fill="#feecdc"/> |
| | | <path id="Intersection_4" data-name="Intersection 4" d="M8376.456,8861.851v-2.594c0-14.428,24.235-18.738,38.724-18.738s38.367,4.313,38.367,18.738v2.59A49.9,49.9,0,0,1,8415.013,8880H8415A49.892,49.892,0,0,1,8376.456,8861.851Zm17.021-48.454a21.7,21.7,0,0,1,21.7-21.686h.014a21.693,21.693,0,1,1-21.716,21.686Z" transform="translate(-4095.456 -6968)" fill="#d0d0ce" opacity="0.789"/> |
| | | <circle id="Ellipse_724" data-name="Ellipse 724" cx="15" cy="15" r="15" transform="translate(4334 1882)" fill="none"/> |
| | | <path id="female" d="M14.817,13.279l1.116,1.11c.457.457.92.914,1.377,1.383a.856.856,0,0,1-1.235,1.187c-.665-.653-1.324-1.318-1.983-1.977l-.516-.522a.938.938,0,0,1-.107.16l-2.339,2.339a.837.837,0,0,1-.867.231.813.813,0,0,1-.594-.641.843.843,0,0,1,.261-.819l2.321-2.315a.771.771,0,0,1,.125-.095l-1.395-1.4a5.9,5.9,0,0,1-5.028.968,5.734,5.734,0,0,1-3.383-2.333,5.936,5.936,0,1,1,9.669.107l1.431,1.4c.071-.083.142-.184.231-.273.736-.736,1.466-1.472,2.208-2.2A.859.859,0,1,1,17.3,10.822l-1.6,1.6C15.4,12.7,15.114,12.965,14.817,13.279ZM11.683,7.124a4.244,4.244,0,1,0-4.256,4.232A4.244,4.244,0,0,0,11.683,7.124Z" transform="matrix(0.719, 0.695, -0.695, 0.719, 4348.543, 1882.728)" fill="none"/> |
| | | </g> |
| | | </svg> |
| | |
| | | line-height: 40px; |
| | | padding-right: 10px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .remind-container{ |
| | | margin-top: 13px; |
| | | margin-bottom: 20px; |
| | | display: flex; |
| | | .remind-date{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | width: 70px; |
| | | border-radius: 6px; |
| | | border-bottom: 1px solid #CCCCCC; |
| | | border-right: 1px solid #CCCCCC; |
| | | border-left: 1px solid #CCCCCC; |
| | | } |
| | | .remind-content-txt{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | border: 1px solid #CCCCCC; |
| | | flex:1; |
| | | border-radius: 5px; |
| | | padding: 10px; |
| | | } |
| | | .mb-3{ |
| | | margin-bottom: 3px; |
| | | } |
| | | .mt-2{ |
| | | margin-top:2px; |
| | | } |
| | | .date-year{ |
| | | color: #fff; |
| | | align-items: center; |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | .bgc-primary-red{ |
| | | background-color:$PRIMARY_RED; |
| | | width: 70.5px; |
| | | border-top-left-radius:6px; |
| | | border-top-right-radius:6px; |
| | | border: 1px solid #CCCCCC; |
| | | } |
| | | } |
| | |
| | | $SKY_BLUE: #009CBD; |
| | | $LIGHT_BLUE: #8DB9CA; |
| | | $DARK_BLUE: #1B365D; |
| | | $LIGHT_RED: #DA3738; |
| | | $BEIGE: #A89968; |
| | | $PRUDENTIAL_GREY: #68737A; |
| | | $LIGHT_GREY: #D0D0CE; |
| | |
| | | @extend .text--bold; |
| | | @extend .text--primary; |
| | | @extend .cursor--pointer; |
| | | @extend .text--underline; |
| | | } |
| | | |
| | | .pam-link-button--lg { |
| | |
| | | @extend .text--bold; |
| | | @extend .text--primary; |
| | | @extend .cursor--pointer; |
| | | @extend .text--underline; |
| | | } |
| | |
| | | <div> |
| | | <div class="interview__header"> |
| | | <div class="mdTxt">ç´è¨ªç´é</div> |
| | | <div class="pam-link-button--lg" |
| | | <div class="pam-link-button" |
| | | @click="addInterview">+æ°å¢</div> |
| | | </div> |
| | | <InterviewCard :interviewList="displayList.slice(0, 3)"></InterviewCard> |
| | | |
| | | <section class="text--right mt-30" v-if="interviewList.length > 3"> |
| | | <div class="pam-link-button--lg" @click="readMoreBtn">å±éçæ´å¤</div> |
| | | <section class="text--right mt-30 interview-check-more" v-if="interviewList.length > 3"> |
| | | <div class="pam-link-button" @click="readMoreBtn"> |
| | | å±éçæ´å¤ |
| | | <i class="icon-expand"></i> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </template> |
| | |
| | | justify-content: space-between; |
| | | margin-bottom : 10px; |
| | | } |
| | | .interview-check-more{ |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | </style> |
| | |
| | | position : relative; |
| | | .circle { |
| | | background-color: white; |
| | | border : 1px solid #707070; |
| | | border : 1px solid #CCCCCC; |
| | | border-radius : 50%; |
| | | height : 8px; |
| | | margin : 0; |
| | | width : 8px; |
| | | z-index : 5; |
| | | &.activate { |
| | | background-color: $PRIMARY_BLACK; |
| | | background-color: $BEIGE; |
| | | } |
| | | } |
| | | .line { |
| | |
| | | <div class="dateTime"> |
| | | <UiDatePicker |
| | | @changeDate="changeDateTime($event, 'date')" |
| | | :isPastDateDisabled="isPastDateDisabled" |
| | | :defaultValue="defaultValue" |
| | | ></UiDatePicker> |
| | | <UiTimePicker |
| | | @changeTime="changeDateTime($event, 'time')" |
| | | :defaultValue="defaultValue" |
| | | :isPastDateDisabled="isPastDateDisabled" |
| | | :changeDate="changeDate" |
| | | ></UiTimePicker> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | import { Component, Emit, Prop, Vue } from "nuxt-property-decorator"; |
| | | import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator"; |
| | | |
| | | @Component |
| | | export default class DateTimePicker extends Vue { |
| | | changeDate!: Date; |
| | | changeDate: Date | string = ''; |
| | | changeTime!: string; |
| | | |
| | | @Prop() |
| | | defaultValue!: string; |
| | | |
| | | @Prop() |
| | | isPastDateDisabled!: boolean; |
| | | |
| | | @Emit('changeDateTime') |
| | | changeDateTime(event, type) { |
| | |
| | | this.changeTime = event; |
| | | } |
| | | if (this.changeDate && this.changeTime) { |
| | | const timeArray = this.changeTime.split(':'); |
| | | const interViewTime = this.changeDate.setHours(+timeArray[0], +timeArray[1]); |
| | | const hour = this.changeTime.split(':')[0]; |
| | | const minute = this.changeTime.split(':')[1]; |
| | | const interViewTime = new Date(this.changeDate).setHours(+hour, +minute); |
| | | return new Date(interViewTime); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | } |
| | | </script> |
| | | |
| | |
| | | <span>{{interviewRecord.lastModifiedDate | formatDate}} æ´æ°</span> |
| | | </div> |
| | | <el-row class="mdTxt mb-10"> |
| | | <el-col :xs="16" :sm="20" class="required">ç´è¨ªæé</el-col> |
| | | <el-col :xs="16" :sm="20"> |
| | | <span :class="{'required': !interviewId || isEdit}">ç´è¨ªæé</span> |
| | | </el-col> |
| | | <el-col :xs="8" :sm="4" class="text--right" v-if="interviewId"> |
| | | <span |
| | | v-if="!isEdit" |
| | | class="mr-10 text--primary text--underline cursor--pointer" |
| | | class="mr-10 text--primary cursor--pointer" |
| | | @click="showCancelPopUp = true" |
| | | >åªé¤</span> |
| | | ><i class="icon-delet"></i></span> |
| | | <span |
| | | v-if="!isEdit" |
| | | class="text--primary text--underline cursor--pointer" |
| | | class="text--primary cursor--pointer" |
| | | @click="isEdit = !isEdit" |
| | | >編輯</span> |
| | | ><i class="icon-edit"></i></span> |
| | | </el-col> |
| | | </el-row> |
| | | <template v-if="!interviewId || isEdit"> |
| | |
| | | ></DateTimePicker> |
| | | </template> |
| | | <template v-else> |
| | | <div class="mdTxt lighter mt-20"> |
| | | <div class="fs-20 mt-20"> |
| | | {{formatInterviewDate}} |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="mdTxt mb-10 mt-30">ç´è¨ªç´é</div> |
| | | <div class="mdTxt mb-10 mt-30" :class="{'required': !interviewId || isEdit}">ç´è¨ªç´é</div> |
| | | <template v-if="!interviewId || isEdit"> |
| | | <el-input |
| | | type="textarea" |
| | |
| | | </el-input> |
| | | </template> |
| | | <template v-else> |
| | | <div class="mdTxt lighter mt-20"> |
| | | <div class="fs-20 mt-20"> |
| | | {{content}} |
| | | </div> |
| | | </template> |
| | |
| | | <InterviewMsg |
| | | :isVisible.sync="showInterviewMsgPopup" |
| | | :client="appointmentDetail" |
| | | :defaultValue="interviewTime" |
| | | @closeDialog="closePopup" |
| | | ></InterviewMsg> |
| | | </div> |
| | |
| | | } |
| | | } |
| | | .required { |
| | | position: relative; |
| | | transform: translateX(10px); |
| | | position: relative; |
| | | |
| | | &::before { |
| | | content: '*'; |
| | | font-size: 15px; |
| | | font-weight: bold; |
| | | position: absolute; |
| | | color: #FF0000; |
| | | transform: translateX(-10px); |
| | | z-index: 5; |
| | | &::before { |
| | | content: '*'; |
| | | position: absolute; |
| | | color: #FF0000; |
| | | transform: translate(-12px, 0); |
| | | z-index: 5; |
| | | } |
| | | } |
| | | } |
| | | |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <template v-if="!interviewList.length"> |
| | | <div class="record-card record-card--empty"> |
| | | <template v-if="!interviewList.length"> |
| | | <span class="record-card record-card--empty" style="display:flex"> |
| | | ç¡ç´è¨ªç´é |
| | | </div> |
| | | </span> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <div class="interview--future"> |
| | | <div class="record-card mb-10" |
| | | <div class="record-card mb-10" |
| | | v-for="(item, index) in futureList" |
| | | :key="index + 'feature'" |
| | | @click="editInterview(item)" |
| | | > |
| | | <div class="record-card-date"> |
| | | <div> |
| | | <UiDateFormat |
| | | > |
| | | <div class="remind-container"> |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2"> |
| | | <UiDateFormat |
| | | class="date bold" |
| | | :date="item.interviewDate" |
| | | onlyShowSection="DAY" /> |
| | | </div> |
| | | <div> |
| | | <UiDateFormat |
| | | class="time mt-5 line-space" |
| | | onlyShowSection="YEAR" /> |
| | | </div> |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <UiDateFormat |
| | | class="date bold" |
| | | :date="item.interviewDate" |
| | | onlyShowSection="DATE" /> |
| | | </div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div style="display:flex"> |
| | | <UiDateFormat |
| | | class="date bold " |
| | | :date="item.interviewDate" |
| | | onlyShowSection="TIME" /> |
| | | </div> |
| | | </div> |
| | | <div class="record-card-content"> |
| | | <span>{{item.content}}</span> |
| | | </div> |
| | | </div> |
| | | <div class="interview-card-content">{{item.content}}</div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | :key="index + 'past'" |
| | | @click="editInterview(item)" |
| | | > |
| | | <div class="record-card-date"> |
| | | <div> |
| | | <UiDateFormat |
| | | class="date bold" |
| | | |
| | | <div class="remind-container"> |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2"> |
| | | <UiDateFormat |
| | | class="bold date" |
| | | :date="item.interviewDate" |
| | | onlyShowSection="DAY" /> |
| | | onlyShowSection="YEAR" /> |
| | | </div> |
| | | <div> |
| | | <UiDateFormat |
| | | class="time mt-5 line-space" |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <UiDateFormat |
| | | class="mt-5 line-space time" |
| | | :date="item.interviewDate" |
| | | onlyShowSection="DATE" /> |
| | | </div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div style="display:flex"> |
| | | <UiDateFormat |
| | | class="mt-5 line-space mb-3 time" |
| | | :date="item.interviewDate" |
| | | onlyShowSection="TIME" /> |
| | | </div> |
| | | </div> |
| | | <div class="interview-card-content">{{item.content}}</div> |
| | | </div> |
| | | <div class="record-card-content"> |
| | | <span>{{item.content}}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </template> |
| | |
| | | } |
| | | } |
| | | .interview--past { |
| | | margin-top: 10px; |
| | | border-top: 1px solid #CCCCCC; |
| | | padding-top: 17px; |
| | | margin-top: 17px; |
| | | } |
| | | .record-card { |
| | | height: 62px; |
| | | border: 1px solid #707070; |
| | | border-radius: 5px; |
| | | display: flex; |
| | | border-bottom: 1px solid #000; |
| | | height: 64px; |
| | | margin-bottom: 20px; |
| | | .record-card-date{ |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | margin-right: 10px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .record-card-content{ |
| | | height: 42px; |
| | | margin-top: 10px; |
| | | margin-right: 10px; |
| | | line-height: 1.2; |
| | | |
| | | } |
| | | &.record-card--empty { |
| | | align-items : center; |
| | |
| | | justify-content : center; |
| | | } |
| | | } |
| | | .interview-card-content{ |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | display: -webkit-box; |
| | | -webkit-box-orient: vertical; |
| | | word-break: break-all; |
| | | word-wrap: break-word; |
| | | -webkit-line-clamp: 2; |
| | | } |
| | | .line-space{ |
| | | letter-spacing: 1px; |
| | | } |
| | |
| | | </el-input> |
| | | |
| | | <div v-if="client.phone"> |
| | | <div class="mdTxt mt-30 mb-10">é è¨ç´è¨ªææ®µ</div> |
| | | <div class="mdTxt mt-30 mb-10 required">é è¨ç´è¨ªææ®µ</div> |
| | | <DateTimePicker |
| | | @changeDateTime="interviewTime = $event" |
| | | :isPastDateDisabled="true" |
| | | :defaultValue="defaultValue" |
| | | ></DateTimePicker> |
| | | </div> |
| | | |
| | |
| | | </el-dialog> |
| | | |
| | | <PopUpFrame |
| | | :isOpen.sync="isShowSuccessAlert"> |
| | | :isOpen.sync="isShowSuccessAlert" |
| | | @closePopUp="closeAllDialog"> |
| | | <div class="text--middle invite-review"> |
| | | <div class="mb-30 mt-10">å·²ç¼éç´è¨ªéç¥</div> |
| | | <div class="text--primary text--middle cursor--pointer text--underline" @click="closeAllDialog " :size="'250px'">æç¥éäº</div> |
| | |
| | | </div> |
| | | </template> |
| | | <script lang="ts"> |
| | | import { Vue, Component, Prop, PropSync, Emit, Action, namespace } from 'nuxt-property-decorator'; |
| | | import { Vue, Component, Prop, PropSync, Emit, namespace } from 'nuxt-property-decorator'; |
| | | |
| | | import appointmentService from '~/shared/services/appointment.service'; |
| | | import { Appointment, ToInformAppointment } from '~/shared/models/appointment.model'; |
| | |
| | | @Component |
| | | export default class InterviewMsg extends Vue { |
| | | |
| | | @Action |
| | | storeMyAppointmentList!: () => Promise<number>; |
| | | |
| | | @appointmentStore.Action |
| | | updateAppointmentDetail!: (id: number) => Appointment; |
| | | |
| | | @appointmentStore.Action |
| | | getMyAppointmentList!: () => Promise<Appointment[]>; |
| | | |
| | | @PropSync('isVisible') |
| | | dialogVisible!: boolean; |
| | |
| | | |
| | | @Prop() |
| | | client!: Appointment; |
| | | |
| | | @Prop() |
| | | defaultValue!: string; |
| | | |
| | | @Emit('closeDialog') |
| | | closeDialog() { |
| | |
| | | }; |
| | | appointmentService.informAppointment(appointmentInformation).then((_) => { |
| | | this.isShowSuccessAlert = true ; |
| | | this.updateAppointmentDetail(this.client.id); |
| | | }); |
| | | } |
| | | |
| | | closeAllDialog() { |
| | | this.isShowSuccessAlert = false ; |
| | | this.dialogVisible = false; |
| | | this.storeMyAppointmentList(); |
| | | this.updateAppointmentDetail(this.client.id); |
| | | this.getMyAppointmentList(); |
| | | } |
| | | |
| | | get isBtnDisabled() :Boolean { |
| | |
| | | <style lang="scss" > |
| | | .interview-msg-component{ |
| | | |
| | | .required { |
| | | position: relative; |
| | | &::before { |
| | | content: '*'; |
| | | position: absolute; |
| | | color: #FF0000; |
| | | transform: translate(-12px, 0); |
| | | } |
| | | } |
| | | .msg-dialog-title{ |
| | | display: flex; |
| | | justify-content: center; |
| | |
| | | <span v-else-if="item.email">(Email)</span> |
| | | <span v-else>(ææ©ç°¡è¨)</span> |
| | | </div> |
| | | <div class="mt-10">é ç´{{item.interviewDate | formatDate}}</div> |
| | | <div v-if="item.phone" class="mt-10">é ç´{{item.interviewDate | formatDate}}</div> |
| | | </div> |
| | | </section> |
| | | <div class="time-line"></div> |
| | |
| | | justify-content: center; |
| | | align-items: center; |
| | | align-content: center; |
| | | background-color:#1B365D; |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | |
| | | margin-left: 13px; |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <div class="mdTxt text--black">å³å°ç´è¨ªæç¨</div> |
| | | <div class="smTxt_bold text--primary sub-title cursor--pointer" @click="toAgendaPage">æ¥çå
¨é¨</div> |
| | | </div> |
| | | |
| | | <div class="remind-container"> |
| | | <div class="remind-content"> |
| | | <div class="remind-first-line mr-10"> |
| | | <div class="txt-margin">2021</div> |
| | | <div>12/25</div> |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2">2021</div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div class="txt-margin">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <div>12/25</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="remind-content-txt"> |
| | | <div class="mb-3">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | border-bottom: 1px solid $PRIMARY_RED; |
| | | } |
| | | } |
| | | .remind-container{ |
| | | margin-top: 13px; |
| | | border: 1px solid #CCCCCC; |
| | | height: 61px; |
| | | border-radius: 5px; |
| | | margin-bottom: 20px; |
| | | .remind-content{ |
| | | display: flex; |
| | | padding: 13px 10px; |
| | | .remind-first-line{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | .remind-content-txt{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .txt-margin{ |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | </style> |
| | |
| | | </div> |
| | | <div class="pam-header__action-bar"> |
| | | <i |
| | | v-if="currentRole" |
| | | v-if="isShowNotification" |
| | | class="icon-bell text--dark-blue cursor--pointer fix-chrome-click--issue" |
| | | @click="$router.push('/notification')" |
| | | ></i> |
| | |
| | | |
| | | <script lang="ts"> |
| | | import { Vue, Component } from 'vue-property-decorator'; |
| | | import { namespace } from 'nuxt-property-decorator'; |
| | | import { Action, namespace, State, Watch } from 'nuxt-property-decorator'; |
| | | import { Role } from '~/shared/models/enum/Role'; |
| | | import * as _ from 'lodash'; |
| | | import { NotificationList } from '~/shared/models/reviews.model'; |
| | | import { AppointmentLog } from '~/shared/models/appointment.model'; |
| | | |
| | | const roleStorage = namespace('localStorage'); |
| | | @Component |
| | |
| | | |
| | | @roleStorage.Getter |
| | | isAdminLogin!: boolean; |
| | | |
| | | @roleStorage.Getter |
| | | isUserLogin!: boolean; |
| | | |
| | | @Action |
| | | storeMyPersonalNotification!: () => void; |
| | | |
| | | @State |
| | | notificationList!: NotificationList[]; |
| | | |
| | | @Action |
| | | storeMyAppointmentReviewLog!: () => void; |
| | | |
| | | @State |
| | | unReviewLogList!: AppointmentLog[]; |
| | | |
| | | isOpenDropdown = false; |
| | | |
| | |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | @Watch('$route', {immediate: true}) |
| | | onRouterChange() { |
| | | this.getNotificationAndReviewLog(); |
| | | } |
| | | |
| | | private getNotificationAndReviewLog() { |
| | | if (this.isUserLogin) { |
| | | this.storeMyPersonalNotification(); |
| | | this.storeMyAppointmentReviewLog(); |
| | | } |
| | | |
| | | if (this.isAdminLogin) { |
| | | this.storeMyPersonalNotification(); |
| | | } |
| | | } |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | routerNavigateTo(url: string): void { |
| | | (this.$refs.dropdown as any).hide(); |
| | | _.isEqual(url,'') |
| | |
| | | return this.idToken && this.currentRole ? (this.currentRole as Role): Role.NOT_LOGIN; |
| | | } |
| | | |
| | | get isShowNotification() { |
| | | if (this.isUserLogin) { |
| | | return this.notificationList.length || this.unReviewLogList.length; |
| | | } |
| | | if (this.isAdminLogin) { |
| | | return this.notificationList.length |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | </script> |
| | |
| | | </section> |
| | | <div class="user-reviews-page"> |
| | | |
| | | <template v-if="myAppointmentReviewLogList.length"> |
| | | <template v-if="reviewLogList.length"> |
| | | <section class="user-reviews-content"> |
| | | <div |
| | | class="user-reviews-card" |
| | | v-for="(appointmentLog, index) in myAppointmentReviewLogList" |
| | | v-for="(appointmentLog, index) in reviewLogList" |
| | | :key="index"> |
| | | <div class="user-reviews-card-content" v-if="isUserLogin"> |
| | | æ¨å°<span class="mdTxt">{{ ` ${appointmentLog.agentName} ` }}</span>åäº <UiReviewScore :score="appointmentLog.score" /> è©å¹ï¼ |
| | |
| | | </template> |
| | | <script lang="ts"> |
| | | import { AppointmentLog } from '~/shared/models/appointment.model'; |
| | | import { Vue, Component, Prop } from 'nuxt-property-decorator'; |
| | | import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator'; |
| | | import authService from '~/shared/services/auth.service'; |
| | | |
| | | @Component |
| | | export default class ReviewRecords extends Vue{ |
| | | |
| | | @Prop() |
| | | myAppointmentReviewLogList!: AppointmentLog[]; |
| | | reviewLogList!: AppointmentLog[]; |
| | | |
| | | isUserLogin = false; |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | mounted() { |
| | | this.isUserLogin = authService.isUserLogin(); |
| | | } |
| | |
| | | format="yyyy/MM/dd" |
| | | placeholder="é¸ææ¥æ" |
| | | prefix-icon="icon-down down-icon" |
| | | :picker-options="pickerOptions" |
| | | @change="changeDate" |
| | | > |
| | | </el-date-picker> |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator"; |
| | | import { Component, Emit, Prop, PropSync, Vue, Watch } from "nuxt-property-decorator"; |
| | | |
| | | @Component |
| | | export default class UiDatePicker extends Vue { |
| | |
| | | |
| | | @Prop() |
| | | defaultValue!: string; |
| | | |
| | | @Prop({default: false}) |
| | | isPastDateDisabled!: boolean; |
| | | |
| | | @Emit('changeDate') |
| | | changeDate() { |
| | |
| | | this.changeDate(); |
| | | } |
| | | } |
| | | |
| | | get pickerOptions() { |
| | | if (this.isPastDateDisabled) { |
| | | return { |
| | | disabledDate(time: Date) { |
| | | const date = new Date(); |
| | | const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; |
| | | const pickedDate = `${time.getFullYear()}/${time.getMonth() + 1}/${time.getDate()}` |
| | | return new Date(pickedDate).getTime() < new Date(currentDate).getTime(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | </script> |
| | |
| | | @Component |
| | | export default class UiTimePicker extends Vue { |
| | | timeValue = ''; |
| | | pickerOptions = { |
| | | start: '09:00', |
| | | step: '00:15', |
| | | end: '21:00' |
| | | } |
| | | |
| | | @Prop() |
| | | defaultValue!: string; |
| | | |
| | | @Prop({default: ''}) |
| | | changeDate!: Date | string; |
| | | |
| | | @Prop() |
| | | isPastDateDisabled!: boolean; |
| | | |
| | | /////////////////////////////////////////////////////////////////////// |
| | | |
| | | @Emit('changeTime') |
| | | changeTime() { |
| | |
| | | @Watch('defaultValue', {immediate: true}) |
| | | updateDefault() { |
| | | if (this.defaultValue) { |
| | | const hours = new Date(this.defaultValue).getHours(); |
| | | const minutes = new Date(this.defaultValue).getMinutes(); |
| | | this.timeValue = `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`; |
| | | const defaultDate = new Date(this.defaultValue); |
| | | this.timeValue = this.formatTimeString(defaultDate); |
| | | this.changeTime(); |
| | | } |
| | | } |
| | | |
| | | /////////////////////////////////////////////////////////////////////// |
| | | |
| | | get pickerOptions() { |
| | | let minTime = ''; |
| | | const currentDate = new Date(); |
| | | |
| | | if (this.isPastDateDisabled && this.changeDate && this.isPickedToday(currentDate)) { |
| | | minTime = this.formatTimeString(currentDate); |
| | | this.isPickedDisableTime(currentDate, minTime); |
| | | } |
| | | |
| | | return { |
| | | start: '09:00', |
| | | step: '00:15', |
| | | end: '21:00', |
| | | minTime: minTime |
| | | } |
| | | } |
| | | |
| | | private isPickedDisableTime(currentDate: Date, minTime: string) { |
| | | const currentTime = this.getTimeValue(currentDate, minTime); |
| | | const pickedTime = this.getTimeValue(currentDate, this.timeValue); |
| | | if (pickedTime < currentTime) { |
| | | this.timeValue = ''; |
| | | this.changeTime(); |
| | | } |
| | | } |
| | | |
| | | private isPickedToday(currentDate: Date) { |
| | | const pickedDate = new Date(this.changeDate); |
| | | const today = this.formatDateString(currentDate); |
| | | const picked = this.formatDateString(pickedDate); |
| | | return today === picked; |
| | | } |
| | | |
| | | private getTimeValue(date: Date, time: string) { |
| | | const hour = time.split(':')[0]; |
| | | const minute = time.split(':')[1]; |
| | | return date.setHours(+hour, +minute); |
| | | } |
| | | |
| | | private formatDateString(date: Date) { |
| | | return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; |
| | | } |
| | | |
| | | private formatTimeString(date: Date) { |
| | | const hours = date.getHours(); |
| | | const minutes = date.getMinutes(); |
| | | return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`; |
| | | } |
| | | } |
| | | </script> |
| | |
| | | <el-row |
| | | type="flex" |
| | | class="pam-paragraph"> |
| | | <UiField icon="comment" label="å人ç念"> |
| | | <UiField icon="comment" label="å人ç念" class="agent-info-textarea"> |
| | | {{ agentInfo.concept }} |
| | | </UiField> |
| | | </el-row> |
| | |
| | | <el-row |
| | | type="flex" |
| | | class="pam-paragraph"> |
| | | <UiField icon="school" label="åäººèæ¯"> |
| | | <UiField icon="school" label="åäººèæ¯" class="agent-info-textarea"> |
| | | <span> |
| | | {{ agentInfo.experiences }} |
| | | </span> |
| | |
| | | <el-row |
| | | type="flex" |
| | | class="pam-paragraph"> |
| | | <UiField icon="trophy" label="å¾çç¶æ·"> |
| | | <UiField icon="trophy" label="å¾çç¶æ·" class="agent-info-textarea"> |
| | | {{ agentInfo.awards }} |
| | | </UiField> |
| | | </el-row> |
| | |
| | | .pam-field{ |
| | | display: flex; |
| | | } |
| | | |
| | | .agent-info-textarea{ |
| | | word-break: break-all; |
| | | word-wrap: break-word; |
| | | } |
| | | </style> |
| | |
| | | contactStatus : this.contactStatus.CLOSE, |
| | | remark : this.appointmentCloseInfo.remark, |
| | | } |
| | | appointmentService.closeAppointment(toCloseAppointment).then((_) => this.updateAppointmentDetail(appointmentId)); |
| | | this.isShowSuccessAlert = true; |
| | | appointmentService.closeAppointment(toCloseAppointment).then((_) => { |
| | | this.updateAppointmentDetail(appointmentId); |
| | | this.isShowSuccessAlert = true; |
| | | }); |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | <section class="client-detail"> |
| | | |
| | | <div class="client-detail-info"> |
| | | <div class="client-detail-info mb-30"> |
| | | <div class="client-detail-info__avatar"> |
| | | <div class="circle"> |
| | | {{ appointmentDetail.name || 'NO NAME' }} |
| | | <div class="sm-circle"> |
| | | {{ appointmentDetail.gender === 'male' ? 'ç·' : '女'}} |
| | | <div class="sm-circle sm-circle-male" v-if="appointmentDetail.gender === 'male'"> |
| | | <i class="icon-sex-male sex-icon"></i> |
| | | </div> |
| | | <div class="sm-circle sm-circle-female" v-if="appointmentDetail.gender === 'female'"> |
| | | <i class="icon-sex-female sex-icon"></i> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | } |
| | | |
| | | getHopeContactTimeContent(hopeContactTimeString: string): string[] { |
| | | const result = hopeContactTimeString.replace("'", '').split('ã'); |
| | | const result = hopeContactTimeString.replaceAll("'", '').split('ã'); |
| | | return result; |
| | | } |
| | | |
| | | inviteReview(): void { |
| | | reviewsService.sendSatisfactionToClient(this.appointmentDetail.id).then(res => res); |
| | | this.isShowInviteReviewDialog = true ; |
| | | reviewsService.sendSatisfactionToClient(this.appointmentDetail.id).then(res => { |
| | | this.isShowInviteReviewDialog = true; |
| | | }); |
| | | } |
| | | } |
| | | </script> |
| | |
| | | height: 100px; |
| | | width: 100px; |
| | | border-radius: 50%; |
| | | background-color: #fff; |
| | | border: 1px solid $PRIMARY_BLACK; |
| | | background-image: url('~/assets/images/appointment/avatar_bg.svg'); |
| | | position: relative; |
| | | display: flex; |
| | | justify-content: center; |
| | |
| | | width: 30px; |
| | | border-radius: 50%; |
| | | background-color: #fff; |
| | | border: 1px solid $PRIMARY_BLACK; |
| | | bottom: 0; |
| | | right: 0; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | .sex-icon { |
| | | font-size: 20px; |
| | | &.icon-sex-male{ |
| | | color: $SKY_BLUE; |
| | | } |
| | | &.icon-sex-female{ |
| | | color: $CORAL; |
| | | } |
| | | } |
| | | &-male { |
| | | border: 1px solid $SKY_BLUE; |
| | | } |
| | | &-female { |
| | | border: 1px solid $LIGHT_RED; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | @extend .mb-10; |
| | | @extend .mdTxt; |
| | | @extend .mr-10; |
| | | line-height: 1.3; |
| | | color : $DARK_BLUE; |
| | | flex-basis: auto; |
| | | min-width : 40px; |
| | | } |
| | | .client-detail-demand__demand-list-content { |
| | | text-align: justify; |
| | | line-height: 1.3; |
| | | text-justify: auto; |
| | | word-break: break-all; |
| | | } |
| | |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | .required { |
| | | position: relative; |
| | | &::before { |
| | | content: '*'; |
| | | position: absolute; |
| | | color: #FF0000; |
| | | transform: translate(-12px, 0); |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <div class="mdTxt">å³å°ç´è¨ªæç¨(3)</div> |
| | | |
| | | <div class="remind-container"> |
| | | <div class="remind-content"> |
| | | <div class="remind-first-line mr-10"> |
| | | <div class="txt-margin">2021</div> |
| | | <div>12/25</div> |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2">2021</div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div class="txt-margin">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <div>12/25</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="remind-content-txt"> |
| | | <div class="mb-3">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | </div> |
| | | <div class="remind-container"> |
| | | <div class="remind-content"> |
| | | <div class="remind-first-line mr-10"> |
| | | <div class="txt-margin">2021</div> |
| | | <div>12/22</div> |
| | | |
| | | <div class="remind-container"> |
| | | |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2">2021</div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div class="txt-margin">13:00</div> |
| | | <div>ç´è¨ªçè°æ</div> |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <div>12/25</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="remind-content-txt"> |
| | | <div class="mb-3">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | |
| | | </div> |
| | | <div class="remind-container"> |
| | | <div class="remind-content"> |
| | | <div class="remind-first-line mr-10"> |
| | | <div class="txt-margin">2021</div> |
| | | <div>12/18</div> |
| | | <div class="remind-container"> |
| | | |
| | | <div class="remind-date mr-10"> |
| | | <div class="mb-3 smTxt bgc-primary-red date-year"> |
| | | <div class="mb-3 mt-2">2021</div> |
| | | </div> |
| | | <div class="remind-content-txt"> |
| | | <div class="txt-margin">09:00</div> |
| | | <div>ç´è¨ªæä¼¯ä¼¯</div> |
| | | </div> |
| | | <div class="p mt-5"> |
| | | <div>12/25</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="remind-content-txt"> |
| | | <div class="mb-3">09:00</div> |
| | | <div>ç´è¨ªæå¿é¶</div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | } |
| | | </script> |
| | | <style lang="scss" scoped> |
| | | .remind-container{ |
| | | margin-top: 13px; |
| | | border: 1px solid #CCCCCC; |
| | | height: 61px; |
| | | border-radius: 5px; |
| | | margin-bottom: 20px; |
| | | background-color: #fff; |
| | | .remind-content{ |
| | | display: flex; |
| | | padding: 13px 10px; |
| | | .remind-first-line{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | } |
| | | .remind-content-txt{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .txt-margin{ |
| | | margin-bottom: 3px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <div class="pam-myAppointment-banner"></div> |
| | | <InterviewNotification></InterviewNotification> |
| | | <div class="pam-container"> |
| | | <div class="pam-cus-tabs mb-30"> |
| | | <div class="pam-cus-tabs mb-10"> |
| | | <div |
| | | class="cus-tab-item" |
| | | :class="{'is-active': activeTabName === 'appointmentList'}" |
| | |
| | | ></i> |
| | | </el-input> |
| | | |
| | | <div class="closed-appointment__tag-filter"> |
| | | <div class="closed-appointment__tag-filter mb-10"> |
| | | <el-radio v-model="selectedClosedCategory" :label="'all'" border>å
¨é¨({{ itemSum }})</el-radio> |
| | | <el-radio v-model="selectedClosedCategory" :label="'done'" border>æäº¤({{ doneItemSum }})</el-radio> |
| | | <el-radio v-model="selectedClosedCategory" :label="'closed'" border>æªæäº¤({{ closedItemSum }})</el-radio> |
| | |
| | | .closed-appointment__tag-filter { |
| | | display: flex; |
| | | .el-radio { |
| | | border-color: $PRIMARY_BLACK; |
| | | border-width: 2px; |
| | | border-color: $LIGHT_GREY; |
| | | border-radius: 30px; |
| | | border-width: 1px; |
| | | font-size : 16px; |
| | | margin-left : 0 !important; |
| | | margin-right: 10px; |
| | | padding : 10px; |
| | | @extend .fix-chrome-click--issue; |
| | | &.is-checked { |
| | | background-color: #D0D0CE; |
| | | background-color: $CORAL; |
| | | .el-radio__label { |
| | | color : $PRIMARY_WHITE !important; |
| | | } |
| | | } |
| | | .el-radio__input { |
| | | display: none; |
| | |
| | | ></i> |
| | | </el-input> |
| | | |
| | | <div class="mb-10"> |
| | | <a class="sort-indicator text--primary text--underline cursor--pointer" @click="changeSortType"> |
| | | <div class="mb-10 sort-indicator-container"> |
| | | <a class="sort-indicator cursor--pointer" @click="changeSortType"> |
| | | {{ this.sortType === 'DESC' ? 'æ°->è' : 'è->æ°' }} |
| | | <i v-if="isSortType" class="icon-sort-add"></i> |
| | | <i v-else class="icon-sort-decrease"></i> |
| | | </a> |
| | | </div> |
| | | |
| | | <ClientList |
| | | :clients="pageList" |
| | | :title="'contactedList'" |
| | | class="mt-10" |
| | | ></ClientList> |
| | | |
| | | <UiPagination |
| | |
| | | } |
| | | } |
| | | |
| | | get isSortType () :boolean { |
| | | return this.sortType === 'DESC'; |
| | | } |
| | | |
| | | } |
| | | </script> |
| | | <style lang="scss" scoped> |
| | | .sort-indicator-container{ |
| | | margin-bottom: 20px; |
| | | } |
| | | .sort-indicator{ |
| | | border-radius:30px; |
| | | border: 1px solid #D0D0CE; |
| | | background-color:#fff; |
| | | padding: 10px 20px; |
| | | color: #68737A; |
| | | } |
| | | </style> |
| | |
| | | <div class="text--right mb-10" @click="showNotificationHint = true"> |
| | | <i class="satisfaction-icon icon-edit"></i> |
| | | </div> |
| | | <div class="satisfaction-banner my-10 cursor--pointer" @click="$router.push('/satisfactionList')"> |
| | | <div |
| | | v-if="isUserLogin && unReviewLogList.length" |
| | | class="satisfaction-banner my-10 cursor--pointer" |
| | | @click="$router.push('/satisfactionList')" |
| | | > |
| | | <p class="satisfaction-text text--center">è«å¡«å¯«æ»¿æåº¦èª¿æ¥</p> |
| | | </div> |
| | | <el-row |
| | |
| | | align="middle" |
| | | class="notification-card" |
| | | > |
| | | <el-col class="unRead" :span="3"></el-col> |
| | | <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col> |
| | | <el-col :span="18"> |
| | | <p class="text">{{item.content}}</p> |
| | | </el-col> |
| | |
| | | <div> |
| | | <UiDateFormat |
| | | class="date" |
| | | :date="item.date" |
| | | :date="item.createdDate" |
| | | onlyShowSection="DAY" /> |
| | | </div> |
| | | <div> |
| | | <UiDateFormat |
| | | class="time" |
| | | :date="item.date" |
| | | :date="item.createdDate" |
| | | onlyShowSection="TIME" /> |
| | | </div> |
| | | |
| | |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | import { Component, Vue } from "nuxt-property-decorator"; |
| | | import { Component, State, Vue } from "nuxt-property-decorator"; |
| | | import { AppointmentLog } from "~/shared/models/appointment.model"; |
| | | import { NotificationList } from "~/shared/models/reviews.model"; |
| | | import authService from "~/shared/services/auth.service"; |
| | | |
| | | @Component |
| | | export default class Notification extends Vue { |
| | | showNotificationHint = false; |
| | | |
| | | notificationList = [ |
| | | { |
| | | content: 'ç³»çµ±åæ©å
¬åï¼10/19(äº)22:30è³10/21(æ¥)20:00é²è¡ç³»çµ±æ´æ°', |
| | | date: '2022-01-05T04:18:05.249Z' |
| | | }, |
| | | { |
| | | content: 'ç³»çµ±åæ©å
¬åï¼10/19(äº)22:30è³10/21(æ¥)20:00é²è¡ç³»çµ±æ´æ°', |
| | | date: '2022-01-05T04:18:05.249Z' |
| | | } |
| | | ] |
| | | @State |
| | | unReviewLogList!: AppointmentLog[]; |
| | | |
| | | @State |
| | | notificationList!: NotificationList[]; |
| | | |
| | | showNotificationHint = false; |
| | | isUserLogin = false; |
| | | |
| | | //////////////////////////////////////////////////////////// |
| | | |
| | | mounted() { |
| | | this.isUserLogin = authService.isUserLogin(); |
| | | } |
| | | |
| | | } |
| | | </script> |
| | | |
| | |
| | | </PopUpFrame> |
| | | |
| | | <PopUpFrame :isOpen.sync="sendReserve" @update:isOpen="closeReservePopUp"> |
| | | <div class="text--middle mt-30 sendReserve-txt">é ç´æåï¼</div> |
| | | <div class="text--middle sendReserve-txt">æ¨é ç´çé¡§åæåéèæ¨è¯çµ¡ï¼</div> |
| | | <div class="mdTxt mt-30 sendReserve-txt">é ç´æåï¼</div> |
| | | <div class="mdTxt sendReserve-txt mb-30">æ¨é ç´çé¡§åæåéèæ¨è¯çµ¡ï¼</div> |
| | | <div class="pam-app-review mb-10"> |
| | | <div class="mdTxt mb-10">å°æ¼ |
| | | <span class="mdTxt text--primary text--bold ">æååªå</span> |
| | | å¹³å°çæ´é«æåï¼ |
| | | </div> |
| | | <div class="mdTxt">æ¨çµ¦äºå¹¾é¡æè©å¹ï¼</div> |
| | | </div> |
| | | <el-rate v-model="score" class="pam-satisfaction-rate fix-chrome-click--issue"></el-rate> |
| | | <div class="text--center mdTxt"> |
| | | <el-button @click="closeReservePopUp">ç¥é</el-button> |
| | | <el-button type="primary" |
| | | @click="closeReservePopUp"> |
| | | æç¥éäº |
| | | éåº |
| | | </el-button> |
| | | </div> |
| | | </PopUpFrame> |
| | |
| | | |
| | | @roleStorage.State |
| | | recommendConsultantItem!:string; |
| | | |
| | | score ="" ; |
| | | |
| | | genderOptions=[ |
| | | { |
| | |
| | | display: flex; |
| | | justify-content: center; |
| | | margin-top: 10px; |
| | | margin-bottom: 26px; |
| | | } |
| | | |
| | | //draweræåºä¸æåæ¨£å¼ |
| | |
| | | margin: 0px 20px; |
| | | } |
| | | |
| | | .pam-app-review{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .pam-satisfaction-rate{ |
| | | margin-bottom: 45px; |
| | | } |
| | | |
| | | @include desktop{ |
| | | .ques-header{ |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | </style> |
| | | |
| | |
| | | <template> |
| | | <div> |
| | | <ReviewRecords |
| | | :myAppointmentReviewLogList="myAppointmentReviewLogList" |
| | | :reviewLogList="reviewLogList" |
| | | ></ReviewRecords> |
| | | </div> |
| | | </template> |
| | |
| | | @Component |
| | | export default class Reviews extends Vue{ |
| | | |
| | | @State('myAppointmentReviewLogList') |
| | | myAppointmentReviewLogList!: AppointmentLog[]; |
| | | |
| | | @Action |
| | | storeMyAppointmentReviewLog!: any; |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | |
| | | mounted() { |
| | | this.storeMyAppointmentReviewLog(); |
| | | } |
| | | @State('reviewLogList') |
| | | reviewLogList!: AppointmentLog[]; |
| | | |
| | | } |
| | | </script> |
| | |
| | | <div class="pam-container"> |
| | | <div class="satisfaction-title"> |
| | | <span class="mdTxt">滿æåº¦èª¿æ¥</span> |
| | | <span class="ml-10 text--prudential_grey smTxt_bold">å
± {{satisfactionList.length}} ç</span> |
| | | <span class="ml-10 text--prudential_grey smTxt_bold">å
± {{mapUnReviewLogList.length}} ç</span> |
| | | </div> |
| | | <div class="satisfaction-card" v-for="(item, index) in satisfactionList" :key="index"> |
| | | <div class="satisfaction-card" v-for="(item, index) in mapUnReviewLogList" :key="index"> |
| | | <div class="satisfaction-card-content"> |
| | | <UiAvatar :size="80" :agentNo="''"></UiAvatar> |
| | | <UiAvatar :size="80" :agentNo="item.agentNo"></UiAvatar> |
| | | <div class="satisfaction-card-text">å°æ¼é¡§å |
| | | <span class="text--primary text--bold">{{item.agentName}}</span> |
| | | çæ´é«æåï¼æ¨çµ¦äºå¹¾é¡æè©å¹ï¼ |
| | | </div> |
| | | </div> |
| | | <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10"></el-rate> |
| | | <el-rate v-model="item.score" class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"></el-rate> |
| | | </div> |
| | | <div class="text--center mt-30"> |
| | | <el-button type="primary" :disabled="isBtnDisabled">éåº</el-button> |
| | |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { Vue, Component } from 'nuxt-property-decorator'; |
| | | <script lang="ts"> |
| | | import { Vue, Component, Action, State, Watch } from 'nuxt-property-decorator'; |
| | | import { AppointmentLog } from '~/shared/models/appointment.model'; |
| | | |
| | | @Component({ |
| | | layout: 'home' |
| | | }) |
| | | export default class MySatisfactionList extends Vue { |
| | | satisfactionList = [ |
| | | { |
| | | agentName: 'è¡ç¾ç', |
| | | score: 0 |
| | | }, |
| | | { |
| | | agentName: 'è¡ç¾ç', |
| | | score: 0 |
| | | |
| | | @State |
| | | unReviewLogList!: AppointmentLog[]; |
| | | |
| | | mapUnReviewLogList: AppointmentReviewLog[] = []; |
| | | |
| | | /////////////////////////////////////////////////////// |
| | | |
| | | @Watch('unReviewLogList') |
| | | onUnReviewLogListChange() { |
| | | if (this.unReviewLogList.length) { |
| | | this.mapUnReviewLogList = this.unReviewLogList.map(item => { |
| | | return { |
| | | ...item, |
| | | satisfaction: 0 |
| | | } |
| | | }) |
| | | } |
| | | ] |
| | | } |
| | | |
| | | /////////////////////////////////////////////////////// |
| | | |
| | | get isBtnDisabled() { |
| | | return this.satisfactionList.findIndex(item => item.score > 0) === -1; |
| | | if (this.mapUnReviewLogList.length) { |
| | | return this.mapUnReviewLogList.findIndex(item => item.satisfaction > 0) === -1; |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | interface AppointmentReviewLog extends AppointmentLog { |
| | | satisfaction: number; |
| | | } |
| | | </script> |
| | | |
| | |
| | | } |
| | | |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <template> |
| | | <div> |
| | | <ReviewRecords |
| | | :myAppointmentReviewLogList="myAppointmentReviewLogList" |
| | | :reviewLogList="reviewLogList" |
| | | ></ReviewRecords> |
| | | </div> |
| | | </template> |
| | |
| | | |
| | | @Component |
| | | export default class UserReviewsRecord extends Vue{ |
| | | @State('myAppointmentReviewLogList') |
| | | myAppointmentReviewLogList!: AppointmentLog[]; |
| | | |
| | | @Action |
| | | storeMyAppointmentReviewLog!: any; |
| | | |
| | | ////////////////////////////////////////////////////////////////////// |
| | | mounted() { |
| | | this.storeMyAppointmentReviewLog(); |
| | | } |
| | | @State('reviewLogList') |
| | | reviewLogList!: AppointmentLog[]; |
| | | |
| | | } |
| | | </script> |
| | |
| | | appointmentId: number, |
| | | score : number, |
| | | } |
| | | |
| | | export interface NotificationList { |
| | | id: number; |
| | | /** æ¨é¡ */ |
| | | title: string; |
| | | /** éç¥å
§å®¹ */ |
| | | content: string; |
| | | /** ACTIVITY(å人活åéç¥)ãSYSTEM(系統éç¥) */ |
| | | notificationType: string; |
| | | /** CUSTOMER(客æ¶)ãCONSULTANT(é¡§å) */ |
| | | ownerRole: string; |
| | | /** ç»å
¥è
id */ |
| | | ownerId: number |
| | | createdDate: string; |
| | | /** å·²è®æé */ |
| | | readDate: string; |
| | | } |
| | |
| | | import { http } from "./httpClient"; |
| | | |
| | | import { UserReviewsConsultantsParams } from "../models/reviews.model"; |
| | | import { NotificationList, UserReviewsConsultantsParams } from "../models/reviews.model"; |
| | | import { AppointmentLog } from "../models/appointment.model"; |
| | | |
| | | class ReviewsService { |
| | |
| | | sendSatisfactionToClient(appointmentId: number) { |
| | | return http.post(`/consultant/sendSatisfactionToClient/${appointmentId}`).then((res) => res); |
| | | } |
| | | |
| | | // éç¥å°é´éº |
| | | getMyPersonalNotification(): Promise<NotificationList[]> { |
| | | return http.get('/personal_notification/getMyPersonalNotification').then(res => res.data); |
| | | } |
| | | } |
| | | |
| | | export default new ReviewsService(); |
| | |
| | | import { getFavoriteFromStorage, setFavoriteToStorage } from '~/shared/storageConsultant'; |
| | | import { AppointmentLog } from '~/shared/models/appointment.model'; |
| | | import { AgentOfStrictQuery, StrictQueryParams } from '~/shared/models/strict-query.model'; |
| | | import { NotificationList } from '~/shared/models/reviews.model'; |
| | | |
| | | @Module |
| | | export default class Store extends VuexModule { |
| | |
| | | strictQueryList: AgentOfStrictQuery[] = []; |
| | | myConsultantList: Consultant[] = []; |
| | | |
| | | myAppointmentReviewLogList: AppointmentLog[] = []; |
| | | reviewLogList: AppointmentLog[] = []; |
| | | unReviewLogList: AppointmentLog[] = []; |
| | | notificationList: NotificationList[] = []; |
| | | |
| | | get isUserLogin() { |
| | | return this.context.getters['localStorage/isUserLogin']; |
| | |
| | | } |
| | | |
| | | @Mutation |
| | | updateMyAppointmentReviewLog(data: AppointmentLog[]) { |
| | | this.myAppointmentReviewLogList = data; |
| | | updateReviewLog(data: AppointmentLog[]) { |
| | | this.reviewLogList = data; |
| | | } |
| | | |
| | | @Mutation |
| | | updateUnReviewLog(data: AppointmentLog[]) { |
| | | this.unReviewLogList = data; |
| | | } |
| | | |
| | | @Mutation |
| | | updateNotification(data: NotificationList[]) { |
| | | this.notificationList = data; |
| | | } |
| | | |
| | | @Action |
| | |
| | | } |
| | | }); |
| | | const sortedData = dataWithLatestDate.sort((a, b) => +b.compareDate - +a.compareDate); |
| | | this.context.commit('updateMyAppointmentReviewLog', sortedData); |
| | | const reviewLog = sortedData.filter(item => item.score); |
| | | const unReviewLog = sortedData.filter(item => !item.score); |
| | | this.context.commit('updateReviewLog', reviewLog); |
| | | this.context.commit('updateUnReviewLog', unReviewLog); |
| | | }); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | @Action |
| | | storeMyPersonalNotification() { |
| | | reviewsService.getMyPersonalNotification().then(data => { |
| | | const sortData = data |
| | | .sort((preItem, nextItem) => +new Date(nextItem.createdDate) - +new Date(preItem.createdDate)) |
| | | this.context.commit('updateNotification', sortData); |
| | | }) |
| | | } |
| | | |
| | | } |
¤ñ¹ï·sÀÉ®× |
| | |
| | | CREATE TABLE public.appointment_expiring_notify_record ( |
| | | id bigserial NOT NULL, |
| | | appointment_id bigserial NOT NULL, |
| | | send_time timestamp NULL, |
| | | CONSTRAINT appointment_pending_notify_record_pk PRIMARY KEY (id) |
| | | ); |
¤ñ¹ï·sÀÉ®× |
| | |
| | | http get : |
| | | |
| | | http://localhost:8080/api/appointment/customer/expiring/newest |
| | | |
| | | ç°¡è¨åemailæä»¥è©²ç¶²åé²å
¥é¦é -> http://localhost:3000?notContactAppointmentId={ææ°ä¸çæªèçé ç´å®} |
| | | |
| | | response body: è¥ææå³200並給以ä¸è³æï¼è¥ç¡(æªæä»»ä½é¾ææªèçé ç´å®ï¼åæå404) |
| | | { |
| | | "id": 385, |
| | | "phone": "0911223344", |
| | | "email": "SDD", |
| | | "contactType": "phone", |
| | | "gender": "female", |
| | | "age": "21-30", |
| | | "job": "å
§å¤", |
| | | "requirement": "å¥åº·èä¿é", |
| | | "communicateStatus": "done", |
| | | "hopeContactTime": "'ææä¸,ææäº,ææä¸,ææå,ææäº,ææå
,æææ¥ã9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'", |
| | | "otherRequirement": null, |
| | | "appointmentDate": "2021-12-16T07:11:05.400Z", |
| | | "lastModifiedDate": "2022-01-19T10:57:51.380Z", |
| | | "agentNo": "A568420", |
| | | "customerId": 139, |
| | | "name": "Angula-test", |
| | | "consultantViewTime": "2021-12-27T02:02:18.711Z", |
| | | "consultantReadTime": "2021-12-28T07:16:01.295Z", |
| | | "contactTime": "2021-12-28T07:16:37.004Z", |
| | | "satisfactionScore": null, |
| | | "appointmentMemoList": [], |
| | | "interviewRecordDTOs": [], |
| | | "appointmentNoticeLogs": [ |
| | | { |
| | | "id": 4, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-11T09:33:57.754Z", |
| | | "interviewDate": null |
| | | }, |
| | | { |
| | | "id": 6, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-19T10:38:42.187Z", |
| | | "interviewDate": "2022-11-01T08:00:00.000+00:00" |
| | | } |
| | | ], |
| | | "appointmentClosedInfo": { |
| | | "id": 9, |
| | | "policyholderIdentityId": "A123456789", |
| | | "planCode": "ATMdd", |
| | | "policyEntryDate": "2022-01-12T00:00:00.000+00:00", |
| | | "remark": "test remark", |
| | | "closedReason": "other2", |
| | | "closedOtherReason": "å¿æ
ä¸å¥½ä¸æ³è²·2", |
| | | "appointmentId": 385 |
| | | } |
| | | } |
| | |
| | | |
| | | response body: |
| | | { |
| | | "id": 401, |
| | | "phone": "0912345678", |
| | | "email": "wayne@pollex.com.tw", |
| | | "contactType": "EMAIL", |
| | | "gender": "male", |
| | | "age": "under_20", |
| | | "job": "123", |
| | | "requirement": "å¥åº·èä¿é,ä¿å®å¥æª¢/è¦å", |
| | | "communicateStatus": "contacted", |
| | | "id": 385, |
| | | "phone": "0911223344", |
| | | "email": "SDD", |
| | | "contactType": "phone", |
| | | "gender": "female", |
| | | "age": "21-30", |
| | | "job": "å
§å¤", |
| | | "requirement": "å¥åº·èä¿é", |
| | | "communicateStatus": "done", |
| | | "hopeContactTime": "'ææä¸,ææäº,ææä¸,ææå,ææäº,ææå
,æææ¥ã9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'", |
| | | "otherRequirement": null, |
| | | "appointmentDate": "2021-12-21T08:13:50.154Z", |
| | | "lastModifiedDate": "2022-01-04T09:40:13.715Z", |
| | | "agentNo": "J149388015", |
| | | "customerId": 155, |
| | | "name": "123", |
| | | "consultantViewTime": "2021-12-24T07:27:48.681Z", |
| | | "consultantReadTime": null, |
| | | "contactTime": "2022-01-04T09:40:13.715Z", |
| | | "appointmentDate": "2021-12-16T07:11:05.400Z", |
| | | "lastModifiedDate": "2022-01-19T10:57:51.380Z", |
| | | "agentNo": "A568420", |
| | | "customerId": 139, |
| | | "name": "Angula-test", |
| | | "consultantViewTime": "2021-12-27T02:02:18.711Z", |
| | | "consultantReadTime": "2021-12-28T07:16:01.295Z", |
| | | "contactTime": "2021-12-28T07:16:37.004Z", |
| | | "satisfactionScore": null, |
| | | "appointmentMemoList": [] |
| | | "appointmentMemoList": [], |
| | | "interviewRecordDTOs": [], |
| | | "appointmentNoticeLogs": [ |
| | | { |
| | | "id": 4, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-11T09:33:57.754Z", |
| | | "interviewDate": null |
| | | }, |
| | | { |
| | | "id": 6, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-19T10:38:42.187Z", |
| | | "interviewDate": "2022-11-01T08:00:00.000+00:00" |
| | | } |
| | | ], |
| | | "appointmentClosedInfo": { |
| | | "id": 9, |
| | | "policyholderIdentityId": "A123456789", |
| | | "planCode": "ATMdd", |
| | | "policyEntryDate": "2022-01-12T00:00:00.000+00:00", |
| | | "remark": "test remark", |
| | | "closedReason": "other2", |
| | | "closedOtherReason": "å¿æ
ä¸å¥½ä¸æ³è²·2", |
| | | "appointmentId": 385 |
| | | } |
| | | } |
| | |
| | | appointmentMemoList : åè¨»è³æ |
| | | interviewRecordDTOs : ç´è¨ªç´é |
| | | |
| | | [ { |
| | | "id" : 385, |
| | | "phone" : "0911223344", |
| | | "email" : "SDD", |
| | | "contactType" : "phone", |
| | | "gender" : "female", |
| | | "age" : "21-30", |
| | | "job" : "å
§å¤", |
| | | "requirement" : "å¥åº·èä¿é", |
| | | "communicateStatus" : "contacted", |
| | | "hopeContactTime" : "'ææä¸,ææäº,ææä¸,ææå,ææäº,ææå
,æææ¥ã9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'", |
| | | "otherRequirement" : null, |
| | | "appointmentDate" : "2021-12-16T07:11:05.400Z", |
| | | "lastModifiedDate" : "2021-12-28T07:16:37.004Z", |
| | | "agentNo" : "A568420", |
| | | "customerId" : 139, |
| | | "name" : "Angula-test", |
| | | "consultantViewTime" : "2021-12-27T02:02:18.711Z", |
| | | "consultantReadTime" : "2021-12-28T07:16:01.295Z", |
| | | "contactTime" : "2021-12-28T07:16:37.004Z", |
| | | "satisfactionScore" : null, |
| | | "appointmentMemoList" : [ ], |
| | | "interviewRecordDTOs" : [ { |
| | | "id" : 6, |
| | | "content" : "test record content", |
| | | "createdDate" : "2022-01-07T07:38:05.976Z", |
| | | "lastModifiedDate" : "2022-01-07T07:38:05.976Z", |
| | | "createdBy" : "A568420", |
| | | "lastModifiedBy" : "A568420", |
| | | "interviewDate" : "2021-01-01T08:00:00.000+00:00", |
| | | "appointmentId" : 385 |
| | | } ] |
| | | } ] |
| | | [ |
| | | { |
| | | "id": 385, |
| | | "phone": "0911223344", |
| | | "email": "SDD", |
| | | "contactType": "phone", |
| | | "gender": "female", |
| | | "age": "21-30", |
| | | "job": "å
§å¤", |
| | | "requirement": "å¥åº·èä¿é", |
| | | "communicateStatus": "done", |
| | | "hopeContactTime": "'ææä¸,ææäº,ææä¸,ææå,ææäº,ææå
,æææ¥ã9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'", |
| | | "otherRequirement": null, |
| | | "appointmentDate": "2021-12-16T07:11:05.400Z", |
| | | "lastModifiedDate": "2022-01-19T10:57:51.380Z", |
| | | "agentNo": "A568420", |
| | | "customerId": 139, |
| | | "name": "Angula-test", |
| | | "consultantViewTime": "2021-12-27T02:02:18.711Z", |
| | | "consultantReadTime": "2021-12-28T07:16:01.295Z", |
| | | "contactTime": "2021-12-28T07:16:37.004Z", |
| | | "satisfactionScore": null, |
| | | "appointmentMemoList": [], |
| | | "interviewRecordDTOs": [], |
| | | "appointmentNoticeLogs": [ |
| | | { |
| | | "id": 4, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-11T09:33:57.754Z", |
| | | "interviewDate": null |
| | | }, |
| | | { |
| | | "id": 6, |
| | | "phone": "0912345678", |
| | | "email": "pollex@gmail.com", |
| | | "appointmentId": 385, |
| | | "content": "notice customer invterview time", |
| | | "createdDate": "2022-01-19T10:38:42.187Z", |
| | | "interviewDate": "2022-11-01T08:00:00.000+00:00" |
| | | } |
| | | ], |
| | | "appointmentClosedInfo": { |
| | | "id": 9, |
| | | "policyholderIdentityId": "A123456789", |
| | | "planCode": "ATMdd", |
| | | "policyEntryDate": "2022-01-12T00:00:00.000+00:00", |
| | | "remark": "test remark", |
| | | "closedReason": "other2", |
| | | "closedOtherReason": "å¿æ
ä¸å¥½ä¸æ³è²·2", |
| | | "appointmentId": 385 |
| | | } |
| | | } |
| | | ] |
¤ñ¹ï·sÀÉ®× |
| | |
| | | http get : |
| | | http://localhost:8080/api/appointment/consultant/pending/sum |
| | | |
| | | response body: |
| | | 2 |
| | |
| | | public static final String SYSTEM = "system"; |
| | | public static final String DEFAULT_LANGUAGE = "zh-tw"; |
| | | |
| | | /** |
| | | * é»è©±åemailå¨å»ºç«é ç´å®(T)å¾çN天è¦çºæªèçé ç´å® |
| | | * ç®åNçæ«å®çº2 |
| | | */ |
| | | public static final int APPOINTMENT_PENDING_PHONE_INTERVAL = 2; |
| | | public static final int APPOINTMENT_PENDING_EMAIL_INTERVAL = 2; |
| | | |
| | | /** |
| | | * é»è©±åemailå¨å»ºç«é ç´å®(T)å¾çN天æè¢«è¦çºæªèçé ç´å®ï¼ç¶å¤©æ¹æ¬¡æç¼éæé給顧å |
| | | * èå¨å¾ä¸å¤©(T+N+1)ï¼å°±æç¼éæ¹æ¬¡çµ¦å®¢æ¶åç¥ è©²é¡§åå¯è½å¿ç¢ç¡æ³èçï¼æ¯å¦éè¦åæ¶ |
| | | */ |
| | | public static final int APPOINTMENT_EXPIRING_PHONE_INTERVAL = APPOINTMENT_PENDING_PHONE_INTERVAL + 1; |
| | | public static final int APPOINTMENT_EXPIRING_EMAIL_INTERVAL = APPOINTMENT_PENDING_EMAIL_INTERVAL + 1; |
| | | |
| | | /** |
| | | * éç¥å®¢æ¶ç次æ¸éå¶ |
| | | */ |
| | | public static final int SEND_EXPIRING_NOTIFY_LIMIT = 1; |
| | | |
| | | private Constants() {} |
| | | } |
¤ñ¹ï·sÀÉ®× |
| | |
| | | package com.pollex.pam.domain; |
| | | |
| | | import javax.persistence.*; |
| | | import java.io.Serializable; |
| | | import java.time.Instant; |
| | | |
| | | @Entity |
| | | @Table(name = "appointment_expiring_notify_record") |
| | | public class AppointmentExpiringNotifyRecord implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @Id |
| | | @GeneratedValue(strategy = GenerationType.IDENTITY) |
| | | private Long id; |
| | | |
| | | @Column(name = "appointment_id") |
| | | private Long appointmentId; |
| | | |
| | | @Column(name = "send_time") |
| | | private Instant sendTime; |
| | | |
| | | public Long getId() { |
| | | return id; |
| | | } |
| | | |
| | | public void setId(Long id) { |
| | | this.id = id; |
| | | } |
| | | |
| | | public Long getAppointmentId() { |
| | | return appointmentId; |
| | | } |
| | | |
| | | public void setAppointmentId(Long appointmentId) { |
| | | this.appointmentId = appointmentId; |
| | | } |
| | | |
| | | public Instant getSendTime() { |
| | | return sendTime; |
| | | } |
| | | |
| | | public void setSendTime(Instant sendTime) { |
| | | this.sendTime = sendTime; |
| | | } |
| | | } |
| | |
| | | |
| | | import java.util.List; |
| | | |
| | | import com.pollex.pam.domain.Appointment; |
| | | import com.pollex.pam.enums.AppointmentStatusEnum; |
| | | import com.pollex.pam.enums.ContactStatusEnum; |
| | | import org.springframework.data.jpa.repository.JpaRepository; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | |
| | | public interface AppointmentCustomerViewRepository extends JpaRepository<AppointmentCustomerView, Long>{ |
| | | List<AppointmentCustomerView> findByAgentNo(String agentNo); |
| | | List<AppointmentCustomerView> findByAgentNoAndCustomerId(String agentNo, Long customerId); |
| | | List<AppointmentCustomerView> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status); |
| | | } |
¤ñ¹ï·sÀÉ®× |
| | |
| | | package com.pollex.pam.repository; |
| | | |
| | | import com.pollex.pam.domain.AppointmentExpiringNotifyRecord; |
| | | import org.springframework.data.jpa.repository.JpaRepository; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Repository |
| | | public interface AppointmentExpiringNotifyRecordRepository extends JpaRepository<AppointmentExpiringNotifyRecord, Long> { |
| | | List<AppointmentExpiringNotifyRecord> findAllByAppointmentId(Long appointmentId); |
| | | } |
| | |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | |
| | | import com.pollex.pam.enums.AppointmentStatusEnum; |
| | | import com.pollex.pam.enums.ContactStatusEnum; |
| | | import org.springframework.data.jpa.repository.JpaRepository; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | |
| | | List<Appointment> findByAgentNoAndCustomerId(String agentNo, Long customerId); |
| | | |
| | | Optional<Appointment> findTopByAgentNoAndCustomerIdOrderByAppointmentDateDesc(String agentNo, Long customerId); |
| | | |
| | | List<Appointment> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status); |
| | | } |
| | |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | |
| | | import com.pollex.pam.enums.SatisfactionStatusEnum; |
| | | import org.springframework.data.jpa.repository.JpaRepository; |
| | | import org.springframework.data.jpa.repository.Query; |
| | | import org.springframework.data.repository.query.Param; |
| | |
| | | |
| | | Optional<Satisfaction> findOneByAppointmentId(Long appointmentId); |
| | | |
| | | List<Satisfaction> findAllByStatus(SatisfactionStatusEnum status); |
| | | |
| | | @Query(value = "SELECT avg(score) FROM satisfaction where agent_no=:agent_no" |
| | | + " and score is not null" |
| | | , nativeQuery = true) |
| | | Float getAgentScoreAvg(@Param("agent_no") String agentNo); |
| | | } |
| | | |
| | |
| | | package com.pollex.pam.service; |
| | | |
| | | import java.time.Instant; |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | import java.util.Optional; |
| | |
| | | |
| | | import com.pollex.pam.appointment.process.AppointmentProcess; |
| | | import com.pollex.pam.config.ApplicationProperties; |
| | | import com.pollex.pam.service.dto.AppointmentUpdateDTO; |
| | | import com.pollex.pam.service.dto.ClosedProcessDTO; |
| | | import com.pollex.pam.service.dto.DoneProcessDTO; |
| | | import com.pollex.pam.service.dto.InterviewRecordDTO; |
| | | import com.pollex.pam.config.Constants; |
| | | import com.pollex.pam.service.dto.*; |
| | | import com.pollex.pam.web.rest.errors.NotFoundExpiringAppointmentException; |
| | | import com.pollex.pam.web.rest.errors.SendEmailFailException; |
| | | import com.pollex.pam.web.rest.errors.SendSMSFailException; |
| | | import io.jsonwebtoken.lang.Assert; |
| | |
| | | |
| | | import com.pollex.pam.domain.Appointment; |
| | | import com.pollex.pam.domain.AppointmentCustomerView; |
| | | import com.pollex.pam.domain.InterviewRecord; |
| | | import com.pollex.pam.enums.ContactStatusEnum; |
| | | import com.pollex.pam.enums.InterviewRecordStatusEnum; |
| | | import com.pollex.pam.repository.AppointmentCustomerViewRepository; |
| | | import com.pollex.pam.repository.AppointmentRepository; |
| | | import com.pollex.pam.security.SecurityUtils; |
| | | import com.pollex.pam.service.dto.AppointmentCloseDTO; |
| | | import com.pollex.pam.service.dto.AppointmentCreateDTO; |
| | | import com.pollex.pam.service.dto.AppointmentCustomerViewDTO; |
| | | import com.pollex.pam.service.mapper.AppointmentCustomerViewMapper; |
| | | import com.pollex.pam.service.mapper.AppointmentDTOMapper; |
| | | import com.pollex.pam.web.rest.errors.AppointmentNotFoundException; |
| | |
| | | |
| | | @Autowired |
| | | SpringTemplateEngine springTemplateEngine; |
| | | |
| | | |
| | | @Autowired |
| | | InterviewRecordService interviewRecordService; |
| | | |
| | | |
| | | @Autowired |
| | | AppointmentProcess abstractAppointmentProcess; |
| | | |
| | | |
| | | @Autowired |
| | | PersonalNotificationService personalNotificationService; |
| | | |
| | |
| | | appointment.setCommunicateStatus(ContactStatusEnum.CANCEL); |
| | | appointmentRepository.save(appointment); |
| | | personalNotificationService.createMarkAppointmentDeletedToConsultant(appointment); |
| | | |
| | | |
| | | } |
| | | |
| | | public List<Appointment> findByAgentNo(String agentNo) { |
| | |
| | | Assert.notNull(appointment, "appointment entity cannot be null"); |
| | | |
| | | log.debug("is need send appointment notify msg? = {}", applicationProperties.isSendNotifyMsg()); |
| | | |
| | | |
| | | log.debug("sending appointment notify, appointmentId = {}", appointment.getId()); |
| | | sendAppointmentNotifyBySMS(appointment); |
| | | sendAppointmentNotifyByHtmlEmail(appointment); |
| | |
| | | public String getAppointmentDetailUrl(Long appointmentId) { |
| | | return applicationProperties.getFrontEndDomain() + "/myAppointmentList/contactedList?appointmentId=" + appointmentId; |
| | | } |
| | | |
| | | |
| | | public Appointment findById(Long id) { |
| | | return appointmentRepository.findById(id) |
| | | .orElseThrow(AppointmentNotFoundException::new); |
| | |
| | | abstractAppointmentProcess.process(dto); |
| | | } |
| | | } |
| | | |
| | | public Long getConsultantPendingAppointmentSum(String agentNo) { |
| | | return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE) |
| | | .stream() |
| | | .filter(appointment -> agentNo.equals(appointment.getAgentNo())) |
| | | .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_PENDING_PHONE_INTERVAL, Constants.APPOINTMENT_PENDING_EMAIL_INTERVAL)) |
| | | .count(); |
| | | } |
| | | |
| | | public AppointmentCustomerViewDTO getCustomerNewestExpiringAppointment(Long customerId) { |
| | | return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE) |
| | | .stream() |
| | | .filter(appointment -> customerId.equals(appointment.getCustomerId())) |
| | | .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL)) |
| | | .max(Comparator.comparing(AppointmentCustomerView::getAppointmentDate)) |
| | | .map(appointmentCustomerView -> appointmentCustomerViewMapper.toAppointmentCustomerViewDTO(appointmentCustomerView)) |
| | | .orElse(null); |
| | | } |
| | | |
| | | public boolean isAppointmentDateNotInIntervalFromNow(AppointmentCustomerView appointment, int phoneInterval, int emailInterval) { |
| | | final boolean isHavePhone = StringUtils.hasText(appointment.getPhone()); |
| | | final boolean isHaveEmail = StringUtils.hasText(appointment.getEmail()); |
| | | |
| | | LocalDate appointmentDate = appointment.getAppointmentDate().atZone(ZoneId.systemDefault()).toLocalDate(); |
| | | LocalDate nowDate = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate(); |
| | | long intervalDays = nowDate.toEpochDay() - appointmentDate.toEpochDay(); |
| | | |
| | | final boolean isAppointmentExpiringByPhone = isHavePhone && intervalDays >= phoneInterval; |
| | | final boolean isAppointmentExpiringByEmail = isHaveEmail && intervalDays >= emailInterval; |
| | | |
| | | return isAppointmentExpiringByPhone || isAppointmentExpiringByEmail; |
| | | } |
| | | } |
| | |
| | | |
| | | @Autowired |
| | | ApplicationProperties applicationProperty; |
| | | |
| | | |
| | | @Autowired |
| | | SendMsgService sendMsgService; |
| | | |
| | | |
| | | @Autowired |
| | | SpringTemplateEngine springTemplateEngine; |
| | | |
| | | |
| | | @Autowired |
| | | ApplicationProperties applicationProperties; |
| | | |
| | | |
| | | @Autowired |
| | | ConsultantService consultantService; |
| | | |
| | | |
| | | @Autowired |
| | | SatisfactionRepository satisfactionRepository; |
| | | |
| | | |
| | | @Autowired |
| | | PersonalNotificationService personalNotificationService; |
| | | |
| | |
| | | dto, |
| | | appointmentService.findAvailableByAgentNoAndCustomerId(consultant.getAgentNo(), customerId) |
| | | ); |
| | | |
| | | |
| | | setFavoriteConsultantUpdatedTime(relation, dto); |
| | | |
| | | return dto; |
| | |
| | | if (!appointments.isEmpty()) { |
| | | AppointmentCustomerView latestAvailableAppointment = appointments.get(0); |
| | | ContactStatusEnum latestStatus = latestAvailableAppointment.getCommunicateStatus(); |
| | | if( latestStatus != ContactStatusEnum.DONE |
| | | || latestStatus != ContactStatusEnum.CLOSED) |
| | | if(latestStatus != ContactStatusEnum.DONE && latestStatus != ContactStatusEnum.CLOSED) |
| | | customerFavoriteConsultantDTO.setContactStatus(latestStatus); |
| | | else |
| | | customerFavoriteConsultantDTO.setContactStatus(PICKED); |
| | |
| | | |
| | | public void sendSatisfactionToClient(Appointment appointment) { |
| | | String subject = "滿æåº¦å¡«å¯«éç¥"; |
| | | |
| | | |
| | | if(StringUtils.hasText(appointment.getEmail())) { |
| | | String content = genSendSatisfactionEmailContent(appointment); |
| | | sendMsgService.sendMsgByEmail(appointment.getEmail(), subject, content, true); |
| | | |
| | | |
| | | }if(StringUtils.hasText(appointment.getPhone())) { |
| | | String content = genSendSatisfactionSMSContent(appointment); |
| | | sendMsgService.sendMsgBySMS(appointment.getPhone(), content); |
| | | } |
| | | |
| | | |
| | | personalNotificationService.createSendSatisfactionToClientToCustomer(appointment); |
| | | } |
| | | |
| | | |
| | | private String genSendSatisfactionSMSContent(Appointment appointment) { |
| | | String agentNo = appointment.getAgentNo(); |
| | | Consultant consultant = consultantService.findByAgentNo(agentNo); |
| | |
| | | public String getSendSatisfactionToClientUrl(Long appointmentId) { |
| | | return applicationProperties.getFrontEndDomain() + "/?appointmentId=" + appointmentId; |
| | | } |
| | | |
| | | |
| | | public void setConsultantAvgScore(Satisfaction satisfaction) { |
| | | float avgScore = getAgentAvgScore(satisfaction.getAgentNo()); |
| | | Consultant consultant = consultantRepository.findOneByAgentNo(satisfaction.getAgentNo()) |
| | |
| | | public float getAgentAvgScore(String agentNo) { |
| | | Float avgScore = satisfactionRepository.getAgentScoreAvg(agentNo); |
| | | if(avgScore==null)return 0; |
| | | BigDecimal bigDecimal = new BigDecimal(avgScore); |
| | | BigDecimal bigDecimal = new BigDecimal(avgScore); |
| | | return avgScore = bigDecimal.setScale(1,BigDecimal.ROUND_HALF_UP).floatValue(); |
| | | } |
| | | } |
| | |
| | | @Service |
| | | @Transactional |
| | | public class PersonalNotificationService { |
| | | |
| | | |
| | | @Autowired |
| | | PersonalNotificationRepository personalNotificationRepository; |
| | | |
| | | |
| | | @Autowired |
| | | ConsultantService consultantService; |
| | | |
| | | |
| | | @Autowired |
| | | AppointmentService appointmentService; |
| | | |
| | | |
| | | @Autowired |
| | | CustomerService customerService; |
| | | |
| | | |
| | | @Autowired |
| | | CustomerRepository customerRepository; |
| | | |
| | | |
| | | @Autowired |
| | | SatisfactionService satisfactionService; |
| | | |
| | |
| | | personalNotificationRepository.save(entity); |
| | | } |
| | | |
| | | public void createNotFillSatisfactionSumToCustomer(Long customerId, int notFillSatisfactionSum) { |
| | | PersonalNotification entity = new PersonalNotification(); |
| | | |
| | | String content = "æ¨æ "+notFillSatisfactionSum+" çé¡§åçæ»¿æåº¦éè¦å¡«å¯«"; |
| | | entity.setContent(content); |
| | | entity.setNotificationType(NotificationTypeEnum.ACTIVITY); |
| | | entity.setOwnerId(customerId); |
| | | entity.setOwnerRole(PersonalNotificationRoleEnum.CUSTOMER); |
| | | entity.setTitle("å®¢æ¶æ»¿æåº¦"); |
| | | personalNotificationRepository.save(entity); |
| | | } |
| | | |
| | | public void createEditConsultantToConsultant(Consultant consultant) { |
| | | PersonalNotification entity = new PersonalNotification(); |
| | | String content = "æ¨çå人帳èè¨å®å·²é²è¡æ´æ°"; |
| | |
| | | |
| | | @Autowired |
| | | CustomerRepository customerRepository; |
| | | |
| | | |
| | | @Autowired |
| | | ConsultantRepository consultantRepository; |
| | | |
| | | |
| | | @Autowired |
| | | ConsultantService consultantService; |
| | | |
| | | |
| | | @Autowired |
| | | PersonalNotificationService personalNotificationService; |
| | | |
| | |
| | | consultantService.setConsultantAvgScore(satisfaction); |
| | | return satisfaction; |
| | | } |
| | | |
| | | |
| | | public Satisfaction scorefaction(SatisfactionCustomerScoreDTO scoreDTO) { |
| | | Optional<Satisfaction> satisfactionOP = getByAppointmentId(scoreDTO.getAppointmentId()); |
| | | Satisfaction satisfaction = satisfactionOP.orElseThrow(SatisfactionNotFoundException::new); |
| | |
| | | personalNotificationService.createScorefactionToConsultant(satisfaction); |
| | | return satisfaction; |
| | | } |
| | | |
| | | |
| | | public Satisfaction createSatisfaction(Appointment appointment) { |
| | | boolean isexist = getByAppointmentId(appointment.getId()).isPresent(); |
| | | if(isexist) { |
| | |
| | | return satisfactionRepository.findOneByAppointmentId(appointmentId); |
| | | } |
| | | |
| | | public List<Satisfaction> getByStatus(SatisfactionStatusEnum status) { |
| | | return satisfactionRepository.findAllByStatus(status); |
| | | } |
| | | |
| | | public List<Satisfaction> scoreAllfaction(List<SatisfactionCustomerScoreDTO> scoreDTO) { |
| | | List<Satisfaction> satisfactionList = new ArrayList<>(); |
| | | scoreDTO.stream().forEach(dto ->{ |
¤ñ¹ï·sÀÉ®× |
| | |
| | | package com.pollex.pam.service; |
| | | |
| | | import com.pollex.pam.config.ApplicationProperties; |
| | | import com.pollex.pam.config.Constants; |
| | | import com.pollex.pam.domain.*; |
| | | import com.pollex.pam.enums.AppointmentStatusEnum; |
| | | import com.pollex.pam.enums.ContactStatusEnum; |
| | | import com.pollex.pam.enums.SatisfactionStatusEnum; |
| | | import com.pollex.pam.repository.AppointmentCustomerViewRepository; |
| | | import com.pollex.pam.repository.AppointmentExpiringNotifyRecordRepository; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.StringUtils; |
| | | import org.thymeleaf.context.Context; |
| | | import org.thymeleaf.spring5.SpringTemplateEngine; |
| | | |
| | | import java.time.Instant; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Optional; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Service |
| | | @Transactional |
| | | public class ScheduleTaskService { |
| | | |
| | | private static final String NOT_CONTACTED_NOTIFY_SUBJECT = "é ç´å®æªé²è¡è¯ç¹«éç¥"; |
| | | private static final Logger log = LoggerFactory.getLogger(ScheduleTaskService.class); |
| | | |
| | | @Autowired |
| | | ConsultantService consultantService; |
| | | |
| | | @Autowired |
| | | AppointmentService appointmentService; |
| | | |
| | | @Autowired |
| | | AppointmentCustomerViewRepository appointmentCustomerViewRepository; |
| | | |
| | | @Autowired |
| | | SendMsgService sendMsgService; |
| | | |
| | | @Autowired |
| | | SpringTemplateEngine springTemplateEngine; |
| | | |
| | | @Autowired |
| | | ApplicationProperties applicationProperties; |
| | | |
| | | @Autowired |
| | | AppointmentExpiringNotifyRecordRepository appointmentExpiringNotifyRecordRepository; |
| | | |
| | | @Autowired |
| | | SatisfactionService satisfactionService; |
| | | |
| | | @Autowired |
| | | PersonalNotificationService personalNotificationService; |
| | | |
| | | @Scheduled(cron = "0 30 8 * * *") |
| | | public void sendAppointmentPendingNotifyToConsultant() { |
| | | log.info("Starting send appointment pending notify to consultant"); |
| | | |
| | | Map<String, List<AppointmentCustomerView>> consultantWithPendingAppointments = |
| | | appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE) |
| | | .stream() |
| | | .filter(appointment -> |
| | | appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_PENDING_PHONE_INTERVAL, Constants.APPOINTMENT_PENDING_EMAIL_INTERVAL) |
| | | ) |
| | | .collect(Collectors.groupingBy(AppointmentCustomerView::getAgentNo)); |
| | | |
| | | consultantWithPendingAppointments.forEach((agentNo, pendingAppointments) -> { |
| | | int pendingAppointmentsSum = pendingAppointments.size(); |
| | | Consultant consultant = consultantService.findByAgentNo(agentNo); |
| | | String consultantPhoneNumber = consultant.getPhoneNumber(); |
| | | String consultantEmail = consultant.getEmail(); |
| | | String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum); |
| | | |
| | | sendMsgService.sendMsgBySMS(consultantPhoneNumber, String.format("æ¨æ%såé ç´å®æªé²è¡è¯ç¹«ï¼è«ç¡éèç", pendingAppointmentsSum)); |
| | | sendMsgService.sendMsgByEmail(consultantEmail, NOT_CONTACTED_NOTIFY_SUBJECT, emailContent, true); |
| | | }); |
| | | |
| | | log.info("Sending appointment pending notify to consultant finish"); |
| | | } |
| | | |
| | | @Scheduled(cron = "0 30 8 * * *") |
| | | public void sendAppointmentExpiringNotifyToCustomer() { |
| | | log.info("Starting send appointment expiring notify to customer"); |
| | | |
| | | List<AppointmentCustomerView> allByCommunicateStatus = |
| | | appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AppointmentStatusEnum.AVAILABLE) |
| | | .stream() |
| | | .filter(appointment -> |
| | | appointmentService.isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL) |
| | | ) |
| | | .filter(this::isAppointmentNotifyNotOnLimit) |
| | | .collect(Collectors.toList()); |
| | | |
| | | allByCommunicateStatus.forEach(appointment -> { |
| | | Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo()); |
| | | Optional<String> optionalPhone = Optional.ofNullable(appointment.getPhone()).filter(StringUtils::hasText); |
| | | Optional<String> optionalEmail = Optional.ofNullable(appointment.getEmail()).filter(StringUtils::hasText); |
| | | |
| | | optionalPhone.ifPresent(phone -> |
| | | sendMsgService.sendMsgBySMS(phone, String.format("徿±æï¼æ¨é ç´%s顧忣å¿ç¢ä¸ï¼è«æ¨åæ¶é ç´ä¸¦æ¹é¸å
¶ä»é¡§åï¼è«é»æç¶²åï¼%s" |
| | | , consultant.getName(), getAppointmentUrl(appointment.getId()))) |
| | | ); |
| | | optionalEmail.ifPresent(email -> |
| | | sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentUrl(appointment.getId())), true) |
| | | ); |
| | | |
| | | AppointmentExpiringNotifyRecord record = new AppointmentExpiringNotifyRecord(); |
| | | record.setAppointmentId(appointment.getId()); |
| | | record.setSendTime(Instant.now()); |
| | | |
| | | appointmentExpiringNotifyRecordRepository.save(record); |
| | | }); |
| | | |
| | | log.info("Sending appointment expiring notify to customer finish"); |
| | | } |
| | | |
| | | // todo é確èªè©²æé, otis todo=134497 |
| | | @Scheduled(cron = "0 0 9 * * *") |
| | | public void sendNotFillSatisfactionToPersonalNotification() { |
| | | Map<Long, List<Satisfaction>> customerNotFillSatisfactions = satisfactionService.getByStatus(SatisfactionStatusEnum.UNFILLED) |
| | | .stream() |
| | | .collect(Collectors.groupingBy(Satisfaction::getCustomerId)); |
| | | |
| | | customerNotFillSatisfactions.forEach((customerId, notFillSatisfactions) -> |
| | | personalNotificationService.createNotFillSatisfactionSumToCustomer(customerId, notFillSatisfactions.size()) |
| | | ); |
| | | } |
| | | |
| | | private boolean isAppointmentNotifyNotOnLimit(AppointmentCustomerView appointment) { |
| | | int sendNotifyToCustomerRecordSum = |
| | | appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size(); |
| | | |
| | | return sendNotifyToCustomerRecordSum < Constants.SEND_EXPIRING_NOTIFY_LIMIT; |
| | | } |
| | | |
| | | private String getAppointmentUrl(Long appointmentId) { |
| | | return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId; |
| | | } |
| | | |
| | | private String getAppointmentPendingNotifyEmailContent(int sum) { |
| | | Context context = new Context(); |
| | | context.setVariable("pendingAppointmentSum", sum); |
| | | return springTemplateEngine.process("mail/appointmentPendingNotifyEmail", context); |
| | | } |
| | | |
| | | private String getAppointmentExpiringNotifyEmail(String consultantName, String notifyUrl) { |
| | | Context context = new Context(); |
| | | context.setVariable("consultantName", consultantName); |
| | | context.setVariable("notifyUrl", notifyUrl); |
| | | return springTemplateEngine.process("mail/appointmentExpiringNotifyEmail", context); |
| | | } |
| | | } |
| | |
| | | |
| | | import com.pollex.pam.appointment.process.AppointmentProcess; |
| | | import com.pollex.pam.domain.Appointment; |
| | | import com.pollex.pam.enums.ContactStatusEnum; |
| | | import com.pollex.pam.security.SecurityUtils; |
| | | import com.pollex.pam.service.SendMsgService; |
| | | import com.pollex.pam.service.dto.AppointmentUpdateDTO; |
| | | import com.pollex.pam.service.dto.ClosedProcessDTO; |
| | | import com.pollex.pam.service.dto.DoneProcessDTO; |
| | | |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import com.pollex.pam.service.AppointmentService; |
| | |
| | | import com.pollex.pam.service.dto.AppointmentCloseDTO; |
| | | import com.pollex.pam.service.dto.AppointmentCreateDTO; |
| | | import com.pollex.pam.service.dto.AppointmentCustomerViewDTO; |
| | | |
| | | import java.util.Objects; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/appointment") |
| | |
| | | |
| | | @Autowired |
| | | SendMsgService sendMsgService; |
| | | |
| | | |
| | | @Autowired |
| | | AppointmentProcess abstractAppointmentProcess; |
| | | |
| | | |
| | | @Autowired |
| | | PersonalNotificationService personalNotificationService; |
| | | |
| | |
| | | appointmentService.recordConsultantReadTime(appointmentId); |
| | | return ResponseEntity.noContent().build(); |
| | | } |
| | | |
| | | |
| | | @PostMapping("/close") |
| | | public ResponseEntity<Void> closeAppointment(@RequestBody AppointmentCloseDTO closeDTO) { |
| | | appointmentService.closeAppointment(closeDTO); |
| | | return ResponseEntity.noContent().build(); |
| | | } |
| | | |
| | | |
| | | @GetMapping("/customer/expiring/newest") |
| | | public ResponseEntity<AppointmentCustomerViewDTO> getNewestExpiringAppointment() { |
| | | Long customerId = SecurityUtils.getCustomerDBId(); |
| | | AppointmentCustomerViewDTO customerNewestExpiringAppointment = appointmentService.getCustomerNewestExpiringAppointment(customerId); |
| | | |
| | | if(Objects.nonNull(customerNewestExpiringAppointment)) { |
| | | return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK); |
| | | } |
| | | else { |
| | | return new ResponseEntity<>(HttpStatus.NOT_FOUND); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/consultant/pending/sum") |
| | | public ResponseEntity<Long> getConsultantPendingAppointmentSum() { |
| | | String agentNo = SecurityUtils.getAgentNo(); |
| | | return new ResponseEntity<>(appointmentService.getConsultantPendingAppointmentSum(agentNo), HttpStatus.OK); |
| | | } |
| | | |
| | | // @PostMapping("/close/info/edit") |
| | | // public ResponseEntity<Void> editAppointmentClosedInfo(@RequestBody AppointmentCloseDTO closeDTO) { |
| | | // |
| | | // |
| | | // if(closeDTO.getContactStatus() == ContactStatusEnum.DONE) { |
| | | // DoneProcessDTO dto = new DoneProcessDTO(); |
| | | // BeanUtils.copyProperties(closeDTO, dto); |
| | |
| | | // }else { |
| | | // return ResponseEntity.notFound().build(); |
| | | // } |
| | | // |
| | | // |
| | | // return ResponseEntity.noContent().build(); |
| | | // } |
| | | } |
¤ñ¹ï·sÀÉ®× |
| | |
| | | package com.pollex.pam.web.rest.errors; |
| | | |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.web.bind.annotation.ResponseStatus; |
| | | |
| | | @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "not found any expiring appointment") |
| | | public class NotFoundExpiringAppointmentException extends RuntimeException { |
| | | } |
| | |
| | | datasource: |
| | | type: com.zaxxer.hikari.HikariDataSource |
| | | url: jdbc:postgresql://dev.pollex.com.tw:5433/pam_p2 |
| | | #url: jdbc:postgresql://localhost:5432/omo?currentSchema=omo |
| | | #url: jdbc:postgresql://localhost:5432/omo?currentSchema=public |
| | | username: pamadmin |
| | | password: pamadmin |
| | | hikari: |
| | |
| | | email.reset.text1=For your pamapi account a password reset was requested, please click on the URL below to reset it: |
| | | email.reset.text2=Regards, |
| | | |
| | | # satisfaction write email |
| | | # satisfaction write email |
| | | email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1} |
| | | |
| | | # appointment pending notify email |
| | | email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406 |
| | | |
| | | # appointment expiring notify email |
| | | email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1} |
| | |
| | | email.reset.text1=\u60A8\u7684 pamapi \u5E33\u865F\u88AB\u8981\u6C42\u91CD\u65B0\u8A2D\u5B9A\u5BC6\u78BC\uFF0C\u8ACB\u9EDE\u4E0B\u5217\u7DB2\u5740\u8A2D\u5B9A: |
| | | email.reset.text2=\u795D\u60A8\u4F7F\u7528\u6109\u5FEB\uFF0C |
| | | |
| | | # satisfaction write email |
| | | # satisfaction write email |
| | | email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1} |
| | | |
| | | # appointment pending notify email |
| | | email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406 |
| | | |
| | | # appointment expiring notify email |
| | | email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1} |
| | | |
¤ñ¹ï·sÀÉ®× |
| | |
| | | <!DOCTYPE html> |
| | | <html xmlns:th="http://www.thymeleaf.org" lang="zh"> |
| | | <head> |
| | | <title>é ç´å®æªèçéç¥</title> |
| | | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
| | | </head> |
| | | <body> |
| | | <p th:text="#{email.write.appointment.expiring.content(${consultantName}, ${notifyUrl})}">徿±æï¼æ¨é ç´xxx顧忣å¿ç¢ä¸ , è«æ¨åæ¶é ç´ä¸¦æ¹é¸å
¶ä»é¡§å</p> |
| | | </body> |
| | | </html> |
| | | |
¤ñ¹ï·sÀÉ®× |
| | |
| | | <!DOCTYPE html> |
| | | <html xmlns:th="http://www.thymeleaf.org" lang="zh"> |
| | | <head> |
| | | <title>é ç´å®æªèçéç¥</title> |
| | | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
| | | </head> |
| | | <body> |
| | | <p th:text="#{email.write.appointment.pending.content(${pendingAppointmentSum})}">æ¨æxåé ç´å®æªé²è¡è¯ç¹«ï¼è«ç¡éèç</p> |
| | | </body> |
| | | </html> |
| | | |