保誠-保戶業務員媒合平台
wayne
2022-01-26 6fa4bba623713c396432ba8b863846883d6a1906
Merge branch 'pollex-dev' into sit

刪除1個檔案
修改100個檔案
新增116個檔案
修改14個檔案名稱
11153 ■■■■ 已變更過的檔案
PAMapp/assets/icon/demo.html 72 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/fonts/icomoon.eot 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/fonts/icomoon.svg 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/fonts/icomoon.ttf 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/fonts/icomoon.woff 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/selection.json 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/style.css 25 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/appointment/avatar_bg.svg 9 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/logo.png 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/notification/banner_mob.svg 56 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/notification/banner_web.svg 60 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/satisfaction/satisfactionBtn_mob.svg 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/satisfaction/satisfactionBtn_web.svg 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/images/taiwan-logo.png 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/_common.scss 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/_variable.scss 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_heading.scss 45 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_icon.scss 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_utilities.scss 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/_elementUI.scss 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_dateTimePicker.scss 85 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_dialog.scss 35 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_input.scss 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_rate.scss 20 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_select.scss 42 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_textarea.scss 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_upload.scss 16 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/AddressPicker.vue 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentClosedInfo.vue 75 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentInterviewList.vue 71 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentProgress.vue 119 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Appointment/AppointmentRecordList.vue 53 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/BackActionBar.vue 36 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Client/ClientCard.vue 449 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Client/ClientList.vue 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Consultant/ConsultantCard.vue 214 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Consultant/ConsultantSwiper.vue 22 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/DateTimePicker.vue 65 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewAdd.vue 266 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewCard.vue 176 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewMsg.vue 176 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Interview/InterviewRecordCard.vue 98 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/NavBar.vue 114 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/QuickFilter/QuickFilterConsultantList.vue 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/QuickFilter/QuickFilterSelector.vue 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/ReviewRecords/ReviewRecords.vue 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiAvatar.vue 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiDateFormat.vue 8 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiDatePicker.vue 71 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiField.vue 20 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiSelect.vue 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/Ui/UiTimePicker.vue 118 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/editConsultantAvatar.vue 127 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/loading.vue 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/multiSelectBtn.vue 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/layouts/default.vue 21 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/middleware/errorRoute.ts 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/middleware/getUrlQuery.ts 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/nuxt.config.js 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/accountSetting/index.vue 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/_agentNo.vue 69 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/edit/_agentNo.vue 480 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/close/index.vue 316 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/index.vue 294 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/interview/_interviewId/index.vue 14 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/interview/new/index.vue 55 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/interviewList/index.vue 38 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointment/_appointmentId/recordList/index.vue 32 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/appointmentAgenda/index.vue 68 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/consultantLogin/index.vue 58 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/index.vue 316 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/index.vue 408 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 72 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/appointmentList.vue 64 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/closedList.vue 156 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList.vue 96 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList.vue 40 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/notification/index.vue 126 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/_agentNo.vue 242 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/index.vue 5 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/result.vue 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/index.vue 15 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/satisfactionList.vue 161 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviewsRecord/index.vue 13 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/plugins/filters/appointment-fail-reason.filter.ts 16 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/plugins/filters/date.filter.ts 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/const/agent-communication-style-list.ts 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/const/agent-expert-list.ts 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/const/appointment-fail-reason-list.ts 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/account.model.ts 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/agent-info.model.ts 36 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/appointment.model.ts 251 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/client.model.ts 22 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/enum/contact-status.ts 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/reviews.model.ts 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/models/strict-query.model.ts 18 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/account-setting.service.ts 7 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/appointment.service.ts 52 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/httpClient.ts 13 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/my-consultant.service.ts 24 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/otp.service.ts 57 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/shared/services/reviews.service.ts 28 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/appointment.store.ts 84 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/index.ts 62 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/localStorage.ts 38 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/login.store.ts 28 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20211229_j.sql 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220103_w.sql 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220112_j.sql 66 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220114_j.sql 2 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220120_j.sql 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220121_j.sql 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/20220122_w.sql 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/sql/淨空整個系統資料(除顧問).sql 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/小鈴鐺通知API/取得登入者所有小鈴鐺通知API 38 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/小鈴鐺通知API/所有小鈴鐺通知設為已讀.txt 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/滿意度/客戶填寫滿意度.txt 34 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/滿意度/顧問主動發送滿意度通知API.txt 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/約訪紀錄API/修改約訪紀錄.txt 31 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/約訪紀錄API/刪除約訪紀錄.txt 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/約訪紀錄API/新增約訪紀錄.txt 27 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/約訪通知API/發送約訪通知API.txt 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/刪除預約單備註.txt 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/取得預約單細節API.txt 74 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/客戶取得最新預約的未處理預約單.txt 61 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/新增預約單備註.txt 14 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/更新預約單備註.txt 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/標記為已聯絡API.txt 58 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/結案API.txt 32 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/顧問取得所有自己的預約單API.txt 177 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/預約單/顧問取得未處理預約單數量通知.txt 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/顧問API/取得顧問頭像.txt 6 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/顧問API/指定顧問詳細資訊.txt 44 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/顧問API/編輯修改顧問資料.txt 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/appointment/process/AppointmentProcess.java 57 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/appointment/process/AppointmentProcessInterface.java 14 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/appointment/process/ClosedProcess.java 65 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/appointment/process/DoneProcess.java 64 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/config/ApplicationProperties.java 46 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/config/Constants.java 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/Appointment.java 55 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentClosedInfo.java 112 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentCustomerView.java 19 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java 46 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentMemo.java 57 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/AppointmentNoticeLog.java 135 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/Consultant.java 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/InterviewRecord.java 87 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/PersonalNotification.java 123 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/ContactStatusEnum.java 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/InterviewRecordStatusEnum.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/NotificationTypeEnum.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/PersonalNotificationRoleEnum.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentClosedInfoRepository.java 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentCustomerViewRepository.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentMemoRepository.java 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentNoticeLogRepository.java 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/InterviewRecordRepository.java 18 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/PersonalNotificationRepository.java 16 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/SatisfactionRepository.java 5 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentClosedInfoService.java 22 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentMemoService.java 60 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentNoticeLogService.java 33 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java 104 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ConsultantService.java 154 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/InterviewRecordService.java 84 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/NoticeService.java 57 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/PersonalNotificationService.java 159 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/SatisfactionService.java 76 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java 163 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/SendMsgService.java 69 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AbstractAppointmentProcessDTO.java 37 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentCloseDTO.java 77 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentCustomerViewDTO.java 37 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentMemoCreateDTO.java 22 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentMemoUpdateDTO.java 27 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentNoticeSendDTO.java 53 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/ClosedProcessDTO.java 29 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantDTO.java 9 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantDetailDTO.java 38 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantEditDTO.java 139 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/DoneProcessDTO.java 39 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/InterviewRecordDTO.java 69 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/SatisfactionCustomerScoreDTO.java 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentCustomerViewMapper.java 32 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentMapper.java 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentMemoMapper.java 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentNoticeSendMapper.java 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/ConsultantDTOMapper.java 30 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/ConsultantMapper.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/InterviewRecordMapper.java 37 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/SatisfactionDTOMapper.java 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/util/FileUtil.java 69 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/util/StringUtils.java 18 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentMemoResource.java 45 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java 62 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/ConsultantResource.java 55 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/InterviewRecordResource.java 36 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/NoticeResource.java 35 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/PersonalNotificationResource.java 55 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/SatisfactionResource.java 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/TestSendMsgResource.java 52 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/AppointmentClosedInfoNotFoundException.java 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/AppointmentMemoNotFoundException.java 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/InterviewRecordNotFoundException.java 14 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/NotFoundExpiringAppointmentException.java 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/SatisfactionAlreadyExistException.java 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/errors/SatisfactionNotFoundException.java 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-dev.yml 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-pollex.yml 19 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-prod.yml 24 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-sit.yml 5 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/config/application-uat.yml 5 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages.properties 9 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/i18n/messages_zh_TW.properties 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_A183619275.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_AG0101234567.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_AG0109051204.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_AGAM11249699.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_B282677963.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_D265260662.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_J149388015.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_R221444250.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_X147309614.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/static/consultant/consultant_Z152717443.jpg 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/resources/templates/mail/writeSatisfactionNotice.html 10 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/icon/demo.html
@@ -9,12 +9,82 @@
    <link rel="stylesheet" href="style.css"></head>
<body>
    <div class="bgc1 clearfix">
        <h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;34)</small></h1>
        <h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;39)</small></h1>
    </div>
    <div class="clearfix mhl ptl">
        <h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-sort-decrease"></span>
                <span class="mls"> icon-sort-decrease</span>
            </div>
            <fieldset class="fs0 size1of1 clearfix hidden-false">
                <input type="text" readonly value="e922" class="unit size1of2" />
                <input type="text" maxlength="1" readonly value="&#xe922;" class="unitRight size1of2 talign-right" />
            </fieldset>
            <div class="fs0 bshadow0 clearfix hidden-true">
                <span class="unit pvs fgc1">liga: </span>
                <input type="text" readonly value="" class="liga unitRight" />
            </div>
        </div>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-sort-add"></span>
                <span class="mls"> icon-sort-add</span>
            </div>
            <fieldset class="fs0 size1of1 clearfix hidden-false">
                <input type="text" readonly value="e923" class="unit size1of2" />
                <input type="text" maxlength="1" readonly value="&#xe923;" class="unitRight size1of2 talign-right" />
            </fieldset>
            <div class="fs0 bshadow0 clearfix hidden-true">
                <span class="unit pvs fgc1">liga: </span>
                <input type="text" readonly value="" class="liga unitRight" />
            </div>
        </div>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-expand"></span>
                <span class="mls"> icon-expand</span>
            </div>
            <fieldset class="fs0 size1of1 clearfix hidden-false">
                <input type="text" readonly value="e924" class="unit size1of2" />
                <input type="text" maxlength="1" readonly value="&#xe924;" class="unitRight size1of2 talign-right" />
            </fieldset>
            <div class="fs0 bshadow0 clearfix hidden-true">
                <span class="unit pvs fgc1">liga: </span>
                <input type="text" readonly value="" class="liga unitRight" />
            </div>
        </div>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-sex-female"></span>
                <span class="mls"> icon-sex-female</span>
            </div>
            <fieldset class="fs0 size1of1 clearfix hidden-false">
                <input type="text" readonly value="e925" class="unit size1of2" />
                <input type="text" maxlength="1" readonly value="&#xe925;" class="unitRight size1of2 talign-right" />
            </fieldset>
            <div class="fs0 bshadow0 clearfix hidden-true">
                <span class="unit pvs fgc1">liga: </span>
                <input type="text" readonly value="" class="liga unitRight" />
            </div>
        </div>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-sex-male"></span>
                <span class="mls"> icon-sex-male</span>
            </div>
            <fieldset class="fs0 size1of1 clearfix hidden-false">
                <input type="text" readonly value="e926" class="unit size1of2" />
                <input type="text" maxlength="1" readonly value="&#xe926;" class="unitRight size1of2 talign-right" />
            </fieldset>
            <div class="fs0 bshadow0 clearfix hidden-true">
                <span class="unit pvs fgc1">liga: </span>
                <input type="text" readonly value="" class="liga unitRight" />
            </div>
        </div>
        <div class="glyph fs1">
            <div class="clearfix bshadow0 pbs">
                <span class="icon-decrease"></span>
                <span class="mls"> icon-decrease</span>
            </div>
PAMapp/assets/icon/fonts/icomoon.eot
Binary files differ
PAMapp/assets/icon/fonts/icomoon.svg
@@ -41,4 +41,9 @@
<glyph unicode="&#xe91f;" glyph-name="time" d="M511.488 960c-282.55-0.292-511.488-229.41-511.488-512 0-282.77 229.23-512 512-512s512 229.23 512 512v0c0 0.076 0 0.166 0 0.257 0 282.628-229.116 511.744-511.744 511.744-0.27 0-0.54 0-0.81-0.001h0.042zM512 38.4c-226.216 0-409.6 183.384-409.6 409.6s183.384 409.6 409.6 409.6c226.216 0 409.6-183.384 409.6-409.6v0c0-226.216-183.384-409.6-409.6-409.6v0zM537.6 704h-76.8v-307.2l268.8-161.28 38.4 63.147-230.4 136.533z" />
<glyph unicode="&#xe920;" glyph-name="top" d="M563.115 736.768l407.851-408.107c13.137-12.992 21.272-31.018 21.272-50.943 0-19.756-7.997-37.645-20.932-50.605l-68.138-68.138c-12.998-13.055-30.985-21.133-50.859-21.133-19.725 0-37.592 7.958-50.564 20.838l-289.532 289.788-289.195-289.195c-12.998-13.055-30.985-21.133-50.859-21.133-19.725 0-37.592 7.958-50.564 20.838l0.004-0.004-0.213 0.299-68.267 67.499c-13.075 12.989-21.168 30.978-21.168 50.859 0 19.71 7.956 37.563 20.831 50.521l408.231 408.316c13.040 13.276 31.18 21.503 51.243 21.503 19.596 0 37.358-7.849 50.315-20.575l-0.011 0.010z" />
<glyph unicode="&#xe921;" glyph-name="trophy" horiz-adv-x="1152" d="M1104.017 831.982h-207.987v80.017c0 26.51-21.491 48.001-48.001 48.001v0h-544.040c-26.51 0-48.001-21.491-48.001-48.001v0-80.017h-207.987c-26.51 0-48.001-21.491-48.001-48.001v0-111.986c7.076-85.462 54.363-158.548 122.682-200.96l1.112-0.643c61.778-44.226 136.678-74.099 217.854-83.23l2.133-0.195c34.389-59.486 80.159-108.69 134.709-145.998l1.518-0.98v-144.002h-96.001c-3.854 0.457-8.318 0.718-12.843 0.718-62.746 0-113.776-50.163-115.172-112.574l-0.002-0.13v-24c0-13.255 10.745-24 24-24v0h592.041c13.255 0 24 10.745 24 24v0 24c-1.399 62.54-52.429 112.704-115.175 112.704-4.525 0-8.989-0.261-13.378-0.768l0.535 0.050h-96.001v144.002c56.068 38.356 101.826 87.64 135.041 145.086l1.137 2.132c83.314 9.166 158.252 38.973 221.451 84.147l-1.416-0.962c69.366 43.121 116.621 116.171 123.729 200.646l0.065 0.957v111.986c0 26.51-21.491 48.001-48.001 48.001v0zM198.627 574.41c-35.829 20.914-61.682 55.601-70.437 96.621l-0.172 0.964v32.016h128.402c1.638-61.997 10.873-121.145 26.808-177.479l-1.223 5.061c-31.815 11.002-59.407 25.499-84.329 43.47l0.951-0.653zM1024 671.995c-9.695-41.665-35.309-76.091-69.932-97.202l-0.677-0.383c-24.040-17.319-51.713-31.817-81.346-42.129l-2.271-0.688c14.711 51.273 23.946 110.421 25.564 171.467l0.020 0.952h128.594z" />
<glyph unicode="&#xe922;" glyph-name="sort-decrease" d="M482.987 159.915h115.371c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-115.371c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM244.395 842.923c-5.317 5.071-12.534 8.192-20.48 8.192s-15.163-3.121-20.492-8.203l0.012 0.011-143.701-173.056c-5.035-5.211-8.137-12.317-8.137-20.148 0-15.764 12.572-28.592 28.237-29.003l0.038-0.001h86.357v-548.181c0-0.001 0-0.001 0-0.002 0-15.835 12.837-28.672 28.672-28.672 0.12 0 0.24 0.001 0.36 0.002h57.326c0.101-0.001 0.221-0.002 0.341-0.002 15.835 0 28.672 12.837 28.672 28.672 0 0.001 0 0.001 0 0.002v0 546.133h86.357c0.001 0 0.001 0 0.002 0 15.835 0 28.672 12.837 28.672 28.672 0 0.12-0.001 0.24-0.002 0.36v-0.018c-0.093 7.822-3.197 14.902-8.203 20.15l0.011-0.012zM482.987 620.715h345.771c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-345.771c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM482.987 390.315h230.741c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-230.741c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM482.987 851.115h461.141c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-461.141c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672z" />
<glyph unicode="&#xe923;" glyph-name="sort-add" d="M482.987 851.115h115.371c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-115.371c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM482.987 620.715h230.741c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-230.741c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM482.987 159.915h461.141c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-461.141c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM482.987 390.315h345.771c15.835 0 28.672-12.837 28.672-28.672v-58.027c0-15.835-12.837-28.672-28.672-28.672h-345.771c-15.835 0-28.672 12.837-28.672 28.672v58.027c0 15.835 12.837 28.672 28.672 28.672zM367.957 275.285h-86.357v548.181c0 0.001 0 0.001 0 0.002 0 15.835-12.837 28.672-28.672 28.672-0.12 0-0.24-0.001-0.36-0.002h-57.326c-0.101 0.001-0.221 0.002-0.341 0.002-15.835 0-28.672-12.837-28.672-28.672 0-0.001 0-0.001 0-0.002v0-546.133h-86.357c-15.876-0.193-28.672-13.108-28.672-29.011 0-0.001 0-0.001 0-0.002v0c0-0.005 0-0.011 0-0.017 0-7.894 3.268-15.026 8.526-20.114l0.008-0.007 145.067-175.104c5.264-5.050 12.423-8.16 20.309-8.16s15.045 3.11 20.319 8.169l-0.010-0.010 144.043 173.056c5.21 5.192 8.433 12.374 8.433 20.309s-3.223 15.117-8.432 20.309l-0.001 0.001c-5.098 5.284-12.242 8.566-20.153 8.566-0.475 0-0.948-0.012-1.417-0.035l0.066 0.003z" />
<glyph unicode="&#xe924;" glyph-name="expand" d="M769.024 444.928c-28.277 0-51.2-22.923-51.2-51.2v0-266.581c-0.191-20.659-16.888-37.355-37.528-37.547h-489.149c-20.736 0-37.547 16.81-37.547 37.547v0 489.131c0.571 20.291 17.156 36.523 37.533 36.523 0.005 0 0.010 0 0.015 0h266.581c28.277 0 51.2 22.923 51.2 51.2s-22.923 51.2-51.2 51.2v0h-266.581c-77.212-0.194-139.753-62.734-139.947-139.928v-488.126c0.194-77.212 62.734-139.753 139.928-139.947h489.15c76.772 0.771 138.733 63.091 138.923 139.928v266.599c0 0.003 0 0.007 0 0.010 0 27.917-22.343 50.615-50.122 51.189l-0.054 0.001zM969.045 876.373v0c-5.53 12.604-15.385 22.459-27.646 27.855l-0.344 0.135c-5.708 2.654-12.373 4.272-19.397 4.436l-0.059 0.001h-266.581c-28.277 0-51.2-22.923-51.2-51.2s22.923-51.2 51.2-51.2v0h143.019l-401.408-401.408c-9.335-9.225-15.116-22.028-15.116-36.181s5.781-26.956 15.111-36.176l0.005-0.005c9.263-9.252 22.054-14.974 36.181-14.974s26.918 5.722 36.182 14.974l401.407 401.407v-143.019c0-28.277 22.923-51.2 51.2-51.2s51.2 22.923 51.2 51.2v0 266.581c-0.029 6.778-1.406 13.226-3.877 19.101l0.122-0.328z" />
<glyph unicode="&#xe925;" glyph-name="sex-female" d="M793.941 584.533c3.545 16.844 5.574 36.198 5.574 56.026 0 157.031-127.299 284.331-284.331 284.331-142.597 0-260.676-104.971-281.179-241.856l-0.192-1.562c-2.833-14.767-4.453-31.752-4.453-49.114 0-53.505 15.388-103.416 41.981-145.548l-0.664 1.126c41.637-71.349 112.064-122.023 194.943-136.296l1.665-0.237v-74.069h-149.845c-0.607 0.027-1.319 0.043-2.035 0.043-26.769 0-48.469-21.7-48.469-48.469 0-8.664 2.273-16.798 6.256-23.836l-0.126 0.241c8.178-14.971 23.811-24.956 41.777-24.956 0.673 0 1.343 0.014 2.009 0.042l-0.095-0.003h153.941v-57.344c0-32.085 0-64.512 0-96.597 0-0.060 0-0.132 0-0.203 0-13.24 5.346-25.231 13.997-33.932l-0.002 0.002c8.546-9.046 20.625-14.677 34.019-14.677 0.040 0 0.080 0 0.121 0h-0.006c0.001 0 0.003 0 0.005 0 26.529 0 48.080 21.313 48.464 47.75v0.036c0 22.869 0 45.739 0 68.267v87.381h153.259c0.924-0.164 1.987-0.258 3.072-0.258s2.148 0.094 3.182 0.274l-0.11-0.016c25.517 1.545 45.626 22.617 45.626 48.386 0 14.226-6.129 27.021-15.89 35.888l-0.040 0.036c-8.301 7.658-19.435 12.353-31.666 12.353-0.868 0-1.73-0.024-2.587-0.070l0.119 0.005c-36.523 0-72.704 0-109.227 0h-44.373v75.776c115.769 23.213 205.016 114.415 225.036 229.387l0.244 1.695zM704.171 635.733c0.001-0.273 0.002-0.597 0.002-0.92 0-51.896-21.29-98.82-55.611-132.513l-0.028-0.028c-34.195-34.070-81.368-55.135-133.461-55.135s-99.267 21.064-133.467 55.14l0.006-0.006c-34.070 34.195-55.135 81.368-55.135 133.461s21.064 99.267 55.14 133.467l-0.006-0.006c33.592 33.54 79.971 54.282 131.195 54.282 0.677 0 1.353-0.004 2.028-0.011l-0.103 0.001c0.001 0 0.002 0 0.003 0 103.888 0 188.173-83.927 188.754-187.678v-0.055z" />
<glyph unicode="&#xe926;" glyph-name="sex-male" d="M565.248 556.885v189.781l137.899-138.24c2.383-2.598 5.074-4.855 8.030-6.731l0.162-0.096c7.571-5.13 16.909-8.19 26.961-8.19 3.507 0 6.926 0.372 10.221 1.080l-0.318-0.057c13.076 2.557 24.025 10.155 30.95 20.643l0.111 0.179c5.394 7.775 8.618 17.412 8.618 27.801 0 14.575-6.344 27.668-16.42 36.669l-0.048 0.042-219.819 219.819c-1.695 2.13-3.484 4.027-5.418 5.765l-0.043 0.038c-8.628 7.35-19.905 11.822-32.226 11.822-1.633 0-3.248-0.079-4.841-0.232l0.203 0.016c-13.699-0.917-25.744-7.317-34.079-17.002l-0.054-0.064-216.405-216.405c-3.559-3.176-6.796-6.522-9.776-10.089l-0.122-0.151c-5.953-8.072-9.528-18.216-9.528-29.195 0-14.889 6.574-28.242 16.978-37.315l0.059-0.050c8.443-7.322 19.539-11.784 31.678-11.784 1.465 0 2.915 0.065 4.347 0.192l-0.185-0.013c13.664 0.478 25.785 6.705 34.082 16.323l0.051 0.061 136.533 136.533v-190.805c-137.566-25.119-240.475-144.041-240.475-286.998 0-144.662 105.377-264.711 243.558-287.575l1.695-0.232c14.016-2.329 30.205-3.692 46.701-3.754h0.062c160.99 0 291.499 130.508 291.499 291.499 0 142.407-102.118 260.963-237.121 286.436l-1.813 0.284zM671.744 156.843l-13.312-16.725-7.509-9.216c-34.834-35.13-83.115-56.879-136.476-56.879-47.528 0-91.026 17.254-124.572 45.84l0.27-0.224c-43.547 33.976-71.285 86.461-71.285 145.424 0 21.257 3.605 41.672 10.237 60.666l-0.392-1.29c20.237 73.614 82.804 128.246 159.267 136.462l0.818 0.071h51.883c94.474-13.866 166.186-94.355 166.186-191.594 0-41.636-13.148-80.202-35.519-111.771l0.405 0.602z" />
</font></defs></svg>
PAMapp/assets/icon/fonts/icomoon.ttf
Binary files differ
PAMapp/assets/icon/fonts/icomoon.woff
Binary files differ
PAMapp/assets/icon/selection.json
@@ -1 +1 @@
{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M947.023 423.724h-834.737c-42.513 0-76.977 34.464-76.977 76.977s34.464 76.977 76.977 76.977h834.737c42.513 0 76.977-34.464 76.977-76.977s-34.464-76.977-76.977-76.977h0z"],"attrs":[{}],"width":1059,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["decrease"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":2,"id":34,"name":"decrease","prevSize":32,"code":59648},"setIdx":0,"setId":2,"iconIdx":0},{"icon":{"paths":["M944.305 432.195h-352.501v-352.501c-0.083-44.019-35.777-79.674-79.803-79.695l-0.002-0c-43.994 0.083-79.632 35.735-79.695 79.725l-0 0.006v352.464h-352.537c-44.055 0-79.768 35.713-79.768 79.768s35.713 79.768 79.768 79.768v0h352.501v352.574c0 44.055 35.713 79.768 79.768 79.768s79.768-35.713 79.768-79.768l-0 0v-352.574h352.501c44.055 0 79.768-35.713 79.768-79.768s-35.713-79.768-79.768-79.768l0 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["add"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":3,"id":33,"name":"add","prevSize":32,"code":59649},"setIdx":0,"setId":2,"iconIdx":1},{"icon":{"paths":["M358.4 0c-0.076-0-0.166-0-0.256-0-197.797 0-358.144 160.347-358.144 358.144 0 0.090 0 0.18 0 0.27l-0-0.014c0 268.8 358.4 665.6 358.4 665.6s358.4-396.8 358.4-665.6c0-0.076 0-0.166 0-0.256 0-197.797-160.347-358.144-358.144-358.144-0.090 0-0.18 0-0.27 0l0.014-0zM358.4 486.4c-70.692 0-128-57.308-128-128s57.308-128 128-128c70.692 0 128 57.308 128 128v0c0 70.692-57.308 128-128 128h0z"],"attrs":[{}],"width":717,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["address"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":4,"id":32,"name":"address","prevSize":32,"code":59650},"setIdx":0,"setId":2,"iconIdx":2},{"icon":{"paths":["M447.993 511.981c141.38 0 255.991-114.611 255.991-255.991s-114.611-255.991-255.991-255.991c-141.38 0-255.991 114.611-255.991 255.991v-0c0 141.38 114.611 255.991 255.991 255.991v-0zM639.585 577.164l-95.609 382.81-63.988-272.006 63.988-111.998h-191.965l63.988 111.998-63.988 272.006-95.609-382.773c-142.815 6.542-256.155 123.796-256.401 267.577l-0 0.024v179.197h895.986v-179.197c-0.246-143.806-113.586-261.059-255.814-267.58l-0.587-0.021z"],"attrs":[{}],"width":896,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["agent"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":5,"id":31,"name":"agent","prevSize":32,"code":59651},"setIdx":0,"setId":2,"iconIdx":3},{"icon":{"paths":["M889.022 7.054l-831.526 383.772c-34.358 15.411-57.857 49.317-57.857 88.711 0 53.291 43.002 96.539 96.2 96.936l0.038 0h351.649v351.801c0.219 53.372 43.536 96.553 96.938 96.553 39.559 0 73.583-23.696 88.654-57.668l0.245-0.618 383.621-831.564c3.826-10.169 6.041-21.922 6.041-34.192 0-55.109-44.674-99.783-99.783-99.783-12.281 0-24.044 2.219-34.909 6.277l0.689-0.226z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["arrow"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":14,"id":30,"name":"arrow","prevSize":32,"code":59652},"setIdx":0,"setId":2,"iconIdx":4},{"icon":{"paths":["M448.963 503.181c138.95 0 251.59-112.641 251.59-251.59s-112.641-251.59-251.59-251.59v-0c-138.95 0-251.59 112.641-251.59 251.59s112.641 251.59 251.59 251.59v0zM448.963 119.488v0c72.958-0 132.103 59.144 132.103 132.103s-59.144 132.103-132.103 132.103c-72.958 0-132.103-59.144-132.103-132.103 0-0.013 0-0.026 0-0.039l-0 0.002c-0-72.938 59.128-132.065 132.065-132.065 0.013 0 0.026 0 0.039 0l-0.002-0z","M448.963 566.069c-167.938 0-448.963 50.013-448.963 217.355v240.576h893.721v-240.576c0-167.305-276.82-217.355-444.758-217.355zM774.233 904.475h-654.931v-121.051c0-40.263 142.671-97.83 329.475-97.83v0c186.804 0 325.27 57.567 325.27 97.83z"],"attrs":[{},{}],"width":894,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["avatar"],"colorPermutations":{"12552552551":[{},{}]}},"attrs":[{},{}],"properties":{"order":15,"id":29,"name":"avatar","prevSize":32,"code":59653},"setIdx":0,"setId":2,"iconIdx":5},{"icon":{"paths":["M908.486 692.817c-1.1-0.834-112.139-84.872-112.139-362.886 0-182.216-147.715-329.931-329.931-329.931s-329.931 147.715-329.931 329.931v0c0 278.014-111.039 362.052-111.191 362.242-15.359 10.347-25.326 27.674-25.326 47.328 0 31.414 25.465 56.881 56.878 56.885l819.14 0c25.031-0.194 46.179-16.601 53.474-39.229l0.112-0.401c1.696-5.184 2.675-11.15 2.675-17.344 0-19.089-9.291-36.008-23.598-46.482l-0.162-0.113zM174.901 682.616c47.923-94.131 76-205.282 76-322.99 0-10.443-0.221-20.835-0.659-31.17l0.050 1.475c0-119.383 96.779-216.162 216.162-216.162s216.162 96.779 216.162 216.162l0 0c-0.387 8.856-0.608 19.244-0.608 29.684 0 117.708 28.062 228.862 77.862 327.136l-1.9-4.135z","M573.853 872.459c-8.181-4.825-18.026-7.675-28.536-7.675-20.904 0-39.175 11.276-49.061 28.077l-0.145 0.266c-6.013 10.266-16.989 17.051-29.55 17.051-6.311 0-12.222-1.713-17.293-4.699l0.16 0.087c-5.181-3.025-9.376-7.22-12.314-12.241l-0.086-0.16c-10.022-17.142-28.338-28.474-49.3-28.474-31.434 0-56.916 25.482-56.916 56.916 0 10.472 2.828 20.283 7.762 28.711l-0.145-0.269c26.074 44.41 73.596 73.753 127.972 73.753s101.897-29.344 127.594-73.059l0.377-0.694c4.808-8.17 7.648-17.998 7.648-28.489 0-20.831-11.197-39.047-27.901-48.958l-0.263-0.144z"],"attrs":[{},{}],"width":932,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["bell"],"colorPermutations":{"12552552551":[{},{}]}},"attrs":[{},{}],"properties":{"order":16,"id":28,"name":"bell","prevSize":32,"code":59654},"setIdx":0,"setId":2,"iconIdx":6},{"icon":{"paths":["M307.2 460.8h-102.4v102.4h102.4zM512 460.8h-102.4v102.4h102.4zM716.8 460.8h-102.4v102.4h102.4zM819.2 102.4h-51.2v-102.4h-102.4v102.4h-409.6v-102.4h-102.4v102.4h-51.2c-56.277 0.024-101.889 45.651-101.889 101.931 0 0.165 0 0.33 0.001 0.495l-0-0.025-0.512 716.8c0 56.554 45.846 102.4 102.4 102.4v0h716.8c56.486-0.169 102.231-45.914 102.4-102.384l0-0.016v-716.8c-0.169-56.486-45.914-102.231-102.384-102.4l-0.016-0zM819.2 921.6h-716.8v-563.2h716.8z"],"attrs":[{}],"width":922,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["calender"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":17,"id":27,"name":"calender","prevSize":32,"code":59655},"setIdx":0,"setId":2,"iconIdx":7},{"icon":{"paths":["M910.222 0h-796.444c-62.838 0-113.778 50.94-113.778 113.778v0 796.444c0 62.838 50.94 113.778 113.778 113.778v0h796.444c62.838 0 113.778-50.94 113.778-113.778v0-796.444c0-62.838-50.94-113.778-113.778-113.778v0zM398.222 796.444l-284.444-284.444 80.213-80.213 204.231 203.662 431.787-431.787 80.213 80.782z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["checkbox-1"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":18,"id":26,"name":"checkbox-1","prevSize":32,"code":59656},"setIdx":0,"setId":2,"iconIdx":8},{"icon":{"paths":["M910.222 113.778v796.444h-796.444v-796.444h796.444zM910.222 0h-796.444c-62.76 0.193-113.584 51.018-113.778 113.759l-0 0.019v796.444c0.193 62.76 51.018 113.584 113.759 113.778l0.019 0h796.444c62.76-0.193 113.584-51.018 113.778-113.759l0-0.019v-796.444c-0.193-62.76-51.018-113.584-113.759-113.778l-0.019-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["checkbox"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":19,"id":25,"name":"checkbox","prevSize":32,"code":59657},"setIdx":0,"setId":2,"iconIdx":9},{"icon":{"paths":["M512 1024c-282.77 0-512-229.23-512-512s229.23-512 512-512c282.77 0 512 229.23 512 512v0c0 282.77-229.23 512-512 512h-0zM276.821 547.072l279.723 279.723c8.937 8.992 21.312 14.557 34.987 14.557s26.050-5.565 34.984-14.554l0.002-0.002 35.115-35.115c8.992-8.937 14.557-21.312 14.557-34.987s-5.565-26.050-14.554-34.984l-0.002-0.002-209.749-209.749 209.749-209.749c8.992-8.937 14.557-21.312 14.557-34.987s-5.565-26.050-14.554-34.984l-0.002-0.002-35.115-35.115c-8.937-8.992-21.312-14.557-34.987-14.557s-26.050 5.565-34.984 14.554l-0.002 0.002-279.723 279.765c-8.993 8.976-14.557 21.385-14.557 35.093s5.564 26.118 14.556 35.093l0.001 0.001z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["circle-left"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":20,"id":24,"name":"circle-left","prevSize":32,"code":59658},"setIdx":0,"setId":2,"iconIdx":10},{"icon":{"paths":["M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512c-282.77 0-512 229.23-512 512v0c0 282.77 229.23 512 512 512v0zM747.136 547.115l-279.723 279.723c-8.937 8.992-21.312 14.557-34.987 14.557s-26.050-5.565-34.984-14.554l-0.002-0.002-35.115-35.115c-8.992-8.937-14.557-21.312-14.557-34.987s5.565-26.050 14.554-34.984l0.002-0.002 209.749-209.749-209.792-209.792c-8.992-8.937-14.557-21.312-14.557-34.987s5.565-26.050 14.554-34.984l0.002-0.002 35.115-35.115c8.937-8.992 21.312-14.557 34.987-14.557s26.050 5.565 34.984 14.554l0.002 0.002 279.723 279.723c9.019 8.98 14.6 21.406 14.6 35.136 0 13.709-5.564 26.118-14.556 35.093l-0.001 0.001z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["circle-right"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":21,"id":23,"name":"circle-right","prevSize":32,"code":59659},"setIdx":0,"setId":2,"iconIdx":11},{"icon":{"paths":["M843.123 30.996l-331.098 331.098-331.098-331.098c-19.185-19.146-45.668-30.985-74.916-30.985s-55.731 11.839-74.918 30.987l0.002-0.002c-19.117 19.18-30.937 45.643-30.937 74.868s11.82 55.687 30.94 74.87l-0.003-0.003 331.050 331.098-331.050 331.244c-19.191 19.173-31.063 45.67-31.063 74.94 0 58.504 47.427 105.93 105.93 105.93 29.234 0 55.702-11.842 74.868-30.991l-0.001 0.001 331.098-331.098 331.196 331.147c19.046 18.46 45.049 29.839 73.71 29.839 58.521 0 105.961-47.44 105.961-105.961 0-28.661-11.379-54.663-29.865-73.737l0.027 0.028-331.196-331.196 331.098-331.098c18.46-19.046 29.839-45.049 29.839-73.71 0-58.521-47.44-105.961-105.961-105.961-28.661 0-54.663 11.379-73.737 29.865l0.028-0.027z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["close"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":22,"id":22,"name":"close","prevSize":32,"code":59660},"setIdx":0,"setId":2,"iconIdx":12},{"icon":{"paths":["M895.851 0h-767.872c-70.681 0-127.979 57.298-127.979 127.979v0 575.904c0 70.681 57.298 127.979 127.979 127.979v0h191.968v167.993c-0 0.025-0 0.056-0 0.086 0 13.288 10.772 24.060 24.060 24.060 5.399 0 10.382-1.778 14.397-4.781l-0.063 0.045 249.558-187.403h287.952c70.681 0 127.979-57.298 127.979-127.979v0-575.904c0-70.681-57.298-127.979-127.979-127.979l0 0zM604.699 575.904h-371.821v-127.979h371.821zM791.42 296.697h-558.84v-127.979h558.84z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["comment"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":23,"id":21,"name":"comment","prevSize":32,"code":59661},"setIdx":0,"setId":2,"iconIdx":13},{"icon":{"paths":["M646.741 485.035v-323.371l-161.707-161.664-161.664 161.664v107.819h-323.371v754.517h970.112v-538.965zM215.595 916.224h-107.819v-107.819h107.819zM215.595 700.629h-107.819v-107.776h107.819zM215.595 485.035h-107.819v-107.776h107.819zM538.965 916.181h-107.819v-107.776h107.776zM538.965 700.587h-107.819v-107.733h107.776zM538.965 484.992h-107.819v-107.733h107.776zM538.965 269.397h-107.819v-107.733h107.776zM862.336 916.139h-107.819v-107.733h107.776zM862.336 700.544h-107.819v-107.691h107.776z"],"attrs":[{}],"width":970,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["company"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":24,"id":20,"name":"company","prevSize":32,"code":59662},"setIdx":0,"setId":2,"iconIdx":14},{"icon":{"paths":["M587.199 55.924l-55.924-55.924h-279.618l-55.924 55.924h-195.733v111.847h782.932v-111.847z","M33.107 870.508c1.405 85.070 70.697 153.493 155.97 153.493 0.217 0 0.434-0 0.65-0.001l-0.034 0h403.396c0.182 0.001 0.398 0.001 0.614 0.001 85.338 0 154.671-68.528 155.971-153.557l0.001-0.122v-647.895h-716.569zM144.954 332.112h493.023v538.023c-0.295 24.483-20.211 44.217-44.736 44.217-0.001 0-0.002-0-0.003-0l-403.21 0c-0.101 0.001-0.22 0.001-0.34 0.001-24.499 0-44.399-19.692-44.735-44.112l-0-0.032z","M288.939 834.903c30.886 0 55.924-25.038 55.924-55.924v0-316.901c0-30.886-25.038-55.924-55.924-55.924s-55.924 25.038-55.924 55.924v0 316.901c0 30.886 25.038 55.924 55.924 55.924v0z","M493.993 834.903c30.886 0 55.924-25.038 55.924-55.924v0-316.901c0-30.886-25.038-55.924-55.924-55.924s-55.924 25.038-55.924 55.924v0 316.901c0 30.886 25.038 55.924 55.924 55.924v0z"],"attrs":[{},{},{},{}],"width":783,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["delet"],"colorPermutations":{"12552552551":[{},{},{},{}]}},"attrs":[{},{},{},{}],"properties":{"order":6,"id":18,"name":"delet","prevSize":32,"code":59663},"setIdx":0,"setId":2,"iconIdx":15},{"icon":{"paths":["M563.115 854.229l407.851-408.107c13.086-12.984 21.186-30.975 21.186-50.858 0-19.714-7.963-37.568-20.848-50.521l0.003 0.003-68.139-68.139c-12.998-13.055-30.985-21.133-50.859-21.133-19.725 0-37.592 7.958-50.564 20.838l0.004-0.004-289.536 289.621-289.195-289.195c-13.008-13.118-31.038-21.24-50.965-21.24-19.778 0-37.688 8.001-50.669 20.943l0.002-0.002-0.299 0.299-67.968 67.499c-13.075 12.989-21.168 30.978-21.168 50.859 0 19.71 7.956 37.563 20.831 50.521l-0.004-0.004 408.235 408.32c13.040 13.276 31.18 21.503 51.243 21.503 19.596 0 37.358-7.849 50.315-20.575l-0.011 0.010z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["down"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":7,"id":17,"name":"down","prevSize":32,"code":59664},"setIdx":0,"setId":2,"iconIdx":16},{"icon":{"paths":["M864 0c88.366 0 160 71.634 160 160 0 36.2-12.022 69.593-32.292 96.402l0.292-0.402-64 64-224-224 64-64c26.324-19.976 59.637-32 95.758-32 0.085 0 0.17 0 0.255 0l-0.013-0zM64 736l-64 288 288-64 592.171-592-224.171-224.128zM715.563 363.563l-448 448-55.168-55.125 448-448z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["edit"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":8,"id":16,"name":"edit","prevSize":32,"code":59665},"setIdx":0,"setId":2,"iconIdx":17},{"icon":{"paths":["M1123.235 921.346l-217.634-208.847c5.132 1.805 11.050 2.848 17.211 2.848 15.851 0 30.089-6.902 39.875-17.864l0.046-0.052c37.481-38.161 71.036-80.412 99.866-125.951l1.835-3.103c5.666-8.976 9.029-19.899 9.029-31.606 0-11.056-2.999-21.412-8.227-30.299l0.153 0.28c-7.952-13.359-194.813-327.802-528.499-327.802h-1.312c-35.996 0.005-71.127 3.696-105.040 10.716l3.34-0.578c-18.092 3.787-32.895 15.391-40.954 31.036l-0.156 0.333-212.306-203.798c-10.7-10.287-25.264-16.622-41.308-16.622-32.938 0-59.64 26.702-59.64 59.64 0 16.894 7.024 32.148 18.312 42.999l0.020 0.019 162.768 156.208c-5.287 1.648-9.884 3.809-14.111 6.501l0.235-0.14c-95.97 62.868-175.328 142.911-235.543 236.445l-1.89 3.135c-5.835 9.056-9.303 20.115-9.303 31.985 0 11.035 2.997 21.371 8.222 30.236l-0.152-0.279c7.952 13.359 194.813 327.802 529.175 327.802 107.604-0.9 207.542-32.95 291.456-87.555l-2.1 1.281c1.312-0.835 2.385-1.908 3.618-2.863l210.398 201.929c10.7 10.287 25.264 16.622 41.308 16.622 32.938 0 59.64-26.702 59.64-59.64 0-16.894-7.024-32.148-18.312-42.999l-0.020-0.019zM536.729 298.223c210.716 0 354.679 168.573 405.529 238.546-20.464 28.603-41.631 53.756-64.584 77.15l0.097-0.1c-10.193 10.583-16.471 24.997-16.471 40.878 0 6.315 0.993 12.399 2.831 18.103l-0.116-0.417-385.213-369.747c17.213-2.809 37.053-4.414 57.267-4.414 0.232 0 0.464 0 0.696 0.001l-0.036-0zM480.591 505.758c6.937-5.309 12.58-11.943 16.621-19.556l0.156-0.323 89.972 86.354c-14.804 12.070-33.904 19.382-54.711 19.382-7.266 0-14.323-0.892-21.069-2.571l0.598 0.126c-21.583-3.776-39.081-18.284-47.003-37.713l-0.149-0.414c-0.942-3.344-1.484-7.185-1.484-11.153 0-13.911 6.658-26.266 16.961-34.054l0.107-0.078zM536.729 775.315c-210.716 0-354.4-168.215-405.211-238.546 49.32-68.027 109.037-125.034 177.432-169.904l2.551-1.571c11.947-7.883 20.761-19.68 24.668-33.535l0.101-0.418 78.482 75.301c-2.135 1.142-3.89 2.209-5.584 3.359l0.217-0.139c-39.593 30.204-64.883 77.409-64.883 130.521 0 16.497 2.44 32.424 6.979 47.441l-0.301-1.159c21.213 61.143 72.589 106.713 135.851 119.32l1.154 0.192c13.578 3.209 29.168 5.048 45.187 5.048 51.658 0 98.848-19.131 134.872-50.694l-0.235 0.202c1.923-1.788 3.692-3.67 5.327-5.665l0.080-0.1 67.389 64.686c-58.645 34.055-128.851 54.577-203.762 55.657l-0.314 0.004z"],"attrs":[{}],"width":1142,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["eye-1"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":9,"id":15,"name":"eye-1","prevSize":32,"code":59666},"setIdx":0,"setId":2,"iconIdx":18},{"icon":{"paths":["M1343.806 478.457c-9.762-19.575-244.11-478.457-667.948-478.457s-658.136 458.883-667.948 478.457c-4.994 9.78-7.92 21.332-7.92 33.568s2.926 23.787 8.117 33.994l-0.197-0.427c10.013 19.575 244.11 478.407 667.948 478.407s658.186-458.832 667.948-478.407c4.994-9.78 7.92-21.332 7.92-33.568s-2.926-23.787-8.117-33.994l0.197 0.427zM675.858 873.96c-272.446 0-457.030-266.138-514.553-361.809 57.523-95.671 242.107-361.809 514.553-361.809s457.13 266.288 514.553 361.809c-57.523 95.521-242.157 361.659-514.553 361.659z","M675.858 211.619c-165.896 0-300.381 134.485-300.381 300.381s134.485 300.381 300.381 300.381c165.896 0 300.381-134.485 300.381-300.381v0c-0.199-165.816-134.565-300.182-300.362-300.381l-0.020-0zM675.858 662.191c-82.948 0-150.191-67.243-150.191-150.191s67.243-150.191 150.191-150.191c82.948 0 150.191 67.243 150.191 150.191v0c0 82.948-67.243 150.191-150.191 150.191v0z"],"attrs":[{},{}],"width":1352,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["eye"],"colorPermutations":{"12552552551":[{},{}]}},"attrs":[{},{}],"properties":{"order":25,"id":14,"name":"eye","prevSize":32,"code":59667},"setIdx":0,"setId":2,"iconIdx":19},{"icon":{"paths":["M470.315 379.861v136.192c52.181 11.819 98.603 31.189 147.2 44.587v-136.405c-52.011-11.605-98.773-30.976-147.2-44.416zM916.907 133.888c-67.939 35.792-147.729 58.874-232.376 63.923l-1.608 0.077c-107.008-0.213-195.584-69.76-330.368-69.76-0.457-0.002-0.998-0.003-1.539-0.003-48.265 0-94.476 8.794-137.118 24.867l2.678-0.885c4.705-11.905 7.433-25.695 7.433-40.122 0-61.856-50.144-112-112-112s-112 50.144-112 112c0 37.882 18.807 71.371 47.593 91.64l0.355 0.237v772.267c-0 0.038-0 0.083-0 0.128 0 26.439 21.433 47.872 47.872 47.872 0.045 0 0.090-0 0.135-0l-0.007 0h32c0.038 0 0.083 0 0.128 0 26.439 0 47.872-21.433 47.872-47.872 0-0.045-0-0.090-0-0.135l0 0.007v-188.928c64.268-28.012 139.144-44.31 217.827-44.31 3.851 0 7.693 0.039 11.525 0.117l-0.573-0.009c107.179 0 195.584 69.589 330.368 69.589 92.324-0.765 177.356-31.352 246.099-82.581l-1.107 0.789c16.771-11.705 27.605-30.909 27.605-52.644 0-0.047-0-0.094-0-0.141l0 0.007v-486.144c-0.078-35.287-28.702-63.863-64-63.863-9.72 0-18.935 2.167-27.186 6.044l0.391-0.165zM323.157 651.008c-54.473 5.68-104.346 17.389-151.58 34.589l4.38-1.394v-140.8c42.966-16.959 92.83-29.258 144.754-34.612l2.446-0.204zM911.957 382.208c-42.344 20.263-91.614 36.787-143.125 47.106l-4.075 0.681v141.867c55.26-7.286 105.051-25.69 148.773-52.924l-1.573 0.913v140.8c-41.573 28.653-91.64 48.031-145.739 54.055l-1.461 0.132v-142.976c-14.605 2.234-31.455 3.51-48.603 3.51-35.177 0-69.1-5.37-100.994-15.334l2.396 0.645v134.912c-39.685-15.094-88.92-29.775-139.395-41.183l-7.805-1.483v-136.875c-28.906-6.694-62.098-10.532-96.186-10.532-17.976 0-35.702 1.067-53.12 3.141l2.106-0.204v-140.032c-55.565 9.583-104.997 24.104-151.88 43.532l4.68-1.718v-140.8c42.614-19.83 92.097-35.166 143.91-43.55l3.29-0.439v142.976c14.417-2.165 31.054-3.401 47.98-3.401 35.4 0 69.537 5.408 101.63 15.442l-2.41-0.649v-134.741c39.858 15.224 89.098 29.908 139.605 41.235l7.595 1.431v136.704c26.239 6.45 56.363 10.149 87.35 10.149 21.13 0 41.859-1.72 62.054-5.028l-2.204 0.298v-143.872c55.763-10.757 105.071-26.328 151.746-46.79l-4.546 1.777z"],"attrs":[{}],"width":1008,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["flag"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":26,"id":13,"name":"flag","prevSize":32,"code":59668},"setIdx":0,"setId":2,"iconIdx":20},{"icon":{"paths":["M460.771 358.396h102.293v-102.251h-102.293zM511.917 921.749c-0.012 0-0.027 0-0.041 0-226.23 0-409.625-183.395-409.625-409.625s183.395-409.625 409.625-409.625c226.215 0 409.601 183.372 409.625 409.581l0 0.002c0 226.23-183.395 409.625-409.625 409.625v0zM511.876-0.207c-282.717 0.071-511.876 229.274-511.876 512 0 282.77 229.23 512 512 512 282.697 0 511.882-229.113 512-511.782l0-0.011c0-282.77-229.23-512-512-512v0zM460.854 768.021h102.21v-307.25h-102.21z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["information"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":27,"id":12,"name":"information","prevSize":32,"code":59669},"setIdx":0,"setId":2,"iconIdx":21},{"icon":{"paths":["M22.52 566.594l435.159 434.886c13.85 13.942 33.032 22.572 54.23 22.572s40.38-8.63 54.226-22.567l0.004-0.004 72.291-72.291c13.931-13.855 22.553-33.035 22.553-54.23s-8.622-40.375-22.549-54.227l-0.003-0.003-308.501-308.41 308.365-308.365c13.931-13.855 22.553-33.035 22.553-54.23s-8.622-40.375-22.549-54.227l-0.003-0.003-71.973-72.792c-13.85-13.942-33.032-22.572-54.23-22.572s-40.38 8.63-54.226 22.567l-0.004 0.004-435.023 434.932c-14.154 13.904-22.927 33.246-22.927 54.637 0 21.234 8.643 40.449 22.604 54.319l0.004 0.004z"],"attrs":[{}],"width":661,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["left"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":28,"id":11,"name":"left","prevSize":32,"code":59670},"setIdx":0,"setId":2,"iconIdx":22},{"icon":{"paths":["M558.549 0.469l-465.451-0.469c-51.358 0.145-92.954 41.74-93.099 93.085l-0 0.014v837.803c0.145 51.358 41.74 92.954 93.085 93.099l0.014 0h465.451c51.358-0.145 92.954-41.74 93.099-93.085l0-0.014v-837.803c-0.194-51.199-41.744-92.629-92.97-92.629-0.045 0-0.090 0-0.136 0l0.007-0zM558.549 837.803h-465.451v-651.605h465.451z"],"attrs":[{}],"width":652,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["phone"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":29,"id":10,"name":"phone","prevSize":32,"code":59671},"setIdx":0,"setId":2,"iconIdx":23},{"icon":{"paths":["M384 0c-0.789-0.017-1.719-0.027-2.651-0.027-34.738 0-66.285 13.665-89.558 35.914l0.049-0.046c-22.202 23.224-35.867 54.77-35.867 89.509 0 0.932 0.010 1.862 0.029 2.789l-0.002-0.138c5.836 34.821 18.412 65.879 36.393 93.049l-0.553-0.889c2.672 8.237 4.213 17.715 4.213 27.553 0 2.918-0.136 5.803-0.401 8.652l0.027-0.365h-295.68v768h295.68c0.053-1.37 0.084-2.978 0.084-4.594 0-11.101-1.431-21.868-4.119-32.126l0.196 0.88c-17.428-26.281-30.005-57.34-35.644-90.756l-0.196-1.404c-0.017-0.789-0.027-1.719-0.027-2.651 0-34.738 13.665-66.285 35.914-89.558l-0.046 0.049c23.224-22.202 54.77-35.867 89.509-35.867 0.932 0 1.862 0.010 2.789 0.029l-0.138-0.002c0.789-0.017 1.719-0.027 2.651-0.027 34.738 0 66.285 13.665 89.558 35.914l-0.049-0.046c22.202 23.224 35.867 54.77 35.867 89.509 0 0.932-0.010 1.862-0.029 2.789l0.002-0.138c-5.836 34.821-18.412 65.879-36.393 93.049l0.553-0.889c-2.672 8.237-4.213 17.715-4.213 27.553 0 2.918 0.136 5.803 0.401 8.652l-0.027-0.365h295.68v-295.68c1.37-0.053 2.978-0.084 4.594-0.084 11.101 0 21.868 1.431 32.126 4.119l-0.88-0.196c26.281 17.428 57.34 30.005 90.756 35.644l1.404 0.196c70.692 0 128-57.308 128-128s-57.308-128-128-128v0c-34.821 5.836-65.879 18.412-93.049 36.393l0.889-0.553c-8.237 2.672-17.715 4.213-27.553 4.213-2.918 0-5.803-0.136-8.652-0.401l0.365 0.027v-295.68h-295.68c-0.053-1.37-0.084-2.978-0.084-4.594 0-11.101 1.431-21.868 4.119-32.126l-0.196 0.88c17.428-26.281 30.005-57.34 35.644-90.756l0.196-1.404c0.017-0.789 0.027-1.719 0.027-2.651 0-34.738-13.665-66.285-35.914-89.558l0.046 0.049c-23.224-22.202-54.77-35.867-89.509-35.867-0.932 0-1.862 0.010-2.789 0.029l0.138-0.002z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["puzzle"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":30,"id":9,"name":"puzzle","prevSize":32,"code":59672},"setIdx":0,"setId":2,"iconIdx":24},{"icon":{"paths":["M638.368 566.619l-435.042 434.86c-13.855 13.932-33.037 22.554-54.232 22.554s-40.377-8.622-54.229-22.55l-0.003-0.003-72.295-72.295c-13.932-13.855-22.554-33.037-22.554-54.232s8.622-40.377 22.55-54.229l0.003-0.003 308.378-308.378-308.378-308.378c-13.932-13.855-22.554-33.037-22.554-54.232s8.622-40.377 22.55-54.229l0.003-0.003 71.976-72.795c13.855-13.932 33.037-22.554 54.232-22.554s40.377 8.622 54.229 22.55l0.003 0.003 435.042 434.723c14.1 13.896 22.836 33.203 22.836 54.549 0 21.189-8.607 40.368-22.515 54.232l-0.002 0.002z"],"attrs":[{}],"width":661,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["right"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":31,"id":8,"name":"right","prevSize":32,"code":59673},"setIdx":0,"setId":2,"iconIdx":25},{"icon":{"paths":["M227.579 579.116v227.579l398.211 217.305 398.211-217.305v-227.579l-398.211 217.358zM625.789-0l-625.789 341.316 625.789 341.316 512-279.311v393.1h113.789v-455.105z"],"attrs":[{}],"width":1252,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["school"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":32,"id":7,"name":"school","prevSize":32,"code":59674},"setIdx":0,"setId":2,"iconIdx":26},{"icon":{"paths":["M1009.922 885.314l-199.391-199.391c-8.672-8.647-20.639-13.993-33.854-13.993-0.096 0-0.192 0-0.288 0.001l0.015-0h-32.592c54.858-69.783 87.982-158.908 87.982-255.769 0-229.618-186.142-415.76-415.76-415.76s-415.76 186.142-415.76 415.76c0 229.618 186.142 415.76 415.76 415.76 96.861 0 185.986-33.123 256.666-88.661l-0.897 0.679v32.464c-0 0.081-0.001 0.177-0.001 0.273 0 13.216 5.346 25.182 13.994 33.855l-0.001-0.001 199.391 199.391c8.657 8.711 20.645 14.103 33.893 14.103s25.236-5.392 33.891-14.1l0.002-0.002 56.609-56.609c8.774-8.732 14.204-20.817 14.204-34.171 0-13.184-5.292-25.131-13.869-33.834l0.006 0.006zM415.973 672.016c-141.361 0-255.957-114.596-255.957-255.957s114.596-255.957 255.957-255.957c141.361 0 255.957 114.596 255.957 255.957v0c-0.049 141.325-114.626 255.872-255.957 255.872l-0-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["search"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":33,"id":6,"name":"search","prevSize":32,"code":59675},"setIdx":0,"setId":2,"iconIdx":27},{"icon":{"paths":["M264.124 1024l-3.988-0.315c-45.874-4.728-81.349-43.172-81.349-89.9 0-9.949 1.608-19.523 4.579-28.476l-0.183 0.638 89.58-260.661-235.092-165.554c-19.618-13.599-33.145-34.797-36.332-59.244l-0.046-0.43c-0.823-4.244-1.294-9.125-1.294-14.115 0-0.018 0-0.036 0-0.054l-0 0.003c0.197-48.36 39.308-87.52 87.63-87.796l0.026-0h289.553l86.747-258.842c12.356-34.779 44.982-59.235 83.32-59.235 37.789 0 70.029 23.761 82.593 57.157l0.201 0.608 87.272 260.311h287.525c0.011-0 0.025-0 0.038-0 47.863 0 86.99 37.407 89.741 84.58l0.011 0.243v3.848l-0.315 3.883c-2.124 22.565-12.304 42.418-27.594 56.944l-0.039 0.036-11.963 10.074-233.098 167.233 89.755 261.011c3.161 8.633 4.99 18.602 4.99 28.997 0 27.516-12.812 52.041-32.795 67.933l-0.177 0.136c-13.364 11.803-30.658 19.438-49.692 20.728l-0.258 0.014-3.183 0.21-3.148-0.175c-18.973-1.079-36.226-7.647-50.416-18.125l0.256 0.181-229.565-163.77-230.369 164.12c-13.682 9.851-30.475 16.214-48.672 17.473l-0.298 0.017zM812.414 940.051v0.28zM282.243 939.876v0zM838.403 920.498v0zM139.39 423.032l258.003 181.889-97.94 285.006 247.929-176.642 247.649 176.502-97.94-284.586 254.015-182.169h-309.246l-94.442-282.243-94.722 282.243zM1004.972 423.032v0zM97.346 393.405l0.56 0.42z"],"attrs":[{}],"width":1095,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["star-1"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":34,"id":5,"name":"star-1","prevSize":32,"code":59676},"setIdx":0,"setId":2,"iconIdx":28},{"icon":{"paths":["M1060.923 354.485h-362.082l-110.034-328.386c-5.769-15.366-20.337-26.102-37.413-26.102s-31.644 10.737-37.322 25.828l-0.091 0.275-110.034 328.386h-364.574c-21.717 0.069-39.304 17.656-39.374 39.367l-0 0.007c-0 0.051-0.001 0.111-0.001 0.172 0 2.3 0.268 4.537 0.775 6.682l-0.039-0.196c1.082 11.588 7.245 21.545 16.211 27.732l0.127 0.083 297.753 209.856-114.363 332.062c-1.356 3.823-2.14 8.232-2.14 12.824 0 12.816 6.104 24.206 15.563 31.422l0.096 0.070c5.913 5.322 13.556 8.829 21.991 9.588l0.147 0.011c9.263-0.701 17.658-3.918 24.649-8.961l-0.142 0.098 290.483-207.038 290.483 207.038c6.807 5.047 15.229 8.284 24.374 8.857l0.132 0.007c8.688-0.56 16.438-4.176 22.272-9.773l-0.012 0.012c9.602-7.187 15.75-18.533 15.75-31.315 0-4.66-0.817-9.13-2.316-13.273l0.086 0.272-114.159-331.898 295.139-211.694 7.148-6.167c7.108-6.72 11.823-15.905 12.772-26.182l0.012-0.163c-1.204-22.063-19.388-39.497-41.643-39.497-0.078 0-0.156 0-0.234 0.001l0.012-0z"],"attrs":[{}],"width":1103,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["star"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":35,"id":4,"name":"star","prevSize":32,"code":59677},"setIdx":0,"setId":2,"iconIdx":29},{"icon":{"paths":["M662.613 361.941c-7.552-20.181 201.344-206.72 79.531-357.888-28.501-35.328-125.227 169.259-262.528 261.845-75.776 51.2-252.075 159.872-252.075 219.989v389.461c0 72.32 279.552 148.864 491.989 148.864 77.867 0 190.677-487.851 190.677-565.333s-240.128-76.459-247.467-96.725zM170.624 367.36c-0.681-0.010-1.486-0.016-2.292-0.016-93.126 0-168.619 75.493-168.619 168.619 0 3.187 0.088 6.354 0.263 9.498l-0.019-0.437v275.797c-0.161 2.71-0.252 5.878-0.252 9.068 0 90.015 72.972 162.987 162.987 162.987 2.79 0 5.564-0.070 8.32-0.209l-0.387 0.016c37.376 0-56.875-32.555-56.875-128.555v-362.667c0-100.565 94.251-134.187 56.875-134.187z"],"attrs":[{}],"width":910,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["thumbs-up"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":10,"id":3,"name":"thumbs-up","prevSize":32,"code":59678},"setIdx":0,"setId":2,"iconIdx":30},{"icon":{"paths":["M511.488 0c-282.55 0.292-511.488 229.41-511.488 512 0 282.77 229.23 512 512 512s512-229.23 512-512l-0 0c0-0.076 0-0.166 0-0.257 0-282.628-229.116-511.744-511.744-511.744-0.27 0-0.54 0-0.81 0.001l0.042-0zM512 921.6c-226.216 0-409.6-183.384-409.6-409.6s183.384-409.6 409.6-409.6c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM537.6 256h-76.8v307.2l268.8 161.28 38.4-63.147-230.4-136.533z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["time"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":11,"id":2,"name":"time","prevSize":32,"code":59679},"setIdx":0,"setId":2,"iconIdx":31},{"icon":{"paths":["M563.115 223.232l407.851 408.107c13.137 12.992 21.272 31.018 21.272 50.943 0 19.756-7.997 37.645-20.932 50.605l0.001-0.001-68.139 68.139c-12.998 13.055-30.985 21.133-50.859 21.133-19.725 0-37.592-7.958-50.564-20.838l0.004 0.004-289.536-289.792-289.195 289.195c-12.998 13.055-30.985 21.133-50.859 21.133-19.725 0-37.592-7.958-50.564-20.838l0.004 0.004-0.213-0.299-68.267-67.499c-13.075-12.989-21.168-30.978-21.168-50.859 0-19.71 7.956-37.563 20.831-50.521l-0.004 0.004 408.235-408.32c13.040-13.276 31.18-21.503 51.243-21.503 19.596 0 37.358 7.849 50.315 20.575l-0.011-0.010z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["top"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":12,"id":1,"name":"top","prevSize":32,"code":59680},"setIdx":0,"setId":2,"iconIdx":32},{"icon":{"paths":["M1104.017 128.018h-207.987v-80.017c0-26.51-21.491-48.001-48.001-48.001v0h-544.040c-26.51 0-48.001 21.491-48.001 48.001v0 80.017h-207.987c-26.51 0-48.001 21.491-48.001 48.001v0 111.986c7.076 85.462 54.363 158.548 122.682 200.96l1.112 0.643c61.778 44.226 136.678 74.099 217.854 83.23l2.133 0.195c34.389 59.486 80.159 108.69 134.709 145.998l1.518 0.98v144.002h-96.001c-3.854-0.457-8.318-0.718-12.843-0.718-62.746 0-113.776 50.163-115.172 112.574l-0.002 0.13v24c0 13.255 10.745 24 24 24v0h592.041c13.255 0 24-10.745 24-24v0-24c-1.399-62.54-52.429-112.704-115.175-112.704-4.525 0-8.989 0.261-13.378 0.768l0.535-0.050h-96.001v-144.002c56.068-38.356 101.826-87.64 135.041-145.086l1.137-2.132c83.314-9.166 158.252-38.973 221.451-84.147l-1.416 0.962c69.366-43.121 116.621-116.171 123.729-200.646l0.065-0.957v-111.986c0-26.51-21.491-48.001-48.001-48.001v0zM198.627 385.59c-35.829-20.914-61.682-55.601-70.437-96.621l-0.172-0.964v-32.016h128.402c1.638 61.997 10.873 121.145 26.808 177.479l-1.223-5.061c-31.815-11.002-59.407-25.499-84.329-43.47l0.951 0.653zM1024 288.005c-9.695 41.665-35.309 76.091-69.932 97.202l-0.677 0.383c-24.040 17.319-51.713 31.817-81.346 42.129l-2.271 0.688c14.711-51.273 23.946-110.421 25.564-171.467l0.020-0.952h128.594z"],"attrs":[{}],"width":1152,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["trophy"],"colorPermutations":{"12552552551":[{}]}},"attrs":[{}],"properties":{"order":13,"id":0,"name":"trophy","prevSize":32,"code":59681},"setIdx":0,"setId":2,"iconIdx":33}],"height":1024,"metadata":{"name":"icomoon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icomoon"},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon"},"historySize":50,"showCodes":true,"gridSize":16}}
{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M482.987 800.085h115.371c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-115.371c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M244.395 117.077c-5.317-5.071-12.534-8.192-20.48-8.192s-15.163 3.121-20.492 8.203l0.012-0.011-143.701 173.056c-5.035 5.211-8.137 12.317-8.137 20.148 0 15.764 12.572 28.592 28.237 29.003l0.038 0.001h86.357v548.181c-0 0.001-0 0.001-0 0.002 0 15.835 12.837 28.672 28.672 28.672 0.12 0 0.24-0.001 0.36-0.002l-0.018 0h57.344c0.101 0.001 0.221 0.002 0.341 0.002 15.835 0 28.672-12.837 28.672-28.672 0-0.001-0-0.001-0-0.002l0 0v-546.133h86.357c0.001 0 0.001 0 0.002 0 15.835 0 28.672-12.837 28.672-28.672 0-0.12-0.001-0.24-0.002-0.36l0 0.018c-0.093-7.822-3.197-14.902-8.203-20.15l0.011 0.012z","M482.987 339.285h345.771c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-345.771c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M482.987 569.685h230.741c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-230.741c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M482.987 108.885h461.141c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-461.141c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z"],"attrs":[{},{},{},{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["sort-decrease"]},"attrs":[{},{},{},{},{}],"properties":{"order":2,"id":38,"name":"sort-decrease","prevSize":32,"code":59682},"setIdx":0,"setId":2,"iconIdx":0},{"icon":{"paths":["M482.987 108.885h115.371c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-115.371c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M482.987 339.285h230.741c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-230.741c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M482.987 800.085h461.141c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-461.141c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M482.987 569.685h345.771c15.835 0 28.672 12.837 28.672 28.672v58.027c0 15.835-12.837 28.672-28.672 28.672h-345.771c-15.835 0-28.672-12.837-28.672-28.672v-58.027c0-15.835 12.837-28.672 28.672-28.672z","M367.957 684.715h-86.357v-548.181c0-0.001 0-0.001 0-0.002 0-15.835-12.837-28.672-28.672-28.672-0.12 0-0.24 0.001-0.36 0.002l0.018-0h-57.344c-0.101-0.001-0.221-0.002-0.341-0.002-15.835 0-28.672 12.837-28.672 28.672 0 0.001 0 0.001 0 0.002l-0-0v546.133h-86.357c-15.876 0.193-28.672 13.108-28.672 29.011 0 0.001 0 0.001 0 0.002l-0-0c-0 0.005-0 0.011-0 0.017 0 7.894 3.268 15.026 8.526 20.114l0.008 0.007 145.067 175.104c5.264 5.050 12.423 8.16 20.309 8.16s15.045-3.11 20.319-8.169l-0.010 0.010 144.043-173.056c5.21-5.192 8.433-12.374 8.433-20.309s-3.223-15.117-8.432-20.309l-0.001-0.001c-5.098-5.284-12.242-8.566-20.153-8.566-0.475 0-0.948 0.012-1.417 0.035l0.066-0.003z"],"attrs":[{},{},{},{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["sort-add"]},"attrs":[{},{},{},{},{}],"properties":{"order":3,"id":37,"name":"sort-add","prevSize":32,"code":59683},"setIdx":0,"setId":2,"iconIdx":1},{"icon":{"paths":["M769.024 515.072c-28.277 0-51.2 22.923-51.2 51.2v0 266.581c-0.191 20.659-16.888 37.355-37.528 37.547l-0.018 0h-489.131c-20.736 0-37.547-16.81-37.547-37.547v0-489.131c0.571-20.291 17.156-36.523 37.533-36.523 0.005 0 0.010 0 0.015 0l266.581-0c28.277 0 51.2-22.923 51.2-51.2s-22.923-51.2-51.2-51.2v0h-266.581c-77.212 0.194-139.753 62.734-139.947 139.928l-0 0.019v488.107c0.194 77.212 62.734 139.753 139.928 139.947l0.019 0h489.131c76.772-0.771 138.733-63.091 138.923-139.928l0-0.018v-266.581c0-0.003 0-0.007 0-0.010 0-27.917-22.343-50.615-50.122-51.189l-0.054-0.001z","M969.045 83.627v0c-5.53-12.604-15.385-22.459-27.646-27.855l-0.344-0.135c-5.708-2.654-12.373-4.272-19.397-4.436l-0.059-0.001h-266.581c-28.277 0-51.2 22.923-51.2 51.2s22.923 51.2 51.2 51.2v0h143.019l-401.408 401.408c-9.335 9.225-15.116 22.028-15.116 36.181s5.781 26.956 15.111 36.176l0.005 0.005c9.263 9.252 22.054 14.974 36.181 14.974s26.918-5.722 36.182-14.974l-0.001 0.001 401.408-401.408v143.019c0 28.277 22.923 51.2 51.2 51.2s51.2-22.923 51.2-51.2v0-266.581c-0.029-6.778-1.406-13.226-3.877-19.101l0.122 0.328z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["expand"]},"attrs":[{},{}],"properties":{"order":4,"id":36,"name":"expand","prevSize":32,"code":59684},"setIdx":0,"setId":2,"iconIdx":2},{"icon":{"paths":["M793.941 375.467c3.545-16.844 5.574-36.198 5.574-56.026 0-157.031-127.299-284.331-284.331-284.331-142.597 0-260.676 104.971-281.179 241.856l-0.192 1.562c-2.833 14.767-4.453 31.752-4.453 49.114 0 53.505 15.388 103.416 41.981 145.548l-0.664-1.126c41.637 71.349 112.064 122.023 194.943 136.296l1.665 0.237v74.069h-149.845c-0.607-0.027-1.319-0.043-2.035-0.043-26.769 0-48.469 21.7-48.469 48.469 0 8.664 2.273 16.798 6.256 23.836l-0.126-0.241c8.178 14.971 23.811 24.956 41.777 24.956 0.673 0 1.343-0.014 2.009-0.042l-0.095 0.003h153.941v57.344c0 32.085 0 64.512 0 96.597-0 0.060-0 0.132-0 0.203 0 13.24 5.346 25.231 13.997 33.932l-0.002-0.002c8.546 9.046 20.625 14.677 34.019 14.677 0.040 0 0.080-0 0.121-0l-0.006 0c0.001 0 0.003 0 0.005 0 26.529 0 48.080-21.313 48.464-47.75l0-0.036c0-22.869 0-45.739 0-68.267v-87.381h153.259c0.924 0.164 1.987 0.258 3.072 0.258s2.148-0.094 3.182-0.274l-0.11 0.016c25.517-1.545 45.626-22.617 45.626-48.386 0-14.226-6.129-27.021-15.89-35.888l-0.040-0.036c-8.301-7.658-19.435-12.353-31.666-12.353-0.868 0-1.73 0.024-2.587 0.070l0.119-0.005c-36.523 0-72.704 0-109.227 0h-44.373v-75.776c115.769-23.213 205.016-114.415 225.036-229.387l0.244-1.695zM704.171 324.267c0.001 0.273 0.002 0.597 0.002 0.92 0 51.896-21.29 98.82-55.611 132.513l-0.028 0.028c-34.195 34.070-81.368 55.135-133.461 55.135s-99.267-21.064-133.467-55.14l0.006 0.006c-34.070-34.195-55.135-81.368-55.135-133.461s21.064-99.267 55.14-133.467l-0.006 0.006c33.592-33.54 79.971-54.282 131.195-54.282 0.677 0 1.353 0.004 2.028 0.011l-0.103-0.001c0.001-0 0.002-0 0.003-0 103.888 0 188.173 83.927 188.754 187.678l0 0.055z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["sex-female"]},"attrs":[{}],"properties":{"order":5,"id":35,"name":"sex-female","prevSize":32,"code":59685},"setIdx":0,"setId":2,"iconIdx":3},{"icon":{"paths":["M565.248 403.115v-189.781l137.899 138.24c2.383 2.598 5.074 4.855 8.030 6.731l0.162 0.096c7.571 5.13 16.909 8.19 26.961 8.19 3.507 0 6.926-0.372 10.221-1.080l-0.318 0.057c13.076-2.557 24.025-10.155 30.95-20.643l0.111-0.179c5.394-7.775 8.618-17.412 8.618-27.801 0-14.575-6.344-27.668-16.42-36.669l-0.048-0.042-219.819-219.819c-1.695-2.13-3.484-4.027-5.418-5.765l-0.043-0.038c-8.628-7.35-19.905-11.822-32.226-11.822-1.633 0-3.248 0.079-4.841 0.232l0.203-0.016c-13.699 0.917-25.744 7.317-34.079 17.002l-0.054 0.064-216.405 216.405c-3.559 3.176-6.796 6.522-9.776 10.089l-0.122 0.151c-5.953 8.072-9.528 18.216-9.528 29.195 0 14.889 6.574 28.242 16.978 37.315l0.059 0.050c8.443 7.322 19.539 11.784 31.678 11.784 1.465 0 2.915-0.065 4.347-0.192l-0.185 0.013c13.664-0.478 25.785-6.705 34.082-16.323l0.051-0.061 136.533-136.533v190.805c-137.566 25.119-240.475 144.041-240.475 286.998 0 144.662 105.377 264.711 243.558 287.575l1.695 0.232c14.016 2.329 30.205 3.692 46.701 3.754l0.062 0c160.99 0 291.499-130.508 291.499-291.499 0-142.407-102.118-260.963-237.121-286.436l-1.813-0.284zM671.744 803.157l-13.312 16.725-7.509 9.216c-34.834 35.13-83.115 56.879-136.476 56.879-47.528 0-91.026-17.254-124.572-45.84l0.27 0.224c-43.547-33.976-71.285-86.461-71.285-145.424 0-21.257 3.605-41.672 10.237-60.666l-0.392 1.29c20.237-73.614 82.804-128.246 159.267-136.462l0.818-0.071h51.883c94.474 13.866 166.186 94.355 166.186 191.594 0 41.636-13.148 80.202-35.519 111.771l0.405-0.602z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["sex-male"]},"attrs":[{}],"properties":{"order":6,"id":34,"name":"sex-male","prevSize":32,"code":59686},"setIdx":0,"setId":2,"iconIdx":4},{"icon":{"paths":["M947.023 423.724h-834.737c-42.513 0-76.977 34.464-76.977 76.977s34.464 76.977 76.977 76.977h834.737c42.513 0 76.977-34.464 76.977-76.977s-34.464-76.977-76.977-76.977h0z"],"width":1059,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["decrease"],"grid":0},"attrs":[{}],"properties":{"order":2,"id":0,"name":"decrease","prevSize":32,"code":59648},"setIdx":0,"setId":2,"iconIdx":5},{"icon":{"paths":["M944.305 432.195h-352.501v-352.501c-0.083-44.019-35.777-79.674-79.803-79.695l-0.002-0c-43.994 0.083-79.632 35.735-79.695 79.725l-0 0.006v352.464h-352.537c-44.055 0-79.768 35.713-79.768 79.768s35.713 79.768 79.768 79.768v0h352.501v352.574c0 44.055 35.713 79.768 79.768 79.768s79.768-35.713 79.768-79.768l-0 0v-352.574h352.501c44.055 0 79.768-35.713 79.768-79.768s-35.713-79.768-79.768-79.768l0 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["add"],"grid":0},"attrs":[{}],"properties":{"order":3,"id":1,"name":"add","prevSize":32,"code":59649},"setIdx":0,"setId":2,"iconIdx":6},{"icon":{"paths":["M358.4 0c-0.076-0-0.166-0-0.256-0-197.797 0-358.144 160.347-358.144 358.144 0 0.090 0 0.18 0 0.27l-0-0.014c0 268.8 358.4 665.6 358.4 665.6s358.4-396.8 358.4-665.6c0-0.076 0-0.166 0-0.256 0-197.797-160.347-358.144-358.144-358.144-0.090 0-0.18 0-0.27 0l0.014-0zM358.4 486.4c-70.692 0-128-57.308-128-128s57.308-128 128-128c70.692 0 128 57.308 128 128v0c0 70.692-57.308 128-128 128h0z"],"width":717,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["address"],"grid":0},"attrs":[{}],"properties":{"order":4,"id":2,"name":"address","prevSize":32,"code":59650},"setIdx":0,"setId":2,"iconIdx":7},{"icon":{"paths":["M447.993 511.981c141.38 0 255.991-114.611 255.991-255.991s-114.611-255.991-255.991-255.991c-141.38 0-255.991 114.611-255.991 255.991v-0c0 141.38 114.611 255.991 255.991 255.991v-0zM639.585 577.164l-95.609 382.81-63.988-272.006 63.988-111.998h-191.965l63.988 111.998-63.988 272.006-95.609-382.773c-142.815 6.542-256.155 123.796-256.401 267.577l-0 0.024v179.197h895.986v-179.197c-0.246-143.806-113.586-261.059-255.814-267.58l-0.587-0.021z"],"width":896,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["agent"],"grid":0},"attrs":[{}],"properties":{"order":5,"id":3,"name":"agent","prevSize":32,"code":59651},"setIdx":0,"setId":2,"iconIdx":8},{"icon":{"paths":["M889.022 7.054l-831.526 383.772c-34.358 15.411-57.857 49.317-57.857 88.711 0 53.291 43.002 96.539 96.2 96.936l0.038 0h351.649v351.801c0.219 53.372 43.536 96.553 96.938 96.553 39.559 0 73.583-23.696 88.654-57.668l0.245-0.618 383.621-831.564c3.826-10.169 6.041-21.922 6.041-34.192 0-55.109-44.674-99.783-99.783-99.783-12.281 0-24.044 2.219-34.909 6.277l0.689-0.226z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["arrow"],"grid":0},"attrs":[{}],"properties":{"order":14,"id":4,"name":"arrow","prevSize":32,"code":59652},"setIdx":0,"setId":2,"iconIdx":9},{"icon":{"paths":["M448.963 503.181c138.95 0 251.59-112.641 251.59-251.59s-112.641-251.59-251.59-251.59v-0c-138.95 0-251.59 112.641-251.59 251.59s112.641 251.59 251.59 251.59v0zM448.963 119.488v0c72.958-0 132.103 59.144 132.103 132.103s-59.144 132.103-132.103 132.103c-72.958 0-132.103-59.144-132.103-132.103 0-0.013 0-0.026 0-0.039l-0 0.002c-0-72.938 59.128-132.065 132.065-132.065 0.013 0 0.026 0 0.039 0l-0.002-0z","M448.963 566.069c-167.938 0-448.963 50.013-448.963 217.355v240.576h893.721v-240.576c0-167.305-276.82-217.355-444.758-217.355zM774.233 904.475h-654.931v-121.051c0-40.263 142.671-97.83 329.475-97.83v0c186.804 0 325.27 57.567 325.27 97.83z"],"width":894,"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{},{}]},"tags":["avatar"],"grid":0},"attrs":[{},{}],"properties":{"order":15,"id":5,"name":"avatar","prevSize":32,"code":59653},"setIdx":0,"setId":2,"iconIdx":10},{"icon":{"paths":["M908.486 692.817c-1.1-0.834-112.139-84.872-112.139-362.886 0-182.216-147.715-329.931-329.931-329.931s-329.931 147.715-329.931 329.931v0c0 278.014-111.039 362.052-111.191 362.242-15.359 10.347-25.326 27.674-25.326 47.328 0 31.414 25.465 56.881 56.878 56.885l819.14 0c25.031-0.194 46.179-16.601 53.474-39.229l0.112-0.401c1.696-5.184 2.675-11.15 2.675-17.344 0-19.089-9.291-36.008-23.598-46.482l-0.162-0.113zM174.901 682.616c47.923-94.131 76-205.282 76-322.99 0-10.443-0.221-20.835-0.659-31.17l0.050 1.475c0-119.383 96.779-216.162 216.162-216.162s216.162 96.779 216.162 216.162l0 0c-0.387 8.856-0.608 19.244-0.608 29.684 0 117.708 28.062 228.862 77.862 327.136l-1.9-4.135z","M573.853 872.459c-8.181-4.825-18.026-7.675-28.536-7.675-20.904 0-39.175 11.276-49.061 28.077l-0.145 0.266c-6.013 10.266-16.989 17.051-29.55 17.051-6.311 0-12.222-1.713-17.293-4.699l0.16 0.087c-5.181-3.025-9.376-7.22-12.314-12.241l-0.086-0.16c-10.022-17.142-28.338-28.474-49.3-28.474-31.434 0-56.916 25.482-56.916 56.916 0 10.472 2.828 20.283 7.762 28.711l-0.145-0.269c26.074 44.41 73.596 73.753 127.972 73.753s101.897-29.344 127.594-73.059l0.377-0.694c4.808-8.17 7.648-17.998 7.648-28.489 0-20.831-11.197-39.047-27.901-48.958l-0.263-0.144z"],"width":932,"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{},{}]},"tags":["bell"],"grid":0},"attrs":[{},{}],"properties":{"order":16,"id":6,"name":"bell","prevSize":32,"code":59654},"setIdx":0,"setId":2,"iconIdx":11},{"icon":{"paths":["M307.2 460.8h-102.4v102.4h102.4zM512 460.8h-102.4v102.4h102.4zM716.8 460.8h-102.4v102.4h102.4zM819.2 102.4h-51.2v-102.4h-102.4v102.4h-409.6v-102.4h-102.4v102.4h-51.2c-56.277 0.024-101.889 45.651-101.889 101.931 0 0.165 0 0.33 0.001 0.495l-0-0.025-0.512 716.8c0 56.554 45.846 102.4 102.4 102.4v0h716.8c56.486-0.169 102.231-45.914 102.4-102.384l0-0.016v-716.8c-0.169-56.486-45.914-102.231-102.384-102.4l-0.016-0zM819.2 921.6h-716.8v-563.2h716.8z"],"width":922,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["calender"],"grid":0},"attrs":[{}],"properties":{"order":17,"id":7,"name":"calender","prevSize":32,"code":59655},"setIdx":0,"setId":2,"iconIdx":12},{"icon":{"paths":["M910.222 0h-796.444c-62.838 0-113.778 50.94-113.778 113.778v0 796.444c0 62.838 50.94 113.778 113.778 113.778v0h796.444c62.838 0 113.778-50.94 113.778-113.778v0-796.444c0-62.838-50.94-113.778-113.778-113.778v0zM398.222 796.444l-284.444-284.444 80.213-80.213 204.231 203.662 431.787-431.787 80.213 80.782z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["checkbox-1"],"grid":0},"attrs":[{}],"properties":{"order":18,"id":8,"name":"checkbox-1","prevSize":32,"code":59656},"setIdx":0,"setId":2,"iconIdx":13},{"icon":{"paths":["M910.222 113.778v796.444h-796.444v-796.444h796.444zM910.222 0h-796.444c-62.76 0.193-113.584 51.018-113.778 113.759l-0 0.019v796.444c0.193 62.76 51.018 113.584 113.759 113.778l0.019 0h796.444c62.76-0.193 113.584-51.018 113.778-113.759l0-0.019v-796.444c-0.193-62.76-51.018-113.584-113.759-113.778l-0.019-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["checkbox"],"grid":0},"attrs":[{}],"properties":{"order":19,"id":9,"name":"checkbox","prevSize":32,"code":59657},"setIdx":0,"setId":2,"iconIdx":14},{"icon":{"paths":["M512 1024c-282.77 0-512-229.23-512-512s229.23-512 512-512c282.77 0 512 229.23 512 512v0c0 282.77-229.23 512-512 512h-0zM276.821 547.072l279.723 279.723c8.937 8.992 21.312 14.557 34.987 14.557s26.050-5.565 34.984-14.554l0.002-0.002 35.115-35.115c8.992-8.937 14.557-21.312 14.557-34.987s-5.565-26.050-14.554-34.984l-0.002-0.002-209.749-209.749 209.749-209.749c8.992-8.937 14.557-21.312 14.557-34.987s-5.565-26.050-14.554-34.984l-0.002-0.002-35.115-35.115c-8.937-8.992-21.312-14.557-34.987-14.557s-26.050 5.565-34.984 14.554l-0.002 0.002-279.723 279.765c-8.993 8.976-14.557 21.385-14.557 35.093s5.564 26.118 14.556 35.093l0.001 0.001z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["circle-left"],"grid":0},"attrs":[{}],"properties":{"order":20,"id":10,"name":"circle-left","prevSize":32,"code":59658},"setIdx":0,"setId":2,"iconIdx":15},{"icon":{"paths":["M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512c-282.77 0-512 229.23-512 512v0c0 282.77 229.23 512 512 512v0zM747.136 547.115l-279.723 279.723c-8.937 8.992-21.312 14.557-34.987 14.557s-26.050-5.565-34.984-14.554l-0.002-0.002-35.115-35.115c-8.992-8.937-14.557-21.312-14.557-34.987s5.565-26.050 14.554-34.984l0.002-0.002 209.749-209.749-209.792-209.792c-8.992-8.937-14.557-21.312-14.557-34.987s5.565-26.050 14.554-34.984l0.002-0.002 35.115-35.115c8.937-8.992 21.312-14.557 34.987-14.557s26.050 5.565 34.984 14.554l0.002 0.002 279.723 279.723c9.019 8.98 14.6 21.406 14.6 35.136 0 13.709-5.564 26.118-14.556 35.093l-0.001 0.001z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["circle-right"],"grid":0},"attrs":[{}],"properties":{"order":21,"id":11,"name":"circle-right","prevSize":32,"code":59659},"setIdx":0,"setId":2,"iconIdx":16},{"icon":{"paths":["M843.123 30.996l-331.098 331.098-331.098-331.098c-19.185-19.146-45.668-30.985-74.916-30.985s-55.731 11.839-74.918 30.987l0.002-0.002c-19.117 19.18-30.937 45.643-30.937 74.868s11.82 55.687 30.94 74.87l-0.003-0.003 331.050 331.098-331.050 331.244c-19.191 19.173-31.063 45.67-31.063 74.94 0 58.504 47.427 105.93 105.93 105.93 29.234 0 55.702-11.842 74.868-30.991l-0.001 0.001 331.098-331.098 331.196 331.147c19.046 18.46 45.049 29.839 73.71 29.839 58.521 0 105.961-47.44 105.961-105.961 0-28.661-11.379-54.663-29.865-73.737l0.027 0.028-331.196-331.196 331.098-331.098c18.46-19.046 29.839-45.049 29.839-73.71 0-58.521-47.44-105.961-105.961-105.961-28.661 0-54.663 11.379-73.737 29.865l0.028-0.027z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["close"],"grid":0},"attrs":[{}],"properties":{"order":22,"id":12,"name":"close","prevSize":32,"code":59660},"setIdx":0,"setId":2,"iconIdx":17},{"icon":{"paths":["M895.851 0h-767.872c-70.681 0-127.979 57.298-127.979 127.979v0 575.904c0 70.681 57.298 127.979 127.979 127.979v0h191.968v167.993c-0 0.025-0 0.056-0 0.086 0 13.288 10.772 24.060 24.060 24.060 5.399 0 10.382-1.778 14.397-4.781l-0.063 0.045 249.558-187.403h287.952c70.681 0 127.979-57.298 127.979-127.979v0-575.904c0-70.681-57.298-127.979-127.979-127.979l0 0zM604.699 575.904h-371.821v-127.979h371.821zM791.42 296.697h-558.84v-127.979h558.84z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["comment"],"grid":0},"attrs":[{}],"properties":{"order":23,"id":13,"name":"comment","prevSize":32,"code":59661},"setIdx":0,"setId":2,"iconIdx":18},{"icon":{"paths":["M646.741 485.035v-323.371l-161.707-161.664-161.664 161.664v107.819h-323.371v754.517h970.112v-538.965zM215.595 916.224h-107.819v-107.819h107.819zM215.595 700.629h-107.819v-107.776h107.819zM215.595 485.035h-107.819v-107.776h107.819zM538.965 916.181h-107.819v-107.776h107.776zM538.965 700.587h-107.819v-107.733h107.776zM538.965 484.992h-107.819v-107.733h107.776zM538.965 269.397h-107.819v-107.733h107.776zM862.336 916.139h-107.819v-107.733h107.776zM862.336 700.544h-107.819v-107.691h107.776z"],"width":970,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["company"],"grid":0},"attrs":[{}],"properties":{"order":24,"id":14,"name":"company","prevSize":32,"code":59662},"setIdx":0,"setId":2,"iconIdx":19},{"icon":{"paths":["M587.199 55.924l-55.924-55.924h-279.618l-55.924 55.924h-195.733v111.847h782.932v-111.847z","M33.107 870.508c1.405 85.070 70.697 153.493 155.97 153.493 0.217 0 0.434-0 0.65-0.001l-0.034 0h403.396c0.182 0.001 0.398 0.001 0.614 0.001 85.338 0 154.671-68.528 155.971-153.557l0.001-0.122v-647.895h-716.569zM144.954 332.112h493.023v538.023c-0.295 24.483-20.211 44.217-44.736 44.217-0.001 0-0.002-0-0.003-0l-403.21 0c-0.101 0.001-0.22 0.001-0.34 0.001-24.499 0-44.399-19.692-44.735-44.112l-0-0.032z","M288.939 834.903c30.886 0 55.924-25.038 55.924-55.924v0-316.901c0-30.886-25.038-55.924-55.924-55.924s-55.924 25.038-55.924 55.924v0 316.901c0 30.886 25.038 55.924 55.924 55.924v0z","M493.993 834.903c30.886 0 55.924-25.038 55.924-55.924v0-316.901c0-30.886-25.038-55.924-55.924-55.924s-55.924 25.038-55.924 55.924v0 316.901c0 30.886 25.038 55.924 55.924 55.924v0z"],"width":783,"attrs":[{},{},{},{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{},{},{},{}]},"tags":["delet"],"grid":0},"attrs":[{},{},{},{}],"properties":{"order":6,"id":15,"name":"delet","prevSize":32,"code":59663},"setIdx":0,"setId":2,"iconIdx":20},{"icon":{"paths":["M563.115 854.229l407.851-408.107c13.086-12.984 21.186-30.975 21.186-50.858 0-19.714-7.963-37.568-20.848-50.521l0.003 0.003-68.139-68.139c-12.998-13.055-30.985-21.133-50.859-21.133-19.725 0-37.592 7.958-50.564 20.838l0.004-0.004-289.536 289.621-289.195-289.195c-13.008-13.118-31.038-21.24-50.965-21.24-19.778 0-37.688 8.001-50.669 20.943l0.002-0.002-0.299 0.299-67.968 67.499c-13.075 12.989-21.168 30.978-21.168 50.859 0 19.71 7.956 37.563 20.831 50.521l-0.004-0.004 408.235 408.32c13.040 13.276 31.18 21.503 51.243 21.503 19.596 0 37.358-7.849 50.315-20.575l-0.011 0.010z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["down"],"grid":0},"attrs":[{}],"properties":{"order":7,"id":16,"name":"down","prevSize":32,"code":59664},"setIdx":0,"setId":2,"iconIdx":21},{"icon":{"paths":["M864 0c88.366 0 160 71.634 160 160 0 36.2-12.022 69.593-32.292 96.402l0.292-0.402-64 64-224-224 64-64c26.324-19.976 59.637-32 95.758-32 0.085 0 0.17 0 0.255 0l-0.013-0zM64 736l-64 288 288-64 592.171-592-224.171-224.128zM715.563 363.563l-448 448-55.168-55.125 448-448z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["edit"],"grid":0},"attrs":[{}],"properties":{"order":8,"id":17,"name":"edit","prevSize":32,"code":59665},"setIdx":0,"setId":2,"iconIdx":22},{"icon":{"paths":["M1123.235 921.346l-217.634-208.847c5.132 1.805 11.050 2.848 17.211 2.848 15.851 0 30.089-6.902 39.875-17.864l0.046-0.052c37.481-38.161 71.036-80.412 99.866-125.951l1.835-3.103c5.666-8.976 9.029-19.899 9.029-31.606 0-11.056-2.999-21.412-8.227-30.299l0.153 0.28c-7.952-13.359-194.813-327.802-528.499-327.802h-1.312c-35.996 0.005-71.127 3.696-105.040 10.716l3.34-0.578c-18.092 3.787-32.895 15.391-40.954 31.036l-0.156 0.333-212.306-203.798c-10.7-10.287-25.264-16.622-41.308-16.622-32.938 0-59.64 26.702-59.64 59.64 0 16.894 7.024 32.148 18.312 42.999l0.020 0.019 162.768 156.208c-5.287 1.648-9.884 3.809-14.111 6.501l0.235-0.14c-95.97 62.868-175.328 142.911-235.543 236.445l-1.89 3.135c-5.835 9.056-9.303 20.115-9.303 31.985 0 11.035 2.997 21.371 8.222 30.236l-0.152-0.279c7.952 13.359 194.813 327.802 529.175 327.802 107.604-0.9 207.542-32.95 291.456-87.555l-2.1 1.281c1.312-0.835 2.385-1.908 3.618-2.863l210.398 201.929c10.7 10.287 25.264 16.622 41.308 16.622 32.938 0 59.64-26.702 59.64-59.64 0-16.894-7.024-32.148-18.312-42.999l-0.020-0.019zM536.729 298.223c210.716 0 354.679 168.573 405.529 238.546-20.464 28.603-41.631 53.756-64.584 77.15l0.097-0.1c-10.193 10.583-16.471 24.997-16.471 40.878 0 6.315 0.993 12.399 2.831 18.103l-0.116-0.417-385.213-369.747c17.213-2.809 37.053-4.414 57.267-4.414 0.232 0 0.464 0 0.696 0.001l-0.036-0zM480.591 505.758c6.937-5.309 12.58-11.943 16.621-19.556l0.156-0.323 89.972 86.354c-14.804 12.070-33.904 19.382-54.711 19.382-7.266 0-14.323-0.892-21.069-2.571l0.598 0.126c-21.583-3.776-39.081-18.284-47.003-37.713l-0.149-0.414c-0.942-3.344-1.484-7.185-1.484-11.153 0-13.911 6.658-26.266 16.961-34.054l0.107-0.078zM536.729 775.315c-210.716 0-354.4-168.215-405.211-238.546 49.32-68.027 109.037-125.034 177.432-169.904l2.551-1.571c11.947-7.883 20.761-19.68 24.668-33.535l0.101-0.418 78.482 75.301c-2.135 1.142-3.89 2.209-5.584 3.359l0.217-0.139c-39.593 30.204-64.883 77.409-64.883 130.521 0 16.497 2.44 32.424 6.979 47.441l-0.301-1.159c21.213 61.143 72.589 106.713 135.851 119.32l1.154 0.192c13.578 3.209 29.168 5.048 45.187 5.048 51.658 0 98.848-19.131 134.872-50.694l-0.235 0.202c1.923-1.788 3.692-3.67 5.327-5.665l0.080-0.1 67.389 64.686c-58.645 34.055-128.851 54.577-203.762 55.657l-0.314 0.004z"],"width":1142,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["eye-1"],"grid":0},"attrs":[{}],"properties":{"order":9,"id":18,"name":"eye-1","prevSize":32,"code":59666},"setIdx":0,"setId":2,"iconIdx":23},{"icon":{"paths":["M1343.806 478.457c-9.762-19.575-244.11-478.457-667.948-478.457s-658.136 458.883-667.948 478.457c-4.994 9.78-7.92 21.332-7.92 33.568s2.926 23.787 8.117 33.994l-0.197-0.427c10.013 19.575 244.11 478.407 667.948 478.407s658.186-458.832 667.948-478.407c4.994-9.78 7.92-21.332 7.92-33.568s-2.926-23.787-8.117-33.994l0.197 0.427zM675.858 873.96c-272.446 0-457.030-266.138-514.553-361.809 57.523-95.671 242.107-361.809 514.553-361.809s457.13 266.288 514.553 361.809c-57.523 95.521-242.157 361.659-514.553 361.659z","M675.858 211.619c-165.896 0-300.381 134.485-300.381 300.381s134.485 300.381 300.381 300.381c165.896 0 300.381-134.485 300.381-300.381v0c-0.199-165.816-134.565-300.182-300.362-300.381l-0.020-0zM675.858 662.191c-82.948 0-150.191-67.243-150.191-150.191s67.243-150.191 150.191-150.191c82.948 0 150.191 67.243 150.191 150.191v0c0 82.948-67.243 150.191-150.191 150.191v0z"],"width":1352,"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{},{}]},"tags":["eye"],"grid":0},"attrs":[{},{}],"properties":{"order":25,"id":19,"name":"eye","prevSize":32,"code":59667},"setIdx":0,"setId":2,"iconIdx":24},{"icon":{"paths":["M470.315 379.861v136.192c52.181 11.819 98.603 31.189 147.2 44.587v-136.405c-52.011-11.605-98.773-30.976-147.2-44.416zM916.907 133.888c-67.939 35.792-147.729 58.874-232.376 63.923l-1.608 0.077c-107.008-0.213-195.584-69.76-330.368-69.76-0.457-0.002-0.998-0.003-1.539-0.003-48.265 0-94.476 8.794-137.118 24.867l2.678-0.885c4.705-11.905 7.433-25.695 7.433-40.122 0-61.856-50.144-112-112-112s-112 50.144-112 112c0 37.882 18.807 71.371 47.593 91.64l0.355 0.237v772.267c-0 0.038-0 0.083-0 0.128 0 26.439 21.433 47.872 47.872 47.872 0.045 0 0.090-0 0.135-0l-0.007 0h32c0.038 0 0.083 0 0.128 0 26.439 0 47.872-21.433 47.872-47.872 0-0.045-0-0.090-0-0.135l0 0.007v-188.928c64.268-28.012 139.144-44.31 217.827-44.31 3.851 0 7.693 0.039 11.525 0.117l-0.573-0.009c107.179 0 195.584 69.589 330.368 69.589 92.324-0.765 177.356-31.352 246.099-82.581l-1.107 0.789c16.771-11.705 27.605-30.909 27.605-52.644 0-0.047-0-0.094-0-0.141l0 0.007v-486.144c-0.078-35.287-28.702-63.863-64-63.863-9.72 0-18.935 2.167-27.186 6.044l0.391-0.165zM323.157 651.008c-54.473 5.68-104.346 17.389-151.58 34.589l4.38-1.394v-140.8c42.966-16.959 92.83-29.258 144.754-34.612l2.446-0.204zM911.957 382.208c-42.344 20.263-91.614 36.787-143.125 47.106l-4.075 0.681v141.867c55.26-7.286 105.051-25.69 148.773-52.924l-1.573 0.913v140.8c-41.573 28.653-91.64 48.031-145.739 54.055l-1.461 0.132v-142.976c-14.605 2.234-31.455 3.51-48.603 3.51-35.177 0-69.1-5.37-100.994-15.334l2.396 0.645v134.912c-39.685-15.094-88.92-29.775-139.395-41.183l-7.805-1.483v-136.875c-28.906-6.694-62.098-10.532-96.186-10.532-17.976 0-35.702 1.067-53.12 3.141l2.106-0.204v-140.032c-55.565 9.583-104.997 24.104-151.88 43.532l4.68-1.718v-140.8c42.614-19.83 92.097-35.166 143.91-43.55l3.29-0.439v142.976c14.417-2.165 31.054-3.401 47.98-3.401 35.4 0 69.537 5.408 101.63 15.442l-2.41-0.649v-134.741c39.858 15.224 89.098 29.908 139.605 41.235l7.595 1.431v136.704c26.239 6.45 56.363 10.149 87.35 10.149 21.13 0 41.859-1.72 62.054-5.028l-2.204 0.298v-143.872c55.763-10.757 105.071-26.328 151.746-46.79l-4.546 1.777z"],"width":1008,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["flag"],"grid":0},"attrs":[{}],"properties":{"order":26,"id":20,"name":"flag","prevSize":32,"code":59668},"setIdx":0,"setId":2,"iconIdx":25},{"icon":{"paths":["M460.771 358.396h102.293v-102.251h-102.293zM511.917 921.749c-0.012 0-0.027 0-0.041 0-226.23 0-409.625-183.395-409.625-409.625s183.395-409.625 409.625-409.625c226.215 0 409.601 183.372 409.625 409.581l0 0.002c0 226.23-183.395 409.625-409.625 409.625v0zM511.876-0.207c-282.717 0.071-511.876 229.274-511.876 512 0 282.77 229.23 512 512 512 282.697 0 511.882-229.113 512-511.782l0-0.011c0-282.77-229.23-512-512-512v0zM460.854 768.021h102.21v-307.25h-102.21z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["information"],"grid":0},"attrs":[{}],"properties":{"order":27,"id":21,"name":"information","prevSize":32,"code":59669},"setIdx":0,"setId":2,"iconIdx":26},{"icon":{"paths":["M22.52 566.594l435.159 434.886c13.85 13.942 33.032 22.572 54.23 22.572s40.38-8.63 54.226-22.567l0.004-0.004 72.291-72.291c13.931-13.855 22.553-33.035 22.553-54.23s-8.622-40.375-22.549-54.227l-0.003-0.003-308.501-308.41 308.365-308.365c13.931-13.855 22.553-33.035 22.553-54.23s-8.622-40.375-22.549-54.227l-0.003-0.003-71.973-72.792c-13.85-13.942-33.032-22.572-54.23-22.572s-40.38 8.63-54.226 22.567l-0.004 0.004-435.023 434.932c-14.154 13.904-22.927 33.246-22.927 54.637 0 21.234 8.643 40.449 22.604 54.319l0.004 0.004z"],"width":661,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["left"],"grid":0},"attrs":[{}],"properties":{"order":28,"id":22,"name":"left","prevSize":32,"code":59670},"setIdx":0,"setId":2,"iconIdx":27},{"icon":{"paths":["M558.549 0.469l-465.451-0.469c-51.358 0.145-92.954 41.74-93.099 93.085l-0 0.014v837.803c0.145 51.358 41.74 92.954 93.085 93.099l0.014 0h465.451c51.358-0.145 92.954-41.74 93.099-93.085l0-0.014v-837.803c-0.194-51.199-41.744-92.629-92.97-92.629-0.045 0-0.090 0-0.136 0l0.007-0zM558.549 837.803h-465.451v-651.605h465.451z"],"width":652,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["phone"],"grid":0},"attrs":[{}],"properties":{"order":29,"id":23,"name":"phone","prevSize":32,"code":59671},"setIdx":0,"setId":2,"iconIdx":28},{"icon":{"paths":["M384 0c-0.789-0.017-1.719-0.027-2.651-0.027-34.738 0-66.285 13.665-89.558 35.914l0.049-0.046c-22.202 23.224-35.867 54.77-35.867 89.509 0 0.932 0.010 1.862 0.029 2.789l-0.002-0.138c5.836 34.821 18.412 65.879 36.393 93.049l-0.553-0.889c2.672 8.237 4.213 17.715 4.213 27.553 0 2.918-0.136 5.803-0.401 8.652l0.027-0.365h-295.68v768h295.68c0.053-1.37 0.084-2.978 0.084-4.594 0-11.101-1.431-21.868-4.119-32.126l0.196 0.88c-17.428-26.281-30.005-57.34-35.644-90.756l-0.196-1.404c-0.017-0.789-0.027-1.719-0.027-2.651 0-34.738 13.665-66.285 35.914-89.558l-0.046 0.049c23.224-22.202 54.77-35.867 89.509-35.867 0.932 0 1.862 0.010 2.789 0.029l-0.138-0.002c0.789-0.017 1.719-0.027 2.651-0.027 34.738 0 66.285 13.665 89.558 35.914l-0.049-0.046c22.202 23.224 35.867 54.77 35.867 89.509 0 0.932-0.010 1.862-0.029 2.789l0.002-0.138c-5.836 34.821-18.412 65.879-36.393 93.049l0.553-0.889c-2.672 8.237-4.213 17.715-4.213 27.553 0 2.918 0.136 5.803 0.401 8.652l-0.027-0.365h295.68v-295.68c1.37-0.053 2.978-0.084 4.594-0.084 11.101 0 21.868 1.431 32.126 4.119l-0.88-0.196c26.281 17.428 57.34 30.005 90.756 35.644l1.404 0.196c70.692 0 128-57.308 128-128s-57.308-128-128-128v0c-34.821 5.836-65.879 18.412-93.049 36.393l0.889-0.553c-8.237 2.672-17.715 4.213-27.553 4.213-2.918 0-5.803-0.136-8.652-0.401l0.365 0.027v-295.68h-295.68c-0.053-1.37-0.084-2.978-0.084-4.594 0-11.101 1.431-21.868 4.119-32.126l-0.196 0.88c17.428-26.281 30.005-57.34 35.644-90.756l0.196-1.404c0.017-0.789 0.027-1.719 0.027-2.651 0-34.738-13.665-66.285-35.914-89.558l0.046 0.049c-23.224-22.202-54.77-35.867-89.509-35.867-0.932 0-1.862 0.010-2.789 0.029l0.138-0.002z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["puzzle"],"grid":0},"attrs":[{}],"properties":{"order":30,"id":24,"name":"puzzle","prevSize":32,"code":59672},"setIdx":0,"setId":2,"iconIdx":29},{"icon":{"paths":["M638.368 566.619l-435.042 434.86c-13.855 13.932-33.037 22.554-54.232 22.554s-40.377-8.622-54.229-22.55l-0.003-0.003-72.295-72.295c-13.932-13.855-22.554-33.037-22.554-54.232s8.622-40.377 22.55-54.229l0.003-0.003 308.378-308.378-308.378-308.378c-13.932-13.855-22.554-33.037-22.554-54.232s8.622-40.377 22.55-54.229l0.003-0.003 71.976-72.795c13.855-13.932 33.037-22.554 54.232-22.554s40.377 8.622 54.229 22.55l0.003 0.003 435.042 434.723c14.1 13.896 22.836 33.203 22.836 54.549 0 21.189-8.607 40.368-22.515 54.232l-0.002 0.002z"],"width":661,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["right"],"grid":0},"attrs":[{}],"properties":{"order":31,"id":25,"name":"right","prevSize":32,"code":59673},"setIdx":0,"setId":2,"iconIdx":30},{"icon":{"paths":["M227.579 579.116v227.579l398.211 217.305 398.211-217.305v-227.579l-398.211 217.358zM625.789-0l-625.789 341.316 625.789 341.316 512-279.311v393.1h113.789v-455.105z"],"width":1252,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["school"],"grid":0},"attrs":[{}],"properties":{"order":32,"id":26,"name":"school","prevSize":32,"code":59674},"setIdx":0,"setId":2,"iconIdx":31},{"icon":{"paths":["M1009.922 885.314l-199.391-199.391c-8.672-8.647-20.639-13.993-33.854-13.993-0.096 0-0.192 0-0.288 0.001l0.015-0h-32.592c54.858-69.783 87.982-158.908 87.982-255.769 0-229.618-186.142-415.76-415.76-415.76s-415.76 186.142-415.76 415.76c0 229.618 186.142 415.76 415.76 415.76 96.861 0 185.986-33.123 256.666-88.661l-0.897 0.679v32.464c-0 0.081-0.001 0.177-0.001 0.273 0 13.216 5.346 25.182 13.994 33.855l-0.001-0.001 199.391 199.391c8.657 8.711 20.645 14.103 33.893 14.103s25.236-5.392 33.891-14.1l0.002-0.002 56.609-56.609c8.774-8.732 14.204-20.817 14.204-34.171 0-13.184-5.292-25.131-13.869-33.834l0.006 0.006zM415.973 672.016c-141.361 0-255.957-114.596-255.957-255.957s114.596-255.957 255.957-255.957c141.361 0 255.957 114.596 255.957 255.957v0c-0.049 141.325-114.626 255.872-255.957 255.872l-0-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["search"],"grid":0},"attrs":[{}],"properties":{"order":33,"id":27,"name":"search","prevSize":32,"code":59675},"setIdx":0,"setId":2,"iconIdx":32},{"icon":{"paths":["M264.124 1024l-3.988-0.315c-45.874-4.728-81.349-43.172-81.349-89.9 0-9.949 1.608-19.523 4.579-28.476l-0.183 0.638 89.58-260.661-235.092-165.554c-19.618-13.599-33.145-34.797-36.332-59.244l-0.046-0.43c-0.823-4.244-1.294-9.125-1.294-14.115 0-0.018 0-0.036 0-0.054l-0 0.003c0.197-48.36 39.308-87.52 87.63-87.796l0.026-0h289.553l86.747-258.842c12.356-34.779 44.982-59.235 83.32-59.235 37.789 0 70.029 23.761 82.593 57.157l0.201 0.608 87.272 260.311h287.525c0.011-0 0.025-0 0.038-0 47.863 0 86.99 37.407 89.741 84.58l0.011 0.243v3.848l-0.315 3.883c-2.124 22.565-12.304 42.418-27.594 56.944l-0.039 0.036-11.963 10.074-233.098 167.233 89.755 261.011c3.161 8.633 4.99 18.602 4.99 28.997 0 27.516-12.812 52.041-32.795 67.933l-0.177 0.136c-13.364 11.803-30.658 19.438-49.692 20.728l-0.258 0.014-3.183 0.21-3.148-0.175c-18.973-1.079-36.226-7.647-50.416-18.125l0.256 0.181-229.565-163.77-230.369 164.12c-13.682 9.851-30.475 16.214-48.672 17.473l-0.298 0.017zM812.414 940.051v0.28zM282.243 939.876v0zM838.403 920.498v0zM139.39 423.032l258.003 181.889-97.94 285.006 247.929-176.642 247.649 176.502-97.94-284.586 254.015-182.169h-309.246l-94.442-282.243-94.722 282.243zM1004.972 423.032v0zM97.346 393.405l0.56 0.42z"],"width":1095,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["star-1"],"grid":0},"attrs":[{}],"properties":{"order":34,"id":28,"name":"star-1","prevSize":32,"code":59676},"setIdx":0,"setId":2,"iconIdx":33},{"icon":{"paths":["M1060.923 354.485h-362.082l-110.034-328.386c-5.769-15.366-20.337-26.102-37.413-26.102s-31.644 10.737-37.322 25.828l-0.091 0.275-110.034 328.386h-364.574c-21.717 0.069-39.304 17.656-39.374 39.367l-0 0.007c-0 0.051-0.001 0.111-0.001 0.172 0 2.3 0.268 4.537 0.775 6.682l-0.039-0.196c1.082 11.588 7.245 21.545 16.211 27.732l0.127 0.083 297.753 209.856-114.363 332.062c-1.356 3.823-2.14 8.232-2.14 12.824 0 12.816 6.104 24.206 15.563 31.422l0.096 0.070c5.913 5.322 13.556 8.829 21.991 9.588l0.147 0.011c9.263-0.701 17.658-3.918 24.649-8.961l-0.142 0.098 290.483-207.038 290.483 207.038c6.807 5.047 15.229 8.284 24.374 8.857l0.132 0.007c8.688-0.56 16.438-4.176 22.272-9.773l-0.012 0.012c9.602-7.187 15.75-18.533 15.75-31.315 0-4.66-0.817-9.13-2.316-13.273l0.086 0.272-114.159-331.898 295.139-211.694 7.148-6.167c7.108-6.72 11.823-15.905 12.772-26.182l0.012-0.163c-1.204-22.063-19.388-39.497-41.643-39.497-0.078 0-0.156 0-0.234 0.001l0.012-0z"],"width":1103,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["star"],"grid":0},"attrs":[{}],"properties":{"order":35,"id":29,"name":"star","prevSize":32,"code":59677},"setIdx":0,"setId":2,"iconIdx":34},{"icon":{"paths":["M662.613 361.941c-7.552-20.181 201.344-206.72 79.531-357.888-28.501-35.328-125.227 169.259-262.528 261.845-75.776 51.2-252.075 159.872-252.075 219.989v389.461c0 72.32 279.552 148.864 491.989 148.864 77.867 0 190.677-487.851 190.677-565.333s-240.128-76.459-247.467-96.725zM170.624 367.36c-0.681-0.010-1.486-0.016-2.292-0.016-93.126 0-168.619 75.493-168.619 168.619 0 3.187 0.088 6.354 0.263 9.498l-0.019-0.437v275.797c-0.161 2.71-0.252 5.878-0.252 9.068 0 90.015 72.972 162.987 162.987 162.987 2.79 0 5.564-0.070 8.32-0.209l-0.387 0.016c37.376 0-56.875-32.555-56.875-128.555v-362.667c0-100.565 94.251-134.187 56.875-134.187z"],"width":910,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["thumbs-up"],"grid":0},"attrs":[{}],"properties":{"order":10,"id":30,"name":"thumbs-up","prevSize":32,"code":59678},"setIdx":0,"setId":2,"iconIdx":35},{"icon":{"paths":["M511.488 0c-282.55 0.292-511.488 229.41-511.488 512 0 282.77 229.23 512 512 512s512-229.23 512-512l-0 0c0-0.076 0-0.166 0-0.257 0-282.628-229.116-511.744-511.744-511.744-0.27 0-0.54 0-0.81 0.001l0.042-0zM512 921.6c-226.216 0-409.6-183.384-409.6-409.6s183.384-409.6 409.6-409.6c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM537.6 256h-76.8v307.2l268.8 161.28 38.4-63.147-230.4-136.533z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["time"],"grid":0},"attrs":[{}],"properties":{"order":11,"id":31,"name":"time","prevSize":32,"code":59679},"setIdx":0,"setId":2,"iconIdx":36},{"icon":{"paths":["M563.115 223.232l407.851 408.107c13.137 12.992 21.272 31.018 21.272 50.943 0 19.756-7.997 37.645-20.932 50.605l0.001-0.001-68.139 68.139c-12.998 13.055-30.985 21.133-50.859 21.133-19.725 0-37.592-7.958-50.564-20.838l0.004 0.004-289.536-289.792-289.195 289.195c-12.998 13.055-30.985 21.133-50.859 21.133-19.725 0-37.592-7.958-50.564-20.838l0.004 0.004-0.213-0.299-68.267-67.499c-13.075-12.989-21.168-30.978-21.168-50.859 0-19.71 7.956-37.563 20.831-50.521l-0.004 0.004 408.235-408.32c13.040-13.276 31.18-21.503 51.243-21.503 19.596 0 37.358 7.849 50.315 20.575l-0.011-0.010z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["top"],"grid":0},"attrs":[{}],"properties":{"order":12,"id":32,"name":"top","prevSize":32,"code":59680},"setIdx":0,"setId":2,"iconIdx":37},{"icon":{"paths":["M1104.017 128.018h-207.987v-80.017c0-26.51-21.491-48.001-48.001-48.001v0h-544.040c-26.51 0-48.001 21.491-48.001 48.001v0 80.017h-207.987c-26.51 0-48.001 21.491-48.001 48.001v0 111.986c7.076 85.462 54.363 158.548 122.682 200.96l1.112 0.643c61.778 44.226 136.678 74.099 217.854 83.23l2.133 0.195c34.389 59.486 80.159 108.69 134.709 145.998l1.518 0.98v144.002h-96.001c-3.854-0.457-8.318-0.718-12.843-0.718-62.746 0-113.776 50.163-115.172 112.574l-0.002 0.13v24c0 13.255 10.745 24 24 24v0h592.041c13.255 0 24-10.745 24-24v0-24c-1.399-62.54-52.429-112.704-115.175-112.704-4.525 0-8.989 0.261-13.378 0.768l0.535-0.050h-96.001v-144.002c56.068-38.356 101.826-87.64 135.041-145.086l1.137-2.132c83.314-9.166 158.252-38.973 221.451-84.147l-1.416 0.962c69.366-43.121 116.621-116.171 123.729-200.646l0.065-0.957v-111.986c0-26.51-21.491-48.001-48.001-48.001v0zM198.627 385.59c-35.829-20.914-61.682-55.601-70.437-96.621l-0.172-0.964v-32.016h128.402c1.638 61.997 10.873 121.145 26.808 177.479l-1.223-5.061c-31.815-11.002-59.407-25.499-84.329-43.47l0.951 0.653zM1024 288.005c-9.695 41.665-35.309 76.091-69.932 97.202l-0.677 0.383c-24.040 17.319-51.713 31.817-81.346 42.129l-2.271 0.688c14.711-51.273 23.946-110.421 25.564-171.467l0.020-0.952h128.594z"],"width":1152,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"colorPermutations":{"12552552551":[{}]},"tags":["trophy"],"grid":0},"attrs":[{}],"properties":{"order":13,"id":33,"name":"trophy","prevSize":32,"code":59681},"setIdx":0,"setId":2,"iconIdx":38}],"height":1024,"metadata":{"name":"icomoon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icomoon"},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon"},"historySize":50,"showCodes":true,"gridSize":16}}
PAMapp/assets/icon/style.css
@@ -1,10 +1,10 @@
@font-face {
  font-family: 'icomoon';
  src:  url('fonts/icomoon.eot?vrc42b');
  src:  url('fonts/icomoon.eot?vrc42b#iefix') format('embedded-opentype'),
    url('fonts/icomoon.ttf?vrc42b') format('truetype'),
    url('fonts/icomoon.woff?vrc42b') format('woff'),
    url('fonts/icomoon.svg?vrc42b#icomoon') format('svg');
  src:  url('fonts/icomoon.eot?xbg0m9');
  src:  url('fonts/icomoon.eot?xbg0m9#iefix') format('embedded-opentype'),
    url('fonts/icomoon.ttf?xbg0m9') format('truetype'),
    url('fonts/icomoon.woff?xbg0m9') format('woff'),
    url('fonts/icomoon.svg?xbg0m9#icomoon') format('svg');
  font-weight: normal;
  font-style: normal;
  font-display: block;
@@ -25,6 +25,21 @@
  -moz-osx-font-smoothing: grayscale;
}
.icon-sort-decrease:before {
  content: "\e922";
}
.icon-sort-add:before {
  content: "\e923";
}
.icon-expand:before {
  content: "\e924";
}
.icon-sex-female:before {
  content: "\e925";
}
.icon-sex-male:before {
  content: "\e926";
}
.icon-decrease:before {
  content: "\e900";
}
PAMapp/assets/images/appointment/avatar_bg.svg
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,9 @@
<svg id="Component_29" data-name="Component 29" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
  <circle id="Ellipse_726" data-name="Ellipse 726" cx="50" cy="50" r="50" fill="#feecdc"/>
  <g id="Group_3685" data-name="Group 3685" transform="translate(-4269 -1812)">
    <circle id="Ellipse_723" data-name="Ellipse 723" cx="50" cy="50" r="50" transform="translate(4269 1812)" fill="#feecdc"/>
    <path id="Intersection_4" data-name="Intersection 4" d="M8376.456,8861.851v-2.594c0-14.428,24.235-18.738,38.724-18.738s38.367,4.313,38.367,18.738v2.59A49.9,49.9,0,0,1,8415.013,8880H8415A49.892,49.892,0,0,1,8376.456,8861.851Zm17.021-48.454a21.7,21.7,0,0,1,21.7-21.686h.014a21.693,21.693,0,1,1-21.716,21.686Z" transform="translate(-4095.456 -6968)" fill="#d0d0ce" opacity="0.789"/>
    <circle id="Ellipse_724" data-name="Ellipse 724" cx="15" cy="15" r="15" transform="translate(4334 1882)" fill="none"/>
    <path id="female" d="M14.817,13.279l1.116,1.11c.457.457.92.914,1.377,1.383a.856.856,0,0,1-1.235,1.187c-.665-.653-1.324-1.318-1.983-1.977l-.516-.522a.938.938,0,0,1-.107.16l-2.339,2.339a.837.837,0,0,1-.867.231.813.813,0,0,1-.594-.641.843.843,0,0,1,.261-.819l2.321-2.315a.771.771,0,0,1,.125-.095l-1.395-1.4a5.9,5.9,0,0,1-5.028.968,5.734,5.734,0,0,1-3.383-2.333,5.936,5.936,0,1,1,9.669.107l1.431,1.4c.071-.083.142-.184.231-.273.736-.736,1.466-1.472,2.208-2.2A.859.859,0,1,1,17.3,10.822l-1.6,1.6C15.4,12.7,15.114,12.965,14.817,13.279ZM11.683,7.124a4.244,4.244,0,1,0-4.256,4.232A4.244,4.244,0,0,0,11.683,7.124Z" transform="matrix(0.719, 0.695, -0.695, 0.719, 4348.543, 1882.728)" fill="none"/>
  </g>
</svg>
PAMapp/assets/images/logo.png

PAMapp/assets/images/notification/banner_mob.svg
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,56 @@
<svg id="notification_mob" data-name="notification â€“ mob" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="375" height="120" viewBox="0 0 375 120">
  <defs>
    <clipPath id="clip-path">
      <rect id="Rectangle_4042" data-name="Rectangle 4042" width="375" height="120" fill="none"/>
    </clipPath>
    <linearGradient id="linear-gradient" x1="-1.128" y1="2.281" x2="-0.128" y2="2.281" gradientUnits="objectBoundingBox">
      <stop offset="0" stop-color="#edc9c7"/>
      <stop offset="1" stop-color="#f3e2e1"/>
    </linearGradient>
  </defs>
  <g id="Group_3388" data-name="Group 3388" transform="translate(1 -1)">
    <g id="Group_3387" data-name="Group 3387" transform="translate(-1 1)" clip-path="url(#clip-path)">
      <g id="Group_3386" data-name="Group 3386" transform="translate(-3.5 -50.5)">
        <g id="Group_3383" data-name="Group 3383" transform="translate(0 42.5)">
          <rect id="Rectangle_4025" data-name="Rectangle 4025" width="134.5" height="398" transform="translate(0 134.5) rotate(-90)" fill="url(#linear-gradient)"/>
        </g>
        <rect id="Rectangle_4026" data-name="Rectangle 4026" width="225" height="72.5" transform="translate(161 111.5)" fill="#f09491"/>
        <rect id="Rectangle_4027" data-name="Rectangle 4027" width="375" height="120" transform="translate(4.5 50.5)" fill="none"/>
        <path id="Path_8048" data-name="Path 8048" d="M505.6,13.343,480.284-95.912a5.277,5.277,0,0,0-6.332-3.95L183.977-32.675a5.278,5.278,0,0,0-3.951,6.332l8.8,38.431L173,24.641a2.912,2.912,0,0,0,1.188,5.127l19.738,4.31,11.418,48.835a5.278,5.278,0,0,0,6.333,3.95L501.649,19.675A5.277,5.277,0,0,0,505.6,13.343Z" transform="translate(-81.946 100)" fill="#f09491"/>
        <path id="Path_8049" data-name="Path 8049" d="M472.4,135.243l-74.942,34.674a8.661,8.661,0,0,1-6.521-.636L359.2,143.793a2.154,2.154,0,0,1,.792-3.589l80.219-29.133a5.109,5.109,0,0,1,4.938.636l27.519,19.947C473.986,132.974,473.733,134.648,472.4,135.243Z" transform="translate(-175.259 -5.352)" fill="#1b365d" opacity="0.5"/>
        <path id="Path_8050" data-name="Path 8050" d="M473.873,114.133l-74.941,34.674a8.667,8.667,0,0,1-6.522-.636l-31.741-25.488c-.168-.112-.838-.452-.792-1.056.264-3.43.007-6.92,1.055-7.388L441.68,89.962a5.106,5.106,0,0,1,4.938.636l28.574,15.725C475.72,108.433,475.2,113.538,473.873,114.133Z" transform="translate(-175.937 5.203)" fill="#a7a8aa"/>
        <path id="Path_8051" data-name="Path 8051" d="M474.514,103.05l-74.942,34.674a8.663,8.663,0,0,1-6.521-.636L361.309,111.6a2.154,2.154,0,0,1,.792-3.589l80.22-29.132a5.108,5.108,0,0,1,4.938.636l27.519,19.947C476.1,100.781,475.844,102.455,474.514,103.05Z" transform="translate(-176.314 10.745)" fill="#feecdc"/>
        <path id="Path_8052" data-name="Path 8052" d="M495.549,118.372l-14.578,6.789a5.2,5.2,0,0,1-4.223-.825l-21.506-16.67a1.6,1.6,0,0,1,.313-2.61l15.366-5.78a3.132,3.132,0,0,1,3.206.739l21.4,15.717C496.443,116.78,496.371,118,495.549,118.372Z" transform="translate(-223.36 0.453)" fill="#fff"/>
        <path id="Path_8053" data-name="Path 8053" d="M437,142.121l-14.578,6.789a5.2,5.2,0,0,1-4.223-.824l-22.006-17.17a1.6,1.6,0,0,1,.314-2.61l15.866-5.28a3.133,3.133,0,0,1,3.206.739l21.4,15.717C437.889,140.529,437.817,141.753,437,142.121Z" transform="translate(-193.833 -11.422)" fill="#fff"/>
        <path id="Path_8054" data-name="Path 8054" d="M540.751,104.906l-1.5,1.383a1.669,1.669,0,0,1-1.353-.264l-6.625-5.4a.512.512,0,0,1,.1-.836l1.492-1a1,1,0,0,1,1.027.237l6.863,5C541.045,104.365,541.015,104.558,540.751,104.906Z" transform="translate(-261.551 0.635)" fill="#a7a8aa"/>
        <path id="Path_8055" data-name="Path 8055" d="M391.314,173.552l-.712.939a.715.715,0,0,1-.871-.148L383.4,169.33a.634.634,0,0,1-.146-.807l.674-.776c.263-.118.725-.3,1.055,0l6.069,4.75C391.344,172.832,391.578,173.2,391.314,173.552Z" transform="translate(-187.583 -33.791)" fill="#fff"/>
        <rect id="Rectangle_4028" data-name="Rectangle 4028" width="24.277" height="24.541" rx="5.278" transform="translate(131.955 109.942)" fill="#5cb8b2"/>
        <rect id="Rectangle_4029" data-name="Rectangle 4029" width="30.61" height="30.943" rx="5.278" transform="translate(178.662 59.277)" fill="#fff" opacity="0.7"/>
        <rect id="Rectangle_4030" data-name="Rectangle 4030" width="30.61" height="30.943" rx="5.278" transform="translate(236.452 67.194)" fill="#f2c75c" opacity="0.7"/>
        <path id="Path_8056" data-name="Path 8056" d="M149.422,69.681l-6.879,10.753,7.88,6.207-9.28,9.1,5.192-8.723-7.1-5.9Z" transform="translate(-65.616 15.16)" fill="#fff"/>
        <path id="Path_8057" data-name="Path 8057" d="M187.495,180.835,175.68,176l-4.688,8.868-10.621-7.492,9.515,3.539,4.53-8.048Z" transform="translate(-76.186 -36.434)" fill="#fff"/>
        <g id="Group_3384" data-name="Group 3384" transform="translate(21.25 101.584)">
          <ellipse id="Ellipse_684" data-name="Ellipse 684" cx="4.354" cy="4.882" rx="4.354" ry="4.882" transform="translate(18.449 43.12) rotate(-89.901)" fill="#d0d0ce"/>
          <path id="Path_8058" data-name="Path 8058" d="M67.989,173.6s-1.589,3.163-.278,8.179,3.676,11.09,3.676,11.09,1.052,1.849-.268,2.374-7.129,2.627-7.129,2.627-2.641,1.051-3.43-.534-8.145-20.6-8.145-20.6Z" transform="translate(-43.457 -138.383)" fill="#a7a8aa"/>
          <circle id="Ellipse_685" data-name="Ellipse 685" cx="6.333" cy="6.333" r="6.333" transform="translate(40.134 9.656)" fill="#f09491"/>
          <circle id="Ellipse_686" data-name="Ellipse 686" cx="7.784" cy="7.784" r="7.784" transform="translate(0 22.255)" fill="#68737a"/>
          <path id="Path_8059" data-name="Path 8059" d="M42.159,132.015s-4.736-7.66,9-13.442,16.637-7.1,21.922-11.837,6.34-4.211,7.656-1.57,8.929,25.084,9.451,28.251-1.328,5.011-6.869,4.738-19.267,1.814-25.6,3.65S44.771,147.06,42.159,132.015Z" transform="translate(-37.934 -103.169)" fill="#fff"/>
          <path id="Path_8060" data-name="Path 8060" d="M52.376,131.871l7.244,23.674s-7.028,4.033-11.511,2.706-6.318-8.983-6.318-8.983-3.677-10.033,2.134-13.19A91.3,91.3,0,0,1,52.376,131.871Z" transform="translate(-37.571 -117.52)" fill="#f09491"/>
        </g>
        <rect id="Rectangle_4031" data-name="Rectangle 4031" width="21.374" height="3.43" transform="translate(183.28 71.811)" fill="#d0d0ce"/>
        <rect id="Rectangle_4032" data-name="Rectangle 4032" width="21.374" height="3.43" transform="translate(183.28 65.214)" fill="#d0d0ce"/>
        <rect id="Rectangle_4033" data-name="Rectangle 4033" width="13.986" height="3.43" transform="translate(183.28 78.409)" fill="#d0d0ce"/>
        <rect id="Rectangle_4034" data-name="Rectangle 4034" width="21.374" height="3.43" transform="translate(240.806 73.659)" fill="#d0d0ce"/>
        <rect id="Rectangle_4035" data-name="Rectangle 4035" width="13.986" height="3.43" transform="translate(240.806 80.256)" fill="#d0d0ce"/>
        <rect id="Rectangle_4036" data-name="Rectangle 4036" width="17.416" height="3.43" transform="translate(135.254 114.824)" fill="#d0d0ce"/>
        <rect id="Rectangle_4037" data-name="Rectangle 4037" width="11.396" height="3.43" transform="translate(135.254 121.421)" fill="#d0d0ce"/>
        <g id="Group_3385" data-name="Group 3385" transform="translate(312.602 82.875)">
          <rect id="Rectangle_4038" data-name="Rectangle 4038" width="37.851" height="38.263" rx="5.278" fill="#fff"/>
          <rect id="Rectangle_4039" data-name="Rectangle 4039" width="30.346" height="3.43" transform="translate(4.09 12.27)" fill="#d0d0ce"/>
          <rect id="Rectangle_4040" data-name="Rectangle 4040" width="30.346" height="3.43" transform="translate(4.09 5.673)" fill="#d0d0ce"/>
          <rect id="Rectangle_4041" data-name="Rectangle 4041" width="19.856" height="3.43" transform="translate(4.09 18.867)" fill="#d0d0ce"/>
        </g>
      </g>
    </g>
  </g>
</svg>
PAMapp/assets/images/notification/banner_web.svg
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,60 @@
<svg id="notification_web" data-name="notification â€“ web" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="150" viewBox="0 0 1280 150">
  <defs>
    <clipPath id="clip-path">
      <rect id="Rectangle_3981" data-name="Rectangle 3981" width="1280" height="150" transform="translate(0 1)" fill="none"/>
    </clipPath>
    <linearGradient id="linear-gradient" x1="-9.872" y1="-2.825" x2="-8.872" y2="-2.825" gradientUnits="objectBoundingBox">
      <stop offset="0" stop-color="#edc9c7"/>
      <stop offset="1" stop-color="#f3e2e1"/>
    </linearGradient>
  </defs>
  <g id="Group_3422" data-name="Group 3422" transform="translate(0 -1)">
    <g id="Group_3421" data-name="Group 3421" clip-path="url(#clip-path)">
      <g id="Group_3420" data-name="Group 3420">
        <g id="Group_3415" data-name="Group 3415">
          <rect id="Rectangle_3964" data-name="Rectangle 3964" width="269" height="796" transform="translate(-6 247) rotate(-90)" fill="url(#linear-gradient)"/>
        </g>
        <rect id="Rectangle_3965" data-name="Rectangle 3965" width="751.302" height="155.546" transform="translate(541.045 18.832)" fill="#f09491"/>
        <rect id="Rectangle_3966" data-name="Rectangle 3966" width="750" height="240" transform="translate(3 -6)" fill="none"/>
        <path id="Path_8017" data-name="Path 8017" d="M1299.375,29.165l-38.229-164.992a7.971,7.971,0,0,0-9.564-5.966L451.993-1.82a7.971,7.971,0,0,0-5.966,9.563L459.318,65.78,435.41,84.735a4.4,4.4,0,0,0,1.794,7.743l29.808,6.509,17.244,73.748a7.971,7.971,0,0,0,9.564,5.966L1293.409,38.728A7.971,7.971,0,0,0,1299.375,29.165Z" fill="#f09491"/>
        <path id="Path_8018" data-name="Path 8018" d="M746.643,92.665,633.469,145.029c-2.009.9-7.173.8-9.848-.961l-47.934-38.491c-1.993-1.993-.813-4.521,1.2-5.419L698.026,56.163a7.716,7.716,0,0,1,7.458.96l41.558,30.123C749.034,89.238,748.652,91.767,746.643,92.665Z" fill="#1b365d" opacity="0.5"/>
        <path id="Path_8019" data-name="Path 8019" d="M747.838,76.725,634.665,129.088c-2.009.9-7.173.8-9.848-.96L576.882,89.637c-.253-.169-1.265-.684-1.2-1.594.4-5.181.011-10.45,1.594-11.158L699.222,40.223a7.712,7.712,0,0,1,7.457.96L749.831,64.93C750.628,68.118,749.848,75.827,747.838,76.725Z" fill="#a7a8aa"/>
        <path id="Path_8020" data-name="Path 8020" d="M748.237,68.357,635.063,120.72c-2.009.9-7.173.8-9.848-.96L577.281,81.268c-1.993-1.992-.813-4.521,1.2-5.419L699.62,31.854a7.714,7.714,0,0,1,7.458.961l41.558,30.122C750.628,64.93,750.246,67.458,748.237,68.357Z" fill="#feecdc"/>
        <path id="Path_8021" data-name="Path 8021" d="M708.958,75.952,686.943,86.205c-1.241.555-4.563.2-6.377-1.245L648.089,59.786c-1.388-1.582-.768-3.388.473-3.943l23.205-8.728a4.73,4.73,0,0,1,4.841,1.115L708.92,71.966C710.308,73.548,710.2,75.4,708.958,75.952Z" fill="#fff"/>
        <path id="Path_8022" data-name="Path 8022" d="M665.123,93.885l-22.015,10.253c-1.241.555-4.563.2-6.377-1.246L603.5,76.963c-1.388-1.582-.768-3.387.473-3.942l23.96-7.974a4.73,4.73,0,0,1,4.841,1.116L665.085,89.9C666.473,91.481,666.364,93.329,665.123,93.885Z" fill="#fff"/>
        <path id="Path_8023" data-name="Path 8023" d="M719.545,55.892l-2.271,2.089a2.518,2.518,0,0,1-2.042-.4l-10.006-8.15a.774.774,0,0,1,.152-1.263l2.253-1.515a1.515,1.515,0,0,1,1.551.357l10.363,7.558C719.99,55.076,719.944,55.366,719.545,55.892Z" fill="#a7a8aa"/>
        <path id="Path_8024" data-name="Path 8024" d="M605.574,107.569l-1.075,1.42a1.082,1.082,0,0,1-1.316-.224l-9.564-7.572a.959.959,0,0,1-.22-1.219l1.017-1.172c.4-.177,1.095-.455,1.594,0l9.166,7.173C605.62,106.482,605.974,107.044,605.574,107.569Z" fill="#fff"/>
        <rect id="Rectangle_3967" data-name="Rectangle 3967" width="36.662" height="37.061" rx="3.985" transform="translate(497.182 62.539)" fill="#5cb8b2"/>
        <path id="Path_8025" data-name="Path 8025" d="M424.469,22.631,414.081,38.869l11.9,9.373L411.966,61.985l7.841-13.174L409.082,39.9Z" fill="#fff"/>
        <path id="Path_8026" data-name="Path 8026" d="M466,112.577l-17.842-7.3-7.08,13.392-16.039-11.314,14.369,5.344,6.841-12.154Z" fill="#fff"/>
        <g id="Group_3416" data-name="Group 3416">
          <ellipse id="Ellipse_678" data-name="Ellipse 678" cx="6.575" cy="7.372" rx="6.575" ry="7.372" transform="translate(357.861 113.036) rotate(-89.901)" fill="#d0d0ce"/>
          <path id="Path_8027" data-name="Path 8027" d="M367.047,101.1s-2.4,4.778-.42,12.353,5.55,16.747,5.55,16.747,1.589,2.792-.4,3.586-10.767,3.966-10.767,3.966-3.988,1.587-5.179-.806-12.3-31.1-12.3-31.1Z" fill="#a7a8aa"/>
          <circle id="Ellipse_679" data-name="Ellipse 679" cx="9.564" cy="9.564" r="9.564" transform="translate(390.609 62.499)" fill="#f09491"/>
          <circle id="Ellipse_680" data-name="Ellipse 680" cx="11.756" cy="11.756" r="11.756" transform="translate(330 81.526)" fill="#68737a"/>
          <path id="Path_8028" data-name="Path 8028" d="M336.379,91.479s-7.153-11.569,13.584-20.3S375.087,60.463,383.07,53.3s9.575-6.36,11.56-2.371S408.114,88.814,408.9,93.6s-2.006,7.568-10.374,7.155-29.1,2.739-38.664,5.512S340.325,114.2,336.379,91.479Z" fill="#fff"/>
          <path id="Path_8029" data-name="Path 8029" d="M352.357,69.589,363.3,105.342s-10.613,6.091-17.384,4.086-9.54-13.565-9.54-13.565-5.553-15.153,3.222-19.92A137.735,137.735,0,0,1,352.357,69.589Z" fill="#f09491"/>
        </g>
        <g id="Group_3417" data-name="Group 3417">
          <rect id="Rectangle_3968" data-name="Rectangle 3968" width="33.608" height="33.973" rx="2.897" transform="translate(560.717 13.027)" fill="#fff" opacity="0.7"/>
          <rect id="Rectangle_3969" data-name="Rectangle 3969" width="23.468" height="3.766" transform="translate(565.787 26.789)" fill="#d0d0ce"/>
          <rect id="Rectangle_3970" data-name="Rectangle 3970" width="23.468" height="3.766" transform="translate(565.787 19.546)" fill="#d0d0ce"/>
          <rect id="Rectangle_3971" data-name="Rectangle 3971" width="15.355" height="3.766" transform="translate(565.787 34.032)" fill="#d0d0ce"/>
        </g>
        <g id="Group_3418" data-name="Group 3418">
          <rect id="Rectangle_3972" data-name="Rectangle 3972" width="46.226" height="46.729" rx="3.985" transform="translate(649.988 8.982)" fill="#f2c75c" opacity="0.7"/>
          <rect id="Rectangle_3973" data-name="Rectangle 3973" width="32.279" height="5.181" transform="translate(656.564 18.745)" fill="#d0d0ce"/>
          <rect id="Rectangle_3974" data-name="Rectangle 3974" width="21.121" height="5.181" transform="translate(656.564 28.708)" fill="#d0d0ce"/>
        </g>
        <rect id="Rectangle_3975" data-name="Rectangle 3975" width="26.301" height="5.181" transform="translate(502.163 69.911)" fill="#d0d0ce"/>
        <rect id="Rectangle_3976" data-name="Rectangle 3976" width="17.209" height="5.181" transform="translate(502.163 79.874)" fill="#d0d0ce"/>
        <g id="Group_3419" data-name="Group 3419">
          <rect id="Rectangle_3977" data-name="Rectangle 3977" width="57.161" height="57.783" rx="3.985" transform="translate(769.988 32.663)" fill="#fff"/>
          <rect id="Rectangle_3978" data-name="Rectangle 3978" width="45.828" height="5.181" transform="translate(776.164 51.193)" fill="#d0d0ce"/>
          <rect id="Rectangle_3979" data-name="Rectangle 3979" width="45.828" height="5.181" transform="translate(776.164 41.231)" fill="#d0d0ce"/>
          <rect id="Rectangle_3980" data-name="Rectangle 3980" width="29.986" height="5.181" transform="translate(776.164 61.156)" fill="#d0d0ce"/>
        </g>
      </g>
    </g>
  </g>
</svg>
PAMapp/assets/images/satisfaction/satisfactionBtn_mob.svg
PAMapp/assets/images/satisfaction/satisfactionBtn_web.svg
PAMapp/assets/images/taiwan-logo.png

PAMapp/assets/scss/_common.scss
@@ -91,4 +91,48 @@
        line-height: 40px;
        padding-right: 10px;
    }
}
}
.remind-container{
  margin-top: 13px;
  margin-bottom: 20px;
  display: flex;
      .remind-date{
          display: flex;
          flex-direction: column;
          align-items: center;
          font-weight: bold;
          width: 70px;
          border-radius: 6px;
          border-bottom: 1px solid #CCCCCC;
          border-right: 1px solid #CCCCCC;
          border-left: 1px solid #CCCCCC;
      }
      .remind-content-txt{
        display: flex;
        flex-direction: column;
        border: 1px solid #CCCCCC;
        flex:1;
        border-radius: 5px;
        padding: 10px;
      }
      .mb-3{
        margin-bottom: 3px;
      }
      .mt-2{
        margin-top:2px;
      }
      .date-year{
        color: #fff;
        align-items: center;
        display: flex;
        justify-content: center;
      }
      .bgc-primary-red{
        background-color:$PRIMARY_RED;
        width: 70.5px;
        border-top-left-radius:6px;
        border-top-right-radius:6px;
        border: 1px solid #CCCCCC;
      }
}
PAMapp/assets/scss/_variable.scss
@@ -10,6 +10,7 @@
$SKY_BLUE: #009CBD;
$LIGHT_BLUE: #8DB9CA;
$DARK_BLUE: #1B365D;
$LIGHT_RED: #DA3738;
$BEIGE: #A89968;
$PRUDENTIAL_GREY: #68737A;
$LIGHT_GREY: #D0D0CE;
PAMapp/assets/scss/utilities/_heading.scss
@@ -22,10 +22,6 @@
  font-weight: bold;
}
.lighter {
  font-weight: lighter;
}
.smTxt_bold {
  font-size: 16px;
  font-weight: bold;
@@ -39,12 +35,21 @@
  font-size: 14px;
}
.xxsTxt {
  font-size: 12px;
}
.text--bold {
  font-weight: bold !important;
}
.text--regular {
  font-weight: normal !important;
  font-weight: lighter;
}
.text--lighter {
  font-weight: lighter;
}
.text--center {
@@ -55,12 +60,24 @@
  text-align: left;
}
.text--right {
  text-align: right;
}
.text--primary {
  color: $PRIMARY_RED;
}
.text--black{
  color: $PRIMARY_BLACK;
}
.text--orange {
  color: $ORANGE;
}
.text--black{
  color: $PRIMARY_BLACK;
}
.text--dark-blue {
@@ -98,10 +115,22 @@
  cursor: pointer;
}
.text--center {
  text-align: center;
}
.text--underline {
  text-decoration: underline;
}
.pam-link-button {
  @extend .fix-chrome-click--issue;
  @extend .smTxt;
  @extend .text--bold;
  @extend .text--primary;
  @extend .cursor--pointer;
}
.pam-link-button--lg {
  @extend .fix-chrome-click--issue;
  @extend .mdTxt;
  @extend .text--bold;
  @extend .text--primary;
  @extend .cursor--pointer;
}
PAMapp/assets/scss/utilities/_icon.scss
@@ -9,3 +9,9 @@
      color: $YELLOW;
    }
}
.down-icon {
  color: #ED1B2E;
  font-size: 25px;
  align-self: center;
}
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;
}
@@ -30,6 +35,10 @@
.mb-10 {
  margin-bottom: 10px;
}
.ml-5 {
  margin-left: 5px;
}
.ml-10{
@@ -42,6 +51,10 @@
.mr-30{
  margin-right: 30px;
}
.my-10 {
  margin:10px 0;
}
.pt-30 {
@@ -95,3 +108,7 @@
    font-size: #{$fontSize} + 'px';
  }
}
.text--break-all {
  word-break: break-all
}
PAMapp/assets/scss/vendors/_elementUI.scss
@@ -13,3 +13,7 @@
@import './elementUI/messageBox';
@import './elementUI/input';
@import './elementUI/tree';
@import './elementUI/upload';
@import './elementUI/textarea';
@import './elementUI/dateTimePicker';
@import './elementUI/select';
PAMapp/assets/scss/vendors/elementUI/_dateTimePicker.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,85 @@
.pam-date.el-input,.pam-time.el-input {
    .el-input__inner {
        padding-left: 10px;
        height: 46px;
        border: 1px solid #707070;
    }
    .el-input__prefix {
        left:auto;
        right: 10px;
        top: 50%;
        transform: translateY(-50%);
        color: $PRIMARY_BLACK;
    }
    .el-input__suffix {
        right: 25px;
    }
}
.pam-date.el-input {
    width: 58%;
}
.pam-time.el-input {
    width: 38%;
}
.pam-time-popper {
    .time-select-item {
        font-size: 18px;
        &.selected {
            color: $CORAL;
        }
    }
}
.pam-date-popper {
    .el-date-picker__header {
        .el-picker-panel__icon-btn:hover, .el-date-picker__header-label:hover {
            color: $CORAL;
        }
    }
    .el-picker-panel__body {
        .el-picker-panel__content {
            .el-date-table {
                .el-date-table__row {
                    td.today span {
                        color: $CORAL;
                    }
                    td.current span {
                        background-color: $CORAL;
                    }
                    td.available:hover {
                        color: $CORAL;
                    }
                    td.today.current span {
                        color: $PRIMARY_WHITE;
                    }
                }
            }
            .el-year-table {
                td.current .cell {
                    color: $CORAL;
                }
                td.today .cell {
                    color: $CORAL;
                }
                td.available .cell:hover {
                    color: $CORAL;
                }
            }
            .el-month-table {
                td.current .cell {
                    color: $CORAL;
                }
                td.today .cell {
                    color: $CORAL;
                }
                td .cell:hover {
                    color: $CORAL;
                }
            }
        }
    }
}
PAMapp/assets/scss/vendors/elementUI/_dialog.scss
@@ -35,3 +35,38 @@
    }
  }
}
.pam-dialog-review {
  .review-content {
    display: flex;
    flex-direction: row;
    justify-content: space-evenly;
  }
  .review-text {
    width: 60%;
    line-height: 28px;
    @extend .p;
    @extend .text--lighter;
  }
  .review-score {
    display: flex;
    justify-content: center;
    margin-bottom: 30px;
  }
  .review-btn {
    display: flex;
    justify-content: center;
  }
}
.pam-dialog-reserved {
  .reserved-info {
    font-size: 20px;
    overflow-y:scroll;
    height: 400px;
  }
  .reserved-btn {
    display: flex;
    justify-content: center;
  }
}
PAMapp/assets/scss/vendors/elementUI/_input.scss
@@ -4,4 +4,30 @@
.input-radius > .el-input__inner {
  border-radius: 10px;
}
.pam-appointment-textarea {
  &.is-disabled {
    .el-textarea__inner {
      color: #222222;
      border: 1px solid #707070;
      &::placeholder {
        color: #A7A8AA;
      }
    }
  }
  .el-textarea__inner {
      border: 1px solid #707070;
      padding: 10px 20px;
      box-sizing: border-box;
      border-radius: 5px;
      font-size: 18px;
      &:focus {
          outline: none;
          border: solid 1px $SKY_BLUE;
      }
  }
}
PAMapp/assets/scss/vendors/elementUI/_rate.scss
@@ -1,6 +1,5 @@
.pam-quickFilter-rate {
.pam-rate {
  height: auto;
  margin-top: 30px;
  display: flex;
  justify-content: center;
  @extend .fix-chrome-click--issue;
@@ -40,3 +39,20 @@
    }
  }
}
.pam-satisfaction-rate {
  height: auto;
  display: flex;
  justify-content: center;
  margin-top: 10px;
  @extend .fix-chrome-click--issue;
  .el-rate__item {
    .el-rate__icon {
      font-size: 30px;
    }
    .el-icon-star-off {
      color: $PRIMARY_BLACK !important;
    }
  }
}
PAMapp/assets/scss/vendors/elementUI/_select.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,42 @@
.pam-select.el-select {
  width: 100%;
  .el-input {
    &__inner {
      padding-left: 20px;
      height: 46px;
      border: 1px solid #707070;
      border-radius: 5px;
      font-size: 20px;
    }
    &__icon {
      color: #ED1B2E;
      font-size: 25px;
      transform: none;
    }
  }
  .is-focus {
    .el-input__inner {
      border-color: #707070;
    }
  }
  .el-icon-arrow-up {
    font-family: 'icomoon' !important;
    speak: never;
    font-style: normal;
    font-weight: normal;
    font-variant: normal;
    text-transform: none;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    &:before {
      content: "\e910";
    }
  }
}
.el-select-dropdown__item {
  padding: 0 10px;
}
PAMapp/assets/scss/vendors/elementUI/_textarea.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,8 @@
.el-textarea__inner {
    border: 1px solid #707070;
    padding: 10px 20px;
    font-size: 20px;
    &::placeholder {
        font-size: 20px;
    }
}
PAMapp/assets/scss/vendors/elementUI/_upload.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,16 @@
.pam-avatar-uploader{
  display: flex;
  justify-content: center;
  margin-top: 10px;
  .el-upload{
    @extend .fix-chrome-click--issue;
    .pam-avatar-uploader--title{
      font-size: 20px;
      letter-spacing: 2px;
    }
    &:focus{
      border-color: $PRIMARY_BLACK;
      color: $PRIMARY_BLACK;
    }
  }
}
PAMapp/components/AddressPicker.vue
@@ -6,7 +6,7 @@
          class="p mt-10 cursor--pointer input-radius"
          tabindex="-1"
          v-model="keyWord"
          @change="searchDistrict"
          @input="searchDistrict"
          placeholder="請輸入關鍵字"
      ></el-input>
      <Ui-ScrollPicker
PAMapp/components/Appointment/AppointmentClosedInfo.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,75 @@
<template>
    <section class="close-appointment-detail">
        <div class="close-appointment-detail-nav">
          <div class="mdTxt">結案方式</div>
          <div class="mdTxt text--primary text--underline cursor--pointer" @click="editAppointmentHasClosed">編輯</div>
        </div>
        <span class="mt-10 mb-30">{{ displayClosedType }}</span>
        <template v-if="appointmentDetail.appointmentClosedInfo.policyholderIdentityId">
          <div class="mdTxt mb-10">保戶身份證字號</div>
          <div class="mb-30">{{ appointmentDetail.appointmentClosedInfo.policyholderIdentityId }}</div>
        </template>
        <template v-if="appointmentDetail.appointmentClosedInfo.planCode">
          <div class="mdTxt mb-10">商品代碼Plan Code</div>
          <div class="mb-30">{{ appointmentDetail.appointmentClosedInfo.planCode }}</div>
        </template>
        <template v-if="appointmentDetail.appointmentClosedInfo.closedReason">
          <div class="mdTxt mb-10">未成交原因</div>
          <div >{{ appointmentDetail.appointmentClosedInfo.closedReason | toFailReasonLabel }}</div>
          <div v-if="appointmentDetail.appointmentClosedInfo.closedOtherReason" class="mt-10">{{ appointmentDetail.appointmentClosedInfo.closedOtherReason }}</div>
          <div class="mb-30"></div>
        </template>
        <template v-if="appointmentDetail.appointmentClosedInfo.policyEntryDate">
          <div class="mdTxt mb-10">進件時間</div>
          <div class="mb-30">{{ appointmentDetail.appointmentClosedInfo.policyEntryDate | formatDate }}</div>
        </template>
        <div class="mdTxt mb-10">備註</div>
        <div>{{ appointmentDetail.appointmentClosedInfo.remark || '無' }}</div>
    </section>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator';
import { Appointment } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
@Component
export default class AppointmentClosedInfo extends Vue {
    @Prop()
    appointmentDetail!: Appointment;
    contactStatus = ContactStatus;
    //////////////////////////////////////////////////////////////////////
    editAppointmentHasClosed(): void{
      this.$router.push(`/appointment/${this.appointmentDetail.id}/close`);
    }
    get displayClosedType(): string {
      let closedType = '成交';
      switch (this.appointmentDetail.communicateStatus) {
        case this.contactStatus.CLOSE:
          closedType = '未成交';
          break;
        case this.contactStatus.CANCEL:
          closedType = '取消';
          break;
      }
      return closedType;
    }
}
</script>
<style lang="scss" scoped>
</style>
PAMapp/components/Appointment/AppointmentInterviewList.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,71 @@
<template>
    <div>
        <div class="interview__header">
            <div class="mdTxt">約訪紀錄</div>
            <div class="pam-link-button"
            @click="addInterview">+新增</div>
        </div>
        <InterviewCard :interviewList="displayList.slice(0, 3)"></InterviewCard>
        <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>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator';
import { InterviewRecord } from '~/shared/models/appointment.model';
@Component
export default class AppointmentInterviewList extends Vue {
  @Prop()
  interviewList!: InterviewRecord[];
  appointmentId!: string;
  displayList: InterviewRecord[] = [];
  //////////////////////////////////////////////////////////////////////
  mounted() {
      this.appointmentId = this.$route.params.appointmentId;
  }
  //////////////////////////////////////////////////////////////////////
  @Watch('interviewList', {immediate: true})
  updateInterviewList() {
      if (this.interviewList && this.interviewList.length > 0) {
          this.displayList = this.interviewList
            .map((i) => ({ ...i, sortDate: new Date(i.interviewDate)}))
            .sort((preItem, nextItem) => +nextItem.sortDate - +preItem.sortDate);
      }
  }
  //////////////////////////////////////////////////////////////////////
  addInterview(): void {
    this.$router.push(`/appointment/${this.appointmentId}/interview/new`);
  }
  readMoreBtn() {
      this.$router.push(`/appointment/${this.appointmentId}/interviewList`);
  }
}
</script>
<style lang="scss" scoped>
.interview__header {
  display        : flex;
  justify-content: space-between;
  margin-bottom  : 10px;
}
.interview-check-more{
  display: flex;
  justify-content: center;
}
</style>
PAMapp/components/Appointment/AppointmentProgress.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,119 @@
<template>
    <div class="appointment-progress">
      <div
        class="appointment-progress__indicator"
        :style="{ width: indicatorLineWidth }">
        <div class="line"></div>
        <div
          class="circle"
          v-for="(step, index) in stepList"
          :class="{ 'activate': index < displayCurrentStep }"
          :key="index">
        </div>
      </div>
      <div class="appointment-progress__status-label xxsTxt text--bold ml-5">
        {{ displayStatusLabel }}
      </div>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator';
@Component
export default class AppointmentProgress extends Vue {
  @Prop()
  totalStep?: number;
  @Prop()
  currentStep!: 'picked' | 'reserved' | 'contacted' | 'done' | 'closed' | 'cancel';
  //////////////////////////////////////////////////////////////////////
  get stepList(): number[] {
    const tempList: number[] = [];
    for(let i = 0; i < (this.totalStep || 3); i ++) {
      tempList.push(i);
    }
    return tempList;
  }
  get indicatorLineWidth(): string {
    const connectLineGutter = 10;
    return ((this.totalStep || 3) * 10 + connectLineGutter) + 'px';
  }
  get displayCurrentStep(): number {
    let step: number = 1;
    switch (this.currentStep) {
      case 'contacted':
        step = 2;
        break;
      case 'done':
        step = 3;
        break;
      case 'closed':
        step = 3;
        break;
      case 'cancel':
        step = 3;
        break;
    }
    return step;
  }
  get displayStatusLabel(): '未聯絡' | '約訪中' | '成交' | '未成交' | '已取消' {
    let label: '未聯絡' | '約訪中' | '成交' | '未成交' | '已取消' = '未聯絡';
    switch (this.currentStep) {
      case 'contacted':
        label = '約訪中';
        break;
      case 'done':
        label = '成交';
        break;
      case 'closed':
        label = '未成交';
        break;
      case 'cancel':
        label = '已取消';
        break;
    }
    return label;
  }
}
</script>
<style lang="scss" scoped>
.appointment-progress{
  display: flex;
  .appointment-progress__indicator {
    align-items    : center;
    display        : flex;
    justify-content: space-between;
    position       : relative;
    .circle {
      background-color: white;
      border          : 1px solid #CCCCCC;
      border-radius   : 50%;
      height          : 8px;
      margin          : 0;
      width           : 8px;
      z-index         : 5;
      &.activate {
        background-color: $BEIGE;
      }
    }
    .line {
      background-color: #707070;
      height          : 3px;
      left            : 5%;
      position        : absolute;
      width           : 90%;
    }
  }
}
</style>
PAMapp/components/Appointment/AppointmentRecordList.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,53 @@
<template>
    <div class="record-log-component">
        <div class="mdTxt mt-30 mb-10">系統通知紀錄</div>
            <InterviewRecordCard :noticeLogsList="displayLogs.slice(0, 3)"></InterviewRecordCard>
            <section class="text--center mt-30" v-if="displayLogs.length > 3">
                <div class="pam-link-button"
                    @click="readMoreBtn"
                >展開看更多
                <i class="icon-expand"></i></div>
            </section>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator';
import { NoticeLogs } from '~/shared/models/appointment.model';
@Component
export default class AppointmentRecordList extends Vue {
    @Prop()
    noticeLogs!: NoticeLogs[];
    appointmentId: string       = '';
    displayLogs  : NoticeLogs[] = [];
    //////////////////////////////////////////////////////////////////////
    mounted() {
        this.appointmentId = this.$route.params.appointmentId;
    }
    //////////////////////////////////////////////////////////////////////
    @Watch('noticeLogs', {immediate: true})
    onNoticeLogsChange() {
      if (this.noticeLogs.length) {
        this.displayLogs = this.noticeLogs
                            .map((i) => ({ ...i, sortDate: new Date(i.createdDate)}))
                            .sort((preItem, nextItem) => +nextItem.sortDate - +preItem.sortDate);
      }
    }
    //////////////////////////////////////////////////////////////////////
    readMoreBtn() {
        this.$router.push(`/appointment/${this.appointmentId}/recordList`);
    }
}
</script>
PAMapp/components/BackActionBar.vue
@@ -12,10 +12,10 @@
import { Vue, Component,} from 'vue-property-decorator';
import * as _ from 'lodash';
import { Role } from '~/shared/models/enum/Role';
const roleStorage = namespace('localStorage');
const appointmentStore = namespace('appointment.store');
const roleStorage      = namespace('localStorage');
@Component
export default class UiCarousel extends Vue {
@@ -23,11 +23,16 @@
  @roleStorage.Getter
  currentRole!:string;
  @appointmentStore.Getter
  isCloseAppointment!: boolean;
  //////////////////////////////////////////////////////////////////////
  goBack(): void {
    const pathName = this.$route.name;
    pathName?.includes('myConsultantList') ? this.$router.push('/') : this.$router.go(-1);
    pathName?.includes('myConsultantList')
      ? this.$router.push('/')
      : this.$router.go(-1);
  }
  get label(): string {
@@ -48,7 +53,10 @@
          featureLabel = '我的顧問清單';
          break;
        case 'agentInfo':
          featureLabel = _.isEqual(this.currentRole,Role.ADMIN) ? '查看帳號資訊' : '業務員資訊'
          const agentFeatureLabel = this.$route.name.includes('edit') ? '編輯帳號資訊' : '查看帳號資訊';
          featureLabel = _.isEqual(this.currentRole,Role.ADMIN)
                  ? agentFeatureLabel
                  : '業務員資訊'
          break;
        default:
          featureLabel = '回首頁';
@@ -72,6 +80,26 @@
        case 'faq':
          featureLabel = 'F&Q å¸¸è¦‹å•é¡Œ';
          break;
        case 'appointment':
          const appointmentFeatureLabel = this.$route.name.includes('close')
                                                            ? '結案'
                                                            : this.isCloseAppointment ? '結案明細' : '預約資訊';
          const inInterview = this.$route.name.includes('interview');
          const addNewInterview = this.$route.name.includes('new');
          const interviewList = this.$route.name.includes('interviewList');
          const recordList = this.$route.name.includes('recordList');
          if (interviewList) {
            featureLabel = '約訪紀錄';
          } else if (recordList) {
            featureLabel = '系統通知紀錄';
          } else if (inInterview) {
            featureLabel = addNewInterview
                  ? '新增約訪紀錄'
                  : '編輯約訪紀錄';
          } else {
            featureLabel = appointmentFeatureLabel;
          }
          break;
      }
      return featureLabel;
    } else {
PAMapp/components/Client/ClientCard.vue
@@ -6,12 +6,49 @@
            class="rowStyle cursor--pointer"
            justify="space-between"
            :class="{'new': newAppointment }"
            @click.native="openDetail"
            @click.native="viewAppointmentDetail"
        >
            <el-col :xs="1" :sm="1" class="unread" align="middle" v-if="isReserved">
        <div class="test">
            <div class="unread" v-if="isReserved">
                <div class="circle" v-if="!isRead"></div>
            </el-col>
            <el-col :xs="5" :sm="3" align="middle">
            </div>
            <div class="pl-10">
                <div class="smTxt_bold name">{{ client.name || 'NO NAME' }}</div>
                <div  v-if="client.communicateStatus === contactStatus.RESERVED" class="my-10 xsTxt">預約成功</div>
                <div
                  class="xsTxt mb-10 mt-10"
                  v-else-if="client.communicateStatus === contactStatus.CONTACTED">
                  ç´„訪紀錄
                </div>
                <div
                  class="xsTxt mb-10 mt-10"
                  v-else>
                  æ»¿æ„åº¦
                  <span v-if="client.satisfactionScore" class="xsTxt text--primary">
                    <UiReviewScore :score="client.satisfactionScore"></UiReviewScore>
                  </span>
                  <span v-else class="xsTxt text--mid_grey">未填</span>
                </div>
                <div class="professionals mb-10" v-if="client.communicateStatus === contactStatus.RESERVED">
                    <template v-if="client.requirement">
                        <span
                            v-for="(item, index) in requirements"
                            :key="index"
                            class="professionalsTxt"
                        >#{{item}}</span>
                    </template>
                    <template v-else>
                        <span class="professionalsTxt noProfessionalsTxt"
                        >(客戶未提供需求項目)</span>
                    </template>
                </div>
                <AppointmentProgress
                  :currentStep="client.communicateStatus"
                ></AppointmentProgress>
            </div>
        </div>
            <!-- <el-col :xs="5" :sm="3" align="middle">
                <el-avatar
                    :size="50"
                ></el-avatar>
@@ -25,53 +62,53 @@
                        <div class="unfilled">未填滿意度</div>
                    </template>
                </div>
            </el-col>
            <el-col :xs="14" :sm="14" class="pl-10">
                <div class="smTxt_bold name">{{ client.name }}</div>
                <div class="message">預約成功</div>
                <div class="professionals">
                    <template v-if="client.requirement">
                        <span
                            v-for="(item, index) in requirements"
                            :key="index"
                            class="professionalsTxt"
                        >#{{item}}</span>
                    </template>
                    <template v-else>
                        <span class="professionalsTxt noProfessionalsTxt"
                        >(客戶未提供需求項目)</span>
                    </template>
            </el-col> -->
            <div class="flex-column contactInfo" :xs="4" :sm="6">
                <div
                  class="invite-msg smTxt_bold"
                  @click.stop="showAddInterviewDialog"
                  v-if="client.communicateStatus === contactStatus.RESERVED">
                  å‚³é€ç´„訪通知
                </div>
            </el-col>
            <el-col class="flex-column contactInfo" :xs="4" :sm="6">
                <div
                    class="smTxt_bold fix-chrome-click--issue"
                    :class="{'unread-txt': reservedTxt === '未讀', 'read-txt': reservedTxt !== '未讀'}"
                >{{ reservedTxt }}</div>
                  class="invite-msg smTxt_bold"
                  @click.stop="navigateToCloseAppointment"
                  v-else-if="client.communicateStatus === contactStatus.CONTACTED">
                  çµæ¡ˆ
                </div>
                <div
                    class="date xsTxt text--mid_grey"
                  class="invite-msg smTxt_bold"
                  @click.stop="inviteReview"
                  v-else-if="!client.satisfactionScore">
                  ç™¼é€æ»¿æ„åº¦
                </div>
                <div
                    class="date xsTxt text--black"
                >{{ date }}</div>
                <div
                    class="xsTxt text--mid_grey"
                >{{ time }}</div>
            </el-col>
            </div>
        </el-row>
        <Ui-Dialog
            :isVisible.sync="isVisibleDialog"
            :isVisible.sync="isShowInformDialog"
            :width="dialogWidth"
            @closeDialog="closeDialog"
            @closeDialog="closeInformDialog"
            class="pam-myDemand-dialog"
        >
            <h5 class="subTitle text--center mb-30"
            >{{isReserved ? '預約資訊' : '已聯絡資訊'}}</h5>
            <p v-if='isReserved'
                class="smTxt text-right"
                class="smTxt text--right"
            ><span v-if="isRead">{{client.consultantReadTime | formatDate}}</span> å·²è®€</p>
            <p
                v-if="!isReserved"
                class="smTxt text-right"
                class="smTxt text--right"
            >{{client.contactTime | formatDate}} è¯çµ¡</p>
            <p class="smTxt">{{client.appointmentDate | formatDate}} é ç´„</p>
@@ -86,25 +123,67 @@
                <p v-for="(item, index) in hopeContactTime"
                    :key="index"
                >連絡時段{{index + 1 | formatNumber}}:<span>{{ item | formatHopeContactTime}}</span></p>
            </div>
            <div class="mt-30">
                <div class="memoTitleStyle">
                    <div class="mdTxt">內容描述</div>
                    <div
                        class="smTxt text--bold text--primary cursor--pointer text--underline edit"
                        @click='editMemo'
                    >編輯</div>
                </div>
                <div class="mt-30 text--center" v-if="isReserved">
                    <el-button @click="markAppointment">標註為已連絡</el-button>
                <el-input
                    class="mt-10 pam-appointment-textarea"
                    type="textarea"
                    :rows="3"
                    maxlength="100"
                    placeholder="請輸入,限100字。"
                    :disabled="!isEdit"
                    v-model="memo"
                >
                </el-input>
                <div class="mt-10 smTxt text--bold text--primary text--right fixed-Height">
                    <template v-if="isEdit">
                        <span class="cursor--pointer" @click="cancelEditMemo">取消</span>
                        <span class="pl-20 cursor--pointer" @click="saveMemo">儲存</span>
                    </template>
                </div>
            </div>
            <div class="mt-30 text--center" v-if="isReserved">
                <el-button @click="markAppointment">標註為已連絡</el-button>
            </div>
        </Ui-Dialog>
        <InterviewMsg
          :client="client"
          :isVisible.sync="isShowAddInterviewDialog">
        </InterviewMsg>
        <PopUpFrame :isOpen.sync="isShowInviteReviewDialog">
          <div class="text--middle invite-review">
            <div class="mb-30 mt-10">已發送滿意度</div>
            <div class="text--primary text--middle cursor--pointer text--underline" @click="isShowInviteReviewDialog = false" :size="'250px'">我知道了</div>
          </div>
        </PopUpFrame>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Action, namespace, Watch } from 'nuxt-property-decorator';
import { Vue, Component, Prop, namespace, Watch } from 'nuxt-property-decorator';
import appointmentService from '~/shared/services/appointment.service';
import myConsultantService from '~/shared/services/my-consultant.service';
import UtilsService from '~/shared/services/utils.service';
import { hideReviews } from '~/shared/const/hide-reviews';
import { ClientInfo } from '~/shared/models/client.model';
import myConsultantService from '~/shared/services/my-consultant.service';
import { ElRow } from 'element-ui/types/row';
import { Appointment, AppointmentMemoInfo } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
import reviewsService from '~/shared/services/reviews.service';
const localStorage = namespace('localStorage');
const appointmentStore = namespace('appointment.store');
const localStorage     = namespace('localStorage');
@Component({
    filters: {
        formatNumber(index: number) {
@@ -125,18 +204,39 @@
    }
})
export default class ClientList extends Vue {
    @Action
    updateMyAppointment!: (data: ClientInfo) => void;
    @Prop()
    client!: ClientInfo;
    client!: Appointment;
    @appointmentStore.Action
    updateMyAppointmentList!: (data: Appointment) => void;
    @appointmentStore.Action
    getAppointmentDetail!: (appointmentId: number) => Promise<Appointment>;
    @appointmentStore.Action
    updateAppointmentDetail!: (id: number) => Appointment;
    @appointmentStore.Getter
    appointmentProgress!: ContactStatus;
    @localStorage.Mutation
    storageClearAppointmentIdFromMsg!: () => void;
    isVisibleDialog = false;
    dialogWidth = '';
    hideReviews = hideReviews;
    contactStatus            = ContactStatus;
    dialogWidth              = '';
    hideReviews              = hideReviews;
    isEdit                   = false;
    isShowAddInterviewDialog = false;
    isShowInformDialog       = false;
    isShowInviteReviewDialog = false;
    memo                     = '';
    memoInfo: AppointmentMemoInfo = {
        appointmentId: 0,
        content      : '',
        id           : 0
    };
    //////////////////////////////////////////////////////////////////////
@@ -149,6 +249,125 @@
    }
    //////////////////////////////////////////////////////////////////////
    mounted() {
        this.memoInfo = this.client.appointmentMemoList.length > 0
            ? JSON.parse(JSON.stringify(this.client.appointmentMemoList[0]))
            : {appointmentId: 0, content: '', id: 0};
        this.memo = this.memoInfo.content;
    }
    //////////////////////////////////////////////////////////////////////
    viewAppointmentDetail(): void {
      this.getAppointmentDetail(this.client.id).then((_) => {
        const unread = !this.client.consultantReadTime;
        if (unread) {
          this.readAppointment();
        }
        this.$router.push(`/appointment/${this.client.id}`);
      });
    }
    showAddInterviewDialog(): void {
      this.isShowAddInterviewDialog = true;
    }
    navigateToCloseAppointment(): void {
      this.getAppointmentDetail(this.client.id).then((_) => {
        this.$router.push(`/appointment/${this.client.id}/close`);
      });
    }
    inviteReview(): void {
        reviewsService.sendSatisfactionToClient(this.client.id).then(res => {
            this.isShowInviteReviewDialog = true ;
        })
    }
    openDetail() {
        setTimeout(() => {
            (this.$refs.clientCardRef as any).$el.classList.add('currentShowStyle');
        }, 0)
        this.dialogWidth = UtilsService.isMobileDevice() ? '80%' : '';
        this.isShowInformDialog = true;
    }
    markAppointment() {
        myConsultantService.markAsContact(this.client.id).then(data => {
            this.updateMyAppointmentList(data);
            this.isShowInformDialog = false;
        })
    }
    closeInformDialog(): void {
        this.readAppointment();
        this.isEdit = false;
        this.clearAppointmentIdFromMsg();
    }
    private readAppointment(): void {
      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() {
        this.storageClearAppointmentIdFromMsg();
        this.$router.push({query: {}});
        setTimeout(() => {
            (this.$refs.clientCardRef as ElRow).$el.classList.remove('currentShowStyle')
        },1000)
    }
    saveMemo() {
        if (this.client.appointmentMemoList.length > 0) {
            const params = {
                content: this.memo,
                id: this.client.appointmentMemoList[0].id
            };
            this.updateMemo(params);
            return;
        }
        const params = {
            content: this.memo,
            appointmentId: this.client.id,
        }
        this.createMemo(params);
    }
    private createMemo(params) {
        appointmentService.createMemo(params).then(memoRes => {
            this.storeMemo(memoRes);
        });
    }
    private updateMemo(params) {
        appointmentService.updateMemo(params).then(memoRes => {
            this.storeMemo(memoRes);
        });
    }
    private storeMemo(memoRes) {
        this.memoInfo = memoRes;
        this.memo = this.memoInfo.content;
        this.client.appointmentMemoList[0] = this.memoInfo;
        this.isEdit = false;
    }
    editMemo() {
        this.isEdit = !this.isEdit;
        this.memo = this.memoInfo.content;
    }
    cancelEditMemo() {
        this.isEdit = false;
        this.memo = this.memoInfo.content;
    }
    get newAppointment(): boolean {
      return !this.client.consultantViewTime
@@ -195,55 +414,14 @@
        }
    }
    get time() {
        const formatDate = (this.$options.filters as any).formatDate(this.displayTime);
        return formatDate.split(' ')[1]
    }
    get date() {
        const formatDate = (this.$options.filters as any).formatDate(this.displayTime);
        return formatDate.split(' ')[0];
    }
    openDetail() {
        setTimeout(() => {
            (this.$refs.clientCardRef as any).$el.classList.add('currentShowStyle');
        }, 0)
        this.dialogWidth = UtilsService.isMobileDevice() ? '80%' : '';
        this.isVisibleDialog = true;
    }
    markAppointment() {
        myConsultantService.markAsContact(this.client.id).then(data => {
            // TODO: è¦æŽ¥å¾Œå°å‚³å›žçš„ updated client è³‡æ–™ - Ben 2021/11/16
            const updatedClient = {...this.client};
            updatedClient.communicateStatus = 'contacted';
            updatedClient.contactTime = new Date().toString();
            this.updateMyAppointment(updatedClient);
            this.isVisibleDialog = false;
        })
    }
    closeDialog(): void {
      const unread = !this.client.consultantReadTime;
        if (unread) {
            appointmentService.recordRead(this.client.id).then((_) => {
                const updatedClient = {...this.client};
                updatedClient.consultantReadTime = new Date().toString();
                this.updateMyAppointment(updatedClient);
            });
        };
        this.clearAppointmentIdFromMsg();
    }
    private clearAppointmentIdFromMsg() {
        this.storageClearAppointmentIdFromMsg();
        this.$router.push({query: {}});
        setTimeout(() => {
            (this.$refs.clientCardRef as ElRow).$el.classList.remove('currentShowStyle')
        },1000)
    get time() {
        const formatDate = (this.$options.filters as any).formatDate(this.displayTime);
        return formatDate.split(' ')[1]
    }
}
@@ -251,99 +429,104 @@
<style lang="scss" scoped>
    .rowStyle {
        padding: 10px 15px 10px 5px;
        background-color: $PRIMARY_WHITE;
        margin-bottom: 10px;
        display: flex;
        justify-content: space-between;
        border-left     : solid 4px transparent;
        display         : flex;
        justify-content : space-between;
        margin-bottom   : 10px;
        padding         : 10px 15px 10px 5px;
        transition: background-color 0.5s;
        &.new {
            border-left: solid 4px $YELLOW;
            border-color: $YELLOW;
        }
        &.currentShowStyle {
            background-color: rgba(236, 195, 178, 0.5);
            transition: background-color 0.5s;
            transition      : background-color 0.5s;
        }
        .unread {
            align-self: center;
            .circle {
                width: 10px;
                height: 10px;
                border-radius: 50px;
                background-color: $PRIMARY_RED;
                margin: auto;
                border-radius   : 50%;
                height          : 10px;
                margin          : auto;
                width           : 10px;
            }
        }
        .satisfaction {
            font-size: 12px;
            font-size  : 12px;
            font-weight: bold;
            margin-top: 5px;
            margin-top : 5px;
            .unfilled {
                color      : $MID_GREY;
                font-weight: lighter;
                color: $MID_GREY;
            }
        }
        .message {
            margin:10px 0;
        }
        .professionals {
            overflow: hidden;
            white-space: nowrap;
            overflow     : hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-box-orient: vertical;
            -webkit-line-clamp: 1;
            .professionalsTxt {
                font-size: 12px;
                font-weight: bold;
                font-size   : 12px;
                margin-right: 5px;
            }
            }
            .noProfessionalsTxt {
                color: $PRUDENTIAL_GREY;
                color      : $PRUDENTIAL_GREY;
                font-weight: lighter;
            }
        }
        .contactInfo {
            text-align: right;
            .date {
                font-size: 12px;
            }
        }
        .unread-txt {
            @extend .text--primary;
        }
        .read-txt {
            color: $SKY_BLUE;
        }
    }
    .flex-column {
        display: flex;
        flex-direction: column;
        display        : flex;
        flex-direction : column;
        justify-content: space-between;
    }
    .dialogTxt {
        font-size: 20px;
        overflow-y:scroll;
        max-height: 45vh;
        font-size : 20px;
        max-height: 25vh;
        overflow-y: scroll;
        @include desktop {
            height: 400px;
        }
    }
    .text-right {
        text-align: right;
    .memoTitleStyle {
        display        : flex;
        flex-direction : row;
        justify-content: space-between;
       .edit {
            align-self: flex-end;
        }
    }
    .fixed-Height {
        height: 16px;
    }
    .test{
        display: flex;
    }
    .invite-msg{
      width: 96px;
      color: $PRIMARY_RED;
      @extend .text--underline;
    }
  .invite-review{
    align-items   : center;
    display       : flex;
    flex-direction: column;
  }
</style>
PAMapp/components/Client/ClientList.vue
@@ -18,19 +18,24 @@
<script lang='ts'>
import { Vue, Component, Prop } from 'nuxt-property-decorator';
import { ClientInfo } from '~/shared/models/client.model';
import { Appointment } from '~/shared/models/appointment.model';
@Component
export default class ClientList extends Vue {
    @Prop() clients!: ClientInfo[];
    @Prop() clients!: Appointment[];
    @Prop() title!: string;
    //////////////////////////////////////////////////////////////////////
    get noDataPlaceholder(): string {
      return this.title === 'reservedList'
                          ? '您目前無已預約客戶'
                          : '您目前無已聯絡客戶';
      let noDataWording = '您目前無已結案的預約單';
      if (this.title === 'contactedList') {
        noDataWording = '您目前無約訪中的預約單';
      }
      if (this.title === 'reservedList') {
        noDataWording = '您目前無未聯絡的預約單';
      }
      return noDataWording;
    }
}
</script>
PAMapp/components/Consultant/ConsultantCard.vue
@@ -1,70 +1,64 @@
<template>
    <div>
        <el-row type="flex" class="rowStyle" :class="{'new': !agentInfo.customerViewTime }">
            <el-col :xs="2" :sm="1"></el-col>
            <el-col :xs="22" :sm="23">
                <el-row type="flex">
                    <el-col class="flex_column" :xs="5" :sm="3">
                        <UiAvatar
                            :size="50"
                            :fileName="avatarFileName"
                            @click.native="showAgentDetail(agentInfo.agentNo)"
                        ></UiAvatar>
                        <!-- TODO:隱藏滿意度 -->
                        <div v-if="!hideReviews">
                            <i class="icon-star pam-icon icon--yellow satisfaction"  v-if="notScoreAppointmentYet"></i>
                            <span v-if="notScoreAppointmentYet">
                                {{ agentInfo.satisfactionScore }}
                            </span>
                            <div class="unfilled text--center "
                                style="display:flex"
                                v-if="notScoreAppointmentYet">未填<br />滿意度</div>
                            <span v-if="agentInfo.contactStatus !== 'contacted'">{{ agentInfo.avgScore }}</span>
                        </div>
                    </el-col>
                    <el-col :xs="10" :sm="15">
                        <div class="smTxt_bold name">{{agentInfo.name}}</div>
                        <div class="professionals">
                            <span
                                class="professionalsTxt"
                                v-for="(expertise, index) in agentInfo.expertise"
                                :key="index"
                            >#{{expertise}}</span>
                        </div>
                        <div
                            class="delete"
                            v-if="showRemoveBtn"
                            @click="removeAgent"
                        >移除</div>
                    </el-col>
                    <el-col class="flex_column" :xs="9" :sm="6">
                        <el-button
                            class="smTxt_bold outline_btn"
                            @click="reserveCommunication"
                            :class="actionBtnStyle"
                        >{{ actionBtnLabel }}</el-button>
                        <div
                          v-if="notScoreAppointmentYet"
                          class="text--primary mt-10 text--center text--underline cursor--pointer"
                          @click="reviewsBtn = true">給予滿意度評分</div>
                        <div class="updateTime mt-10">
                            {{ agentInfo.updateTime | formatDate }}
                        </div>
                    </el-col>
                </el-row>
            <el-col class="flex_column pl-5" :xs="5" :sm="3">
                <UiAvatar
                    :size="50"
                    :agentNo="agentInfo.agentNo"
                    @click.native="showAgentDetail(agentInfo.agentNo)"
                ></UiAvatar>
                <!-- TODO:隱藏滿意度 -->
                <div v-if="!hideReviews">
                    <i class="icon-star pam-icon icon--yellow satisfaction"  v-if="notScoreAppointmentYet"></i>
                    <span v-if="notScoreAppointmentYet">
                        {{ agentInfo.satisfactionScore }}
                    </span>
                    <div class="unfilled text--center "
                        style="display:flex"
                        v-if="notScoreAppointmentYet">未填<br />滿意度</div>
                    <span v-if="agentInfo.contactStatus !== 'contacted'">{{ agentInfo.avgScore }}</span>
                </div>
            </el-col>
            <el-col :xs="10" :sm="15">
                <div class="smTxt_bold name">{{agentInfo.name}}</div>
                <div class="professionals">
                    <span
                        class="professionalsTxt"
                        v-for="(expertise, index) in agentInfo.expertise"
                        :key="index"
                    >#{{expertise}}</span>
                </div>
                <div
                    class="delete"
                    v-if="showRemoveBtn"
                    @click="isRemoveAgentPopup = true"
                >移除</div>
                <div
                    v-if="notScoreAppointmentYet"
                    class="text--primary text--underline cursor--pointer xsTxt text--bold"
                    @click="reviewsBtn = true">給予滿意度評分</div>
            </el-col>
            <el-col class="flex_column" :xs="9" :sm="6">
                <el-button
                    class="smTxt_bold outline_btn"
                    @click="reservedOrShowAppointmentInfo"
                    :class="actionBtnStyle"
                >{{ actionBtnLabel }}</el-button>
                <div class="updateTime mt-10">
                    {{ agentInfo.updateTime | formatDate }}
                </div>
            </el-col>
        </el-row>
        <Ui-Dialog
            :isVisible.sync="isVisibleDialog"
            :width="width"
            class="pam-myDemand-dialog"
            class="pam-myDemand-dialog pam-dialog-reserved"
        >
            <div v-if="appointmentDetail">
                <h5 class="subTitle text--center mb-30">預約成功</h5>
                <p class="smTxt">{{appointmentDetail.appointmentDate | formatDate}}</p>
                <div class="dialogInfo">
                <div class="reserved-info">
                    <p>姓名:{{appointmentDetail.name}}</p>
                    <p>電話:{{appointmentDetail.phone}}</p>
                    <p>Email:{{appointmentDetail.email}}</p>
@@ -86,7 +80,7 @@
                    </div>
                </div>
                <div v-if="notScoreAppointmentYet" class="dialogInfo-btn">
                <div v-if="notScoreAppointmentYet" class="reserved-btn">
                    <el-button type="primary"
                        @click.native="reviewsBtn = true">給予滿意度評分</el-button>
                </div>
@@ -97,14 +91,24 @@
                </div>
            </div>
        </Ui-Dialog>
        <PopUpFrame :isOpen.sync="reviewsBtn">
            <div class="mdTxt">
        <PopUpFrame :isOpen.sync="reviewsBtn" class="reviewDialog-content">
            <div class="mdTxt pam-dialog-review">
                ä¿éšªé¡§å•æ»¿æ„åº¦
                <span class="hint">選取星星</span>
                <div class="dialogInfo-score">
                    <el-rate v-model="inputScore" class="pam-quickFilter-rate"></el-rate>
                <div class="mt-30 review-content">
                    <UiAvatar :size="80" :agentNo="agentInfo.agentNo"></UiAvatar>
                    <div class="review-text">對於顧問
                        <span class="text--primary">{{agentInfo.name}}</span>
                        çš„æ•´é«”服務,您給予幾顆星評價?
                    </div>
                </div>
                <div class="dialogInfo-btn">
                <div class="review-score">
                    <el-rate v-model="inputScore" class="pam-rate mt-30"></el-rate>
                </div>
                <div class="review-btn">
                    <el-button
                        type="primary"
                        :disabled="!inputScore"
@@ -121,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>
@@ -177,7 +189,8 @@
    isCancelPopup = false;
    hideReviews = hideReviews;
    isConfirmPopup = false;
    isRemoveAgentPopup = false;
    confirmTxt = '';
    appointmentDetail: any = {
        age               : '',
@@ -205,16 +218,16 @@
    get notScoreAppointmentYet(): boolean {
      const isAppointment = !!this.agentInfo['appointmentStatus'];
      if (!isAppointment) return false;
      return !this.agentInfo['appointmentScore'];
      return this.agentInfo['appointmentStatus'] !== 'contacted' ? !this.agentInfo['appointmentScore'] : false;
    }
    get isAppointment(): boolean {
      return !!this.agentInfo['appointmentStatus'];;
      return !!this.agentInfo['appointmentStatus'];
    }
    get latestReservedAppointment(): Appointment {
    get latestNotClosedAppointment(): Appointment {
        return this.agentInfo.appointments!
                .filter((appointment) => appointment.communicateStatus !== 'contacted')
                .filter((appointment) => appointment.communicateStatus === 'reserved' || appointment.communicateStatus === 'contacted')
                .map((reversedAppointment) => {
                    return {
                    ...reversedAppointment,
@@ -252,6 +265,12 @@
        if (this.agentInfo['appointmentStatus'] === 'reserved') {
            return '已預約';
        }
        if (this.agentInfo['appointmentStatus'] === 'done') {
            return '已成交';
        }
        if (this.agentInfo['appointmentStatus'] === 'closed') {
            return '未成交';
        }
      } else {
        if (this.agentInfo.contactStatus === 'contacted') {
            return '已聯絡';
@@ -271,6 +290,12 @@
        }
        if (this.agentInfo['appointmentStatus'] === 'reserved') {
            return 'reservedBtn';
        }
        if (this.agentInfo['appointmentStatus'] === 'done') {
            return 'doneBtn';
        }
        if (this.agentInfo['appointmentStatus'] === 'closed') {
            return 'closedBtn';
        }
      } else {
        if (this.agentInfo.contactStatus === 'contacted') {
@@ -296,7 +321,7 @@
    @Action
    storeConsultantList!: () => void;
    reserveCommunication() {
    reservedOrShowAppointmentInfo() {
      const isAppointment = !!this.agentInfo['appointmentStatus'];
      const contactStatus = this.agentInfo.contactStatus;
        if (!isAppointment && (!contactStatus || contactStatus === 'picked')) {
@@ -310,7 +335,7 @@
      const isAppointment = !!this.agentInfo['appointmentStatus'];
      const appointmentId = isAppointment
                          ? this.agentInfo['appointmentId']
                          : this.latestReservedAppointment.id;
                          : this.latestNotClosedAppointment.id;
        appointmentService.getAppointmentDetail(appointmentId!).then(res => {
            this.appointmentDetail = {
@@ -324,6 +349,12 @@
    removeAgent() {
        this.removeFromMyConsultantList(this.agentInfo.agentNo).then((removeOk) => {
            this.isRemoveAgentPopup = false;
            setTimeout(() => {
                this.confirmTxt = '移除顧問';
                this.isConfirmPopup = true;
            }, 300);
        });
    }
@@ -335,7 +366,7 @@
      const isAppointment = !!this.agentInfo['appointmentStatus'];
      const appointmentId = isAppointment
                          ? this.agentInfo['appointmentId']
                          : this.latestReservedAppointment.id;
                          : this.latestNotClosedAppointment.id;
        const reviewParams: UserReviewsConsultantsParams = {
            appointmentId: appointmentId,
@@ -350,12 +381,14 @@
    }
    cancel() {
        appointmentService.cancelAppointment(this.latestReservedAppointment.id).then(res => {
        appointmentService.cancelAppointment(this.latestNotClosedAppointment.id).then(res => {
            this.storeConsultantList();
            this.isVisibleDialog = false;
            this.isCancelPopup = false;
            setTimeout(() => {
                this.confirmTxt = '取消此筆預約';
                this.isConfirmPopup = true;
            }, 300);
        });
    }
@@ -407,6 +440,7 @@
        }
        .delete {
            display: inline-block;
            color: $PRIMARY_RED;
            font-size: 14px;
            font-weight: bold;
@@ -435,6 +469,28 @@
            }
        }
        .doneBtn {
            color: $BEIGE;
            border-color: $BEIGE;
            &:focus {
                color: $PRIMARY_WHITE;
                background-color: $BEIGE;
                opacity: 0.5;
            }
        }
        .closedBtn {
            color: $PRUDENTIAL_GREY;
            border-color: $PRUDENTIAL_GREY;
            &:focus {
                color: $PRIMARY_WHITE;
                background-color: $PRUDENTIAL_GREY;
                opacity: 0.5;
            }
        }
        .updateTime {
            font-size: 12px;
            font-weight: bold;
@@ -448,20 +504,4 @@
        flex-direction: column;
        justify-content: space-between;
    }
    .dialogInfo {
        font-size: 20px;
        overflow-y:scroll;
        height: 400px;
    }
    .dialogInfo-btn{
        display: flex;
        justify-content: center;
    }
    .dialogInfo-score{
        display: flex;
        justify-content: center;
        margin-bottom: 50px;
    }
</style>
PAMapp/components/Consultant/ConsultantSwiper.vue
@@ -10,7 +10,12 @@
          :key="index"
        >
            <div class="consultantCardStyle" >
              <UiAvatar :size="80" :fileName="agentInfo.img" class="mb-10"></UiAvatar>
              <UiAvatar
                class="mb-10"
                :size="80"
                :agentNo="agentInfo.agentNo"
              >
              </UiAvatar>
              <div class="name">{{agentInfo.name}}</div>
              <div v-if="!hideReviews">
                <!-- TODO:隱藏滿意度 -->
@@ -20,8 +25,12 @@
          </div>
        </swiper-slide>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
        <div class="swiper-button-prev" slot="button-prev">
          <i class="icon-left"></i>
        </div>
        <div class="swiper-button-next" slot="button-next">
          <i class="icon-right"></i>
        </div>
    </swiper>
</div>
</template>
@@ -88,9 +97,12 @@
    height: 100%;
    &:after {
      display: none;
    }
    .icon-right,.icon-left {
      font-size: 20px;
      font-weight: bold;
      color: #707A81;
      color: $CORAL;
    }
    &.swiper-button-disabled {
PAMapp/components/DateTimePicker.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,65 @@
<template>
    <div class="dateTime">
        <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>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator";
@Component
export default class DateTimePicker extends Vue {
    changeDate: Date | string = '';
    changeTime!: string;
    @Prop()
    defaultValue!: string;
    @Prop()
    isPastDateDisabled!: boolean;
    @Prop()
    isFutureDateDisabled!: boolean;
    @Emit('changeDateTime')
    changeDateTime(event, type) {
        if (type === 'date') {
            this.changeDate = event;
        }
        if (type === 'time') {
            this.changeTime = event;
        }
        if (this.changeDate && this.changeTime) {
            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>
<style lang="scss" scoped>
.dateTime {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    font-size: 20px;
}
</style>
PAMapp/components/Interview/InterviewAdd.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,266 @@
<template>
  <div class="edit-appointment-record">
      <div class="edit-appointment-record-date" v-if="interviewId && interviewRecord">
          <span>{{interviewRecord.createdDate | formatDate}} å»ºç«‹</span>
          <span>{{interviewRecord.lastModifiedDate | formatDate}} æ›´æ–°</span>
      </div>
      <el-row class="mdTxt mb-10">
          <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  cursor--pointer"
                @click="showCancelPopUp = true"
              ><i class="icon-delet"></i></span>
              <span
                v-if="!isEdit"
                class="text--primary  cursor--pointer"
                @click="isEdit = !isEdit"
              ><i class="icon-edit"></i></span>
          </el-col>
      </el-row>
      <template v-if="!interviewId || isEdit">
          <DateTimePicker
            @changeDateTime="interviewTime = $event"
            :defaultValue="defaultValue"
          ></DateTimePicker>
      </template>
      <template v-else>
          <div class="fs-20 mt-20">
              {{formatInterviewDate}}
          </div>
      </template>
      <div class="mdTxt mb-10 mt-30" :class="{'required': !interviewId || isEdit}">約訪紀錄</div>
      <template v-if="!interviewId || isEdit">
          <el-input
            type="textarea"
            :rows="5"
            placeholder="請輸入約訪紀錄"
            resize="none"
            v-model="content"
        >
        </el-input>
      </template>
      <template v-else>
          <div class="fs-20 mt-20">
              {{content}}
          </div>
      </template>
      <div class="edit-appointment-record-btn" v-if="!interviewId || isEdit">
          <el-button @click="cancel">取消</el-button>
          <el-button
            :disabled="!interviewTime || !content"
            @click="saveInterviewRecord"
          >確定</el-button>
      </div>
      <PopUpFrame :isOpen.sync="showCancelPopUp"
         @closePopUp="showCancelPopUp = false"
      >
        <div class="text--center mdTxt">是否刪除此筆約訪記錄?</div>
        <div class="text--center mt-30">
            <el-button @click="showCancelPopUp = false">否</el-button>
            <el-button @click="deleteInterviewRecord" type="primary">是</el-button>
        </div>
      </PopUpFrame>
      <PopUpFrame :isOpen.sync="showConfirmPopup"
        @closePopUp="closePopup">
        <div class="text--center mdTxt">{{confirmTxt}}!</div>
        <div class="text--center mt-30">
            <el-button @click="closePopup" type="primary">確定</el-button>
        </div>
      </PopUpFrame>
      <PopUpFrame :isOpen.sync="showFutureDateConfirmPopup"
        @closePopUp="closePopup">
        <div class="text--center mdTxt">{{confirmTxt}}!</div>
        <div class="text--center mdTxt">立即發送約訪通知?</div>
        <div class="text--center mt-30" style="display:flex">
            <el-button @click="closePopup">先不發送</el-button>
            <el-button @click="showInterviewMsgPopup = true" type="primary">傳送約訪通知</el-button>
        </div>
      </PopUpFrame>
      <InterviewMsg
        :isVisible.sync="showInterviewMsgPopup"
        :client="appointmentDetail"
        :defaultValue="interviewTime"
        @closeDialog="closePopup"
      ></InterviewMsg>
  </div>
</template>
<script lang="ts">
import { Appointment, InterviewRecord, InterviewRecordInfo } from '~/shared/models/appointment.model';
import { Vue, Component, Watch, namespace } from 'nuxt-property-decorator';
import appointmentService from '~/shared/services/appointment.service';
const appointmentStore = namespace('appointment.store');
@Component
export default class InterviewAdd extends Vue {
    @appointmentStore.State
    appointmentDetail!: Appointment;
    @appointmentStore.Action
    updateAppointmentDetail!: (id: number) => Appointment;
    interviewTime = '';
    content = '';
    interviewId = '';
    appointmentId = '';
    confirmTxt: '新增成功' | '編輯成功' | '刪除成功' = '新增成功';
    isEdit = false;
    showConfirmPopup = false;
    showCancelPopUp = false;
    showInterviewMsgPopup = false;
    showFutureDateConfirmPopup = false;
    defaultValue = '';
    interviewRecord!: InterviewRecord;
    ////////////////////////////////////////////////////////////////////
    mounted() {
        this.interviewId = this.$route.params.interviewId;
        this.appointmentId = this.$route.params.appointmentId;
        this.onAppointmentDetailChange();
    }
    ////////////////////////////////////////////////////////////////////
    @Watch('appointmentDetail', {immediate: true})
    onAppointmentDetailChange() {
        if (this.appointmentDetail && this.appointmentDetail.id === +this.appointmentId) {
            this.interviewRecord = this.appointmentDetail.interviewRecordDTOs
                .filter(item => item.id === +this.interviewId)[0];
                if (this.interviewRecord && this.interviewId) {
                    this.content = this.interviewRecord.content;
                    this.defaultValue = this.interviewRecord.interviewDate;
                }
        }
    }
    ////////////////////////////////////////////////////////////////////
    saveInterviewRecord() {
        const interviewRecordInfo: InterviewRecordInfo = {
            content: this.content,
            interviewDate: this.interviewTime,
            appointmentId: +this.appointmentId
        };
        if (!this.interviewId) {
            this.createdRecord(interviewRecordInfo);
        } else {
            const updateInterviewRecord = {
                ...interviewRecordInfo,
                id: +this.interviewId
            }
            this.updateRecord(updateInterviewRecord);
        }
    }
    private createdRecord(interviewRecordInfo) {
        appointmentService.createInterviewRecord(interviewRecordInfo).then(res => {
            this.showPopUp('新增成功');
        });
    }
    private updateRecord(updateInterviewRecord) {
        appointmentService.updateInterviewRecord(updateInterviewRecord).then(res => {
            this.showPopUp('編輯成功');
        });
    }
    private showPopUp(confirmTxt) {
        this.confirmTxt = confirmTxt;
        this.updateAppointmentDetail(+this.appointmentId);
        if (new Date(this.interviewTime).getTime() >= new Date().getTime()) {
            this.showFutureDateConfirmPopup = true;
        } else {
            this.showConfirmPopup = true;
        }
    }
    deleteInterviewRecord() {
        appointmentService.deleteInterviewRecord(this.interviewId).then(res => {
            this.confirmTxt = '刪除成功';
            this.showConfirmPopup = true;
            this.updateAppointmentDetail(+this.appointmentId);
        });
    }
    cancel() {
        if (this.interviewId) {
           this.content = this.interviewRecord.content;
           this.defaultValue = this.interviewRecord.interviewDate;
           this.isEdit = false;
        } else {
           this.$router.go(-1);
        }
    }
    closePopup() {
        this.$router.go(-1);
    }
    ////////////////////////////////////////////////////////////////////
    get formatInterviewDate() {
        const interviewDate = new Date(this.interviewRecord.interviewDate);
        const hours = interviewDate.getHours();
        const minutes = interviewDate.getMinutes();
        return `${interviewDate.getFullYear()}/${interviewDate.getMonth() + 1}/${interviewDate.getDate()} ${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`;
    }
}
</script>
<style lang="scss" >
.edit-appointment-record {
    padding-left : 10px;
    padding-right: 10px;
    .edit-appointment-record-date{
        color          : #68737A;
        display        : flex;
        justify-content: space-between;
        margin-bottom  : 26px;
    }
}
.edit-appointment-record-btn{
    margin-top: 30px;
    display: flex;
    justify-content: center;
}
.el-textarea__inner {
    border: 1px solid #707070;
    padding: 10px 20px;
    font-size: 20px;
    &::placeholder {
        font-size: 20px;
    }
}
.required {
      position: relative;
      &::before {
        content: '*';
        position: absolute;
        color: #FF0000;
        transform: translate(-12px, 0);
        z-index: 5;
      }
    }
</style>
PAMapp/components/Interview/InterviewCard.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,176 @@
<template>
    <div>
      <template v-if="!interviewList.length">
          <span class="record-card record-card--empty" style="display:flex">
            ç„¡ç´„訪紀錄
          </span>
      </template>
      <template v-else>
        <div class="interview--future">
          <div class="record-card mb-10"
                v-for="(item, index) in futureList"
                :key="index + 'feature'"
                @click="editInterview(item)"
          >
            <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="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 class="interview-card-content">{{item.content}}</div>
            </div>
          </div>
            </div>
        </div>
        <section class="interview--past" v-if="pastList.length">
            <div class="record-card mb-10"
                v-for="(item, index) in pastList"
                :key="index + 'past'"
                @click="editInterview(item)"
            >
              <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="YEAR" />
                    </div>
                    </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 class="interview-card-content">{{item.content}}</div>
                </div>
              </div>
            </div>
        </section>
      </template>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "nuxt-property-decorator";
import { InterviewRecord } from "~/shared/models/appointment.model";
@Component
export default class InterviewCard extends Vue {
    @Prop()
    interviewList!: InterviewRecord[];
    futureList: InterviewRecord[] = [];
    pastList: InterviewRecord[] = [];
    appointmentId!: number;
    mounted() {
        this.appointmentId = +this.$route.params.appointmentId;
    }
    @Watch('interviewList', {immediate: true})
    onInterviewListChange() {
        if (this.interviewList.length > 0) {
            this.futureList = this.interviewList
            .filter(item => new Date(item.interviewDate).getTime() >= new Date().getTime())
            .sort((preItem, nextItem) => +new Date(nextItem.interviewDate) - +new Date(preItem.interviewDate));
          this.pastList = this.interviewList
            .filter(item =>  new Date(item.interviewDate).getTime() < new Date().getTime())
            .sort((preItem, nextItem) => +new Date(nextItem.interviewDate) - +new Date(preItem.interviewDate));
        }
    }
    editInterview(interviewRecord) {
        this.$router.push(`/appointment/${this.appointmentId}/interview/${interviewRecord.id}`);
    }
}
</script>
<style lang="scss" scoped>
.interview--future{
    .record{
        display: flex;
        justify-content: space-between;
        margin-bottom: 10px;
    }
}
.interview--past {
    margin-top: 10px;
    border-top: 1px solid #CCCCCC;
    padding-top: 17px;
    margin-top: 17px;
}
.record-card {
    height: 64px;
    margin-bottom: 20px;
    .record-card-date{
        display: flex;
        flex-direction: column;
        margin-left: 10px;
        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;
    background-color: #fff;
    color           : $MID_GREY;
    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;
}
</style>
PAMapp/components/Interview/InterviewMsg.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,176 @@
<template>
  <div class="interview-msg-component">
    <el-dialog
      :visible.sync="dialogVisible"
      :width="dialogWidth"
      @close="closeDialog"
      :lock-scroll="false"
        >
        <div class="subTitle msg-dialog-title">約訪通知</div>
      <div class="send-msg-nav">
        <div class="mdTxt">通知內容</div>
      </div>
      <el-input
        type="textarea"
        :autosize="true"
        placeholder="約訪通知"
        resize="none"
        v-model="interviewTxt">
        </el-input>
      <div v-if="client.phone">
        <div class="mdTxt mt-30 mb-10 required">預計約訪時段</div>
        <DateTimePicker
          @changeDateTime="interviewTime = $event"
          :isPastDateDisabled="true"
          :defaultValue="defaultValue"
        ></DateTimePicker>
      </div>
      <div class="msg-dialog-btn">
        <el-button @click="addInterview"  :disabled="isBtnDisabled">傳送</el-button>
      </div>
        </el-dialog>
        <PopUpFrame
          :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>
        </PopUpFrame>
  </div>
</template>
<script lang="ts">
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';
import { AgentInfo } from '~/shared/models/agent-info.model';
const loginStore = namespace('login.store');
const appointmentStore = namespace('appointment.store');
@Component
export default class InterviewMsg extends Vue {
    @appointmentStore.Action
    updateAppointmentDetail!: (id: number) => Appointment;
    @appointmentStore.Action
    getMyAppointmentList!: () => Promise<Appointment[]>;
    @PropSync('isVisible')
    dialogVisible!: boolean;
    @Prop({default:'90%'})
    dialogWidth!:string;
    @Prop()
    client!: Appointment;
    @Prop()
    defaultValue!: string;
    @Emit('closeDialog')
    closeDialog() {
        return;
    }
    @loginStore.State
    loginConsultant!: AgentInfo;
    isShowSuccessAlert = false;
    interviewTxt = "";
    interviewTime = '';
    //////////////////////////////////////////////////////////////////////
    mounted() {
      if(this.loginConsultant.phoneNumber && this.loginConsultant.email)
      {
        this.interviewTxt = "您好!我是保誠媒合平台的保險顧問" + this.loginConsultant.name + ",感謝您的預約!我預計會在下述的時間與您聯繫"+"\n"+"以下是我的電話號碼/Email:"+"\n" + this.loginConsultant.phoneNumber + "\n" + this.loginConsultant.email + "\n"+"若此時間不方便,請與我聯繫!謝謝!"}
        else if (!this.loginConsultant.phoneNumber && this.loginConsultant.email)
          {
            this.interviewTxt = "您好!我是保誠媒合平台的保險顧問" + this.loginConsultant.name + ",感謝您的預約!我預計會在下述的時間與您聯繫"+"\n"+"以下是我的Email:"+"\n" + this.loginConsultant.email + "\n"+"若此時間不方便,請與我聯繫!謝謝!"
        }
        else
        this.interviewTxt = "您好!我是保誠媒合平台的保險顧問" + this.loginConsultant.name + ",感謝您的預約!我預計會在下述的時間與您聯繫"+"\n"+"以下是我的電話號碼:"+"\n" + this.loginConsultant.phoneNumber + "\n"+"若此時間不方便,請與我聯繫!謝謝!"
    }
    addInterview() {
      const appointmentInformation: ToInformAppointment = {
        appointmentId: this.client.id,
        email        : this.client?.email,
        interviewDate: this.interviewTime,
        message      : this.interviewTxt,
        phone        : this.client?.phone,
      };
      appointmentService.informAppointment(appointmentInformation).then((_) => {
        this.isShowSuccessAlert = true ;
      });
    }
    closeAllDialog() {
      this.isShowSuccessAlert = false ;
      this.dialogVisible = false;
      this.updateAppointmentDetail(this.client.id);
      this.getMyAppointmentList();
    }
    get isBtnDisabled() :Boolean {
      const isFormValid = this.client.phone ? this.interviewTxt && this.interviewTime :this.interviewTxt
      return !isFormValid
    }
}
</script>
<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;
    margin-bottom:30px;
    color: $PRIMARY_BLACK;
  }
  .send-msg-nav{
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
    color: $PRIMARY_BLACK;
  }
  .el-dialog{
    width:90%
  }
  .el-textarea__inner{
    font-size: 20px;
    padding:10px;
    text-align: justify;
    font-weight: 600;
  }
  .msg-dialog-btn{
    margin-top: 30px;
    display: flex;
    justify-content: center;
  }
  .invite-review{
      display: flex;
      flex-direction: column;
      align-items: center;
    }
}
</style>
PAMapp/components/Interview/InterviewRecordCard.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,98 @@
<template>
    <div class="record-log-component">
        <div v-for="(item, index) in noticeLogsList"
                :key="index">
                <section
                    class="record-log-card"
                >
                    <div class="record-log-card-date-container">
                        <div class="record-log-card-date-container-circle">
                            <div>
                                <UiDateFormat
                                    class="xxsTxt bold line-height"
                                    :date="item.createdDate"
                                    onlyShowSection="YEAR" />
                            </div>
                            <div>
                                <UiDateFormat
                                    class="xxsTxt bold line-height"
                                    :date="item.createdDate"
                                    onlyShowSection="DATE" />
                            </div>
                            <div>
                                <UiDateFormat
                                    class="xxsTxt mt-4 line-space"
                                    :date="item.createdDate"
                                    onlyShowSection="TIME" />
                            </div>
                        </div>
                    </div>
                        <div class="record-log-msg">
                            <div>發送約訪通知
                                <span v-if="item.email && item.phone">(手機簡訊、Email)</span>
                                <span v-else-if="item.email">(Email)</span>
                                <span v-else>(手機簡訊)</span>
                            </div>
                            <div v-if="item.phone" class="mt-10">預約{{item.interviewDate | formatDate}}</div>
                        </div>
                </section>
                <div class="time-line"></div>
            </div>
    </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "nuxt-property-decorator";
import { NoticeLogs } from "~/shared/models/appointment.model";
@Component
export default class RecordCard extends Vue {
    @Prop()
    noticeLogsList!: NoticeLogs[];
}
</script>
<style lang="scss" scoped>
.record-log-component{
    display: flex;
    flex-direction: column;
    .record-log-card{
        display: flex;
        .record-log-card-date-container{
            position:relative;
            .record-log-card-date-container-circle{
                display: flex;
                flex-direction: column;
                width: 56px;
                height: 56px;
                border-radius: 50%;
                border:1px solid $PRIMARY_BLACK;
                justify-content: center;
                align-items: center;
                align-content: center;
                background-color:#1B365D;
                color: #fff;
            }
        }
    }
}
.mt-4{
    margin-top: 4px;
}
.line-space{
    letter-spacing: 1px;
}
.line-height{
    line-height:1.2;
}
.time-line{
    border-left: 1px solid black;
    height: 30px;
    margin-left: 28px;
}
.record-log-msg{
    margin-left: 13px;
    margin-top: 10px;
}
</style>
PAMapp/components/NavBar.vue
@@ -7,13 +7,32 @@
      <div class="pam-header__title--sub">預約我的幸福守護者</div>
    </div>
    <div class="pam-header__action-bar">
      <!-- <i class="icon-bell text--dark-blue cursor--pointer fix-chrome-click--issue"
        @click="$router.push('/notification')"></i> -->
      <i
        v-if="isShowNotification"
        class="icon-bell text--dark-blue cursor--pointer"
        @click="$router.push('/notification')"
      >
        <span :class="{'newNotification': isNewNotification}"></span>
      </i>
        <el-dropdown :class="{'is-open':isOpenDropdown}"
          ref="dropdown"
          trigger="click"
          @command="routerNavigateTo">
          <i class="icon-avatar text--dark-blue cursor--pointer fix-chrome-click--issue" @click="isOpenDropdown =!isOpenDropdown" @blur="isOpenDropdown =false"></i>
          <div
            @click="isOpenDropdown =!isOpenDropdown"
            @blur="isOpenDropdown =false">
            <template v-if="isAdminLogin">
              <UiAvatar
              :size="30"
              :agentNo="consultantId"
              class="admin-avatar"
              ></UiAvatar>
            </template>
            <template v-else>
              <i class="icon-avatar text--dark-blue cursor--pointer fix-chrome-click--issue"></i>
            </template>
          </div>
          <el-dropdown-menu
            class="pam-header__dropdown">
            <template v-for="(item,index) in navBarList">
@@ -33,22 +52,47 @@
<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
  export default class NavBar extends Vue {
    @roleStorage.Getter idToken!: string | null;
    @roleStorage.Getter currentRole!: string | null;
    @roleStorage.Getter consultantId!: string | null;
    @roleStorage.Getter
    idToken!: string | null;
    @roleStorage.Mutation storageClear!: () => void;
    @roleStorage.Getter
    currentRole!: string | null;
    @roleStorage.Getter
    consultantId!: string | null;
    @roleStorage.Mutation
    storageClear!: () => void;
    @roleStorage.Getter
    isAdminLogin!: boolean;
    @roleStorage.Getter
    isUserLogin!: boolean;
    @Action
    storeMyPersonalNotification!: () => void;
    @State
    notificationList!: NotificationList[];
    @Action
    storeMyAppointmentReviewLog!: () => void;
    @State
    unReviewLogList!: AppointmentLog[];
    isOpenDropdown = false;
    login_role     = Role.NOT_LOGIN;
    navBarList = [{
        authorityOfRoleList: [Role.NOT_LOGIN],
@@ -99,6 +143,16 @@
    //////////////////////////////////////////////////////////////////////
    @Watch('$route', {immediate: true})
    onRouterChange() {
        if (this.currentRole) {
          this.storeMyPersonalNotification();
          this.storeMyAppointmentReviewLog();
        }
    }
    //////////////////////////////////////////////////////////////////////
    routerNavigateTo(url: string): void {
      (this.$refs.dropdown as any).hide();
      _.isEqual(url,'')
@@ -118,6 +172,21 @@
    get loginRole(): Role {
      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
      }
    }
    get isNewNotification() {
      if (this.currentRole) {
        return this.notificationList.filter(item => !item.readDate).length;
      }
    }
  }
@@ -185,9 +254,30 @@
      i {
        padding: 0px 15px;
        @extend .fix-chrome-click--issue;
        @media screen and (max-width: 352px) {
          padding: 0px 10px;
        }
      }
      .admin-avatar {
        margin: 0px 15px;
        @media screen and (max-width: 352px) {
          margin: 0px 10px;
        }
      }
      .icon-bell {
        position: relative;
        .newNotification {
          position: absolute;
          width: 10px;
          height: 10px;
          top: 0;
          right: 15px;
          border-radius: 50px;
          background: $PRIMARY_RED;
        }
      }
    }
@@ -210,11 +300,11 @@
      height: $DESKTOP_NAV_BAR;
      .pam-header__logo {
        width: 180px;
        height: 100%;
        width: 160px;
        height: 70px;
        margin: 0;
        background-image: url('~/assets/images/logo.png');
        background-size: cover;
        background-size: contain;
        background-repeat: no-repeat;
        background-position: center;
      }
PAMapp/components/QuickFilter/QuickFilterConsultantList.vue
@@ -19,7 +19,7 @@
            >
                <UiAvatar
                    :size="200"
                    :fileName="item.img"
                    :agentNo="item.agentNo"
                    class="mx-auto"
                    @click.native="showAgentDetail(item.agentNo)"
                ></UiAvatar>
PAMapp/components/QuickFilter/QuickFilterSelector.vue
@@ -71,7 +71,7 @@
        <div v-else>
            <el-rate
            v-if="!hideReviews"
                class="pam-quickFilter-rate"
                class="pam-rate mt-30"
                v-model="pickedItem.avgScore"
            ></el-rate>
        </div>
PAMapp/components/ReviewRecords/ReviewRecords.vue
@@ -11,11 +11,11 @@
    </section>
    <div class="user-reviews-page">
      <template v-if="myAppointmentReviewLogList.length">
      <template v-if="reviewLogList.length">
        <section class="user-reviews-content">
            <div
                class="user-reviews-card"
                v-for="(appointmentLog, index) in myAppointmentReviewLogList"
                v-for="(appointmentLog, index) in reviewLogList"
                :key="index">
                <div class="user-reviews-card-content" v-if="isUserLogin">
                    æ‚¨å°<span class="mdTxt">{{ ` ${appointmentLog.agentName} ` }}</span>做了 <UiReviewScore :score="appointmentLog.score" /> è©•價!
@@ -55,17 +55,19 @@
</template>
<script lang="ts">
import { AppointmentLog } from '~/shared/models/appointment.model';
import { Vue, Component, Prop } from 'nuxt-property-decorator';
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator';
import authService from '~/shared/services/auth.service';
@Component
export default  class ReviewRecords extends Vue{
    @Prop()
    myAppointmentReviewLogList!: AppointmentLog[];
    reviewLogList!: AppointmentLog[];
    isUserLogin = false;
    //////////////////////////////////////////////////////////////////////
    mounted() {
      this.isUserLogin = authService.isUserLogin();
    }
PAMapp/components/Ui/UiAvatar.vue
@@ -15,10 +15,10 @@
    size!: number;
    @Prop()
    fileName!: string;
    agentNo!: string;
    get imgSrc() {
        return process.env.BASE_URL + '/consultant/avatar/' + this.fileName;
        return process.env.BASE_URL + '/consultant/avatar/' + this.agentNo;
    }
}
</script>
PAMapp/components/Ui/UiDateFormat.vue
@@ -13,7 +13,7 @@
    date!: Date | string;
    @Prop()
    onlyShowSection!: 'DAY' | 'TIME';
    onlyShowSection!: 'YEAR' | 'DATE' | 'DAY' | 'TIME';
    compareTarget!: Date;
    displayValue = '';
@@ -46,6 +46,12 @@
                        this.displayValue = isThisYear(compareTarget) ? thisYearDayLabel : `${compareTarget.getFullYear()}/${compareTarget.getMonth() + 1}/${compareTarget.getDate()}`;
                    } else if (this.onlyShowSection === 'TIME') {
                        this.displayValue = `${compareTarget.getHours()}:${minutes}`;
                    } else if (this.onlyShowSection === 'DATE') {
                        this.displayValue = isThisYear(compareTarget)
                            ? thisYearDayLabel
                            : `${compareTarget.getMonth() + 1}/${compareTarget.getDate()}`;
                    } else if (this.onlyShowSection === 'YEAR') {
                        this.displayValue = `${compareTarget.getFullYear()}`;
                    }
                    if (this.onlyShowSection) return;
PAMapp/components/Ui/UiDatePicker.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,71 @@
<template>
    <el-date-picker
        class="pam-date cursor--pointer"
        popper-class="pam-date-popper"
        v-model="dateValue"
        :clearable="false"
        type="date"
        :editable="false"
        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, PropSync, Vue, Watch } from "nuxt-property-decorator";
@Component
export default class UiDatePicker extends Vue {
    dateValue: Date | string = '';
    @Prop()
    defaultValue!: string;
    @Prop({default: false})
    isPastDateDisabled!: boolean;
    @Prop({default: false})
    isFutureDateDisabled!: boolean;
    @Emit('changeDate')
    changeDate() {
        return this.dateValue;
    }
    @Watch('defaultValue', {immediate: true})
    updateDefault() {
        if (this.defaultValue) {
            this.dateValue = new Date(this.defaultValue);
            this.changeDate();
        }
    }
    get pickerOptions() {
        const date = new Date();
        const currentDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
        if (this.isPastDateDisabled) {
            return {
                disabledDate(time: Date) {
                    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();
                }
            }
        }
    }
}
</script>
PAMapp/components/Ui/UiField.vue
@@ -3,7 +3,7 @@
      v-if="fieldDisplayDevice === 'ALL'
      || fieldDisplayDevice === currentDevice">
      <div class="pam-field__label">
        <div class="pam-field__title"><i :class="fieldIcon"></i>{{ fieldLabel }}</div>
        <div class="pam-field__title" :style="{ 'font-size': fieldLabelSize }"><i :class="fieldIcon"></i>{{ fieldLabel }}</div>
      </div>
      <p class="pam-field__content">
        <slot></slot>
@@ -32,6 +32,9 @@
  content!: string;
  @Prop()
  labelSize?: number;
  @Prop()
  displayDevice!: 'MOBILE' | 'DESKTOP' | 'ALL';
  currentDevice: 'MOBILE' | 'DESKTOP' = 'MOBILE';
@@ -58,28 +61,31 @@
    return this.displayDevice || 'ALL';
  }
  get fieldLabelSize(): string {
    return (this.labelSize || 16) + 'px';
  }
}
</script>
<style lang="scss" scoped>
.pam-field {
  display: flex;
  display       : flex;
  flex-direction: column;
  .pam-field__label {
    display: flex;
    align-items: center;
    display    : flex;
    .pam-icon {
      font-size: 12px;
    }
    .pam-field__title {
      font-size: 16px;
      font-weight: bold;
      display: flex;
      align-items: center;
      display    : flex;
      font-weight: bold;
    }
  }
  .pam-field__content {
    display    : flex;
    padding-top: 10px;
  }
}
PAMapp/components/Ui/UiSelect.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,26 @@
 <template>
  <el-select v-model="syncedCloseReason" class="pam-select" ref="select">
    <el-option
      v-for="(item,index) in options"
      :key="index"
      :label="item.key"
      :value="item.value">
      {{ item.key }}
    </el-option>
  </el-select>
</template>
<script lang="ts">
import { Vue, Component, PropSync, Prop} from 'nuxt-property-decorator';
@Component
export default class UiSelect extends Vue {
  @PropSync('closeReason', { type: String, default: 'other' })
  syncedCloseReason!: string;
  @Prop({ type:Array , default:()=>[] })
  options!: object[];
}
</script>
PAMapp/components/Ui/UiTimePicker.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,118 @@
<template>
    <el-time-select
        class="pam-time cursor--pointer"
        popper-class="pam-time-popper"
        v-model="timeValue"
        :clearable="false"
        :editable="false"
        :picker-options="pickerOptions"
        placeholder="選擇時間"
        prefix-icon="icon-down down-icon"
        value-format="timestamp"
        @change="changeTime"
    >
    </el-time-select>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue, Watch } from "nuxt-property-decorator";
@Component
export default class UiTimePicker extends Vue {
    timeValue = '';
    @Prop()
    defaultValue!: string;
    @Prop({default: ''})
    changeDate!: Date | string;
    @Prop()
    isPastDateDisabled!: boolean;
    @Prop()
    isFutureDateDisabled!: boolean;
    ///////////////////////////////////////////////////////////////////////
    @Emit('changeTime')
    changeTime() {
        return this.timeValue;
    }
    @Watch('defaultValue', {immediate: true})
    updateDefault() {
        if (this.defaultValue) {
            const defaultDate = new Date(this.defaultValue);
            this.timeValue = this.formatTimeString(defaultDate);
            this.changeTime();
        }
    }
    ///////////////////////////////////////////////////////////////////////
    get pickerOptions() {
        let minTime = '';
        let maxTime = '';
        const currentDate = new Date();
        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,
            maxTime: maxTime
        }
    }
    private isPickedDisableTime(currentDate: Date, minMaxTime: string) {
        const currentTime = this.getTimeValue(currentDate, minMaxTime);
        const pickedTime = this.getTimeValue(currentDate, this.timeValue);
        if (this.isPastDateDisabled && pickedTime < currentTime) {
            this.timeValue = '';
            this.changeTime();
        }
        if (this.isFutureDateDisabled && currentTime < pickedTime) {
            this.timeValue = '';
            this.changeTime();
        }
    }
    private isPickedToday(currentDate: Date) {
        const pickedDate = new Date(this.changeDate);
        const today = this.formatDateString(currentDate);
        const picked = this.formatDateString(pickedDate);
        return today === picked;
    }
    private getTimeValue(date: Date, time: string) {
        const hour = time.split(':')[0];
        const minute = time.split(':')[1];
        return date.setHours(+hour, +minute);
    }
    private formatDateString(date: Date) {
        return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
    }
    private formatTimeString(date: Date) {
        const hours = date.getHours();
        const minutes = date.getMinutes();
        return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}`;
    }
}
</script>
PAMapp/components/editConsultantAvatar.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,127 @@
<template>
  <div>
    <el-upload
      class="pam-avatar-uploader"
      ref="upload"
      action="#"
      :auto-upload="false"
      :on-change="handleAvatarUploaded"
      :show-file-list="false"
      accept="image/png, image/jpeg, image/jpg">
        <el-avatar
          :size="150"
          :src="imgSrc"
          class="pam-avatar cursor--pointer fix-chrome-click--issue"
        ></el-avatar>
        <div  class="pam-avatar-uploader__action-label mt-10 cursor--pointer" >設定相片</div>
    </el-upload>
      <div
        v-if="showResetAvatarBtn"
        class="pam-avatar-uploader__action-label text--center mt-10 cursor--pointer"
        @click="resetAvatar">
        å–消上傳相片
      </div>
  </div>
</template>
<script lang="ts">
  import { Vue, Component, Prop, PropSync } from 'nuxt-property-decorator';
  import { MessageBox } from 'element-ui';
  import { MessageBoxData } from 'element-ui/types/message-box';
  import _ from 'lodash';
  import myConsultantService from '~/shared/services/my-consultant.service';
  @Component
  export default class editConsultantAvatar extends Vue {
    @Prop({type:String, default:""})
    agentNo!:string;
    @PropSync('photoBase64',{type:String, default:""})
    syncPhotoBase64!:string;
    _imgSrc: string = '';
    imgSrc: string='';
    //////////////////////////////////////////////////////////////////////
    mounted() {
      if(this.agentNo) this.initConsultantAvatar()
    }
    private initConsultantAvatar(): void {
      myConsultantService.getConsultantAvatar(this.agentNo)
      .then(base64=>
       this.splitBase64WithCommon(base64)
      )
    }
    //////////////////////////////////////////////////////////////////////
    resetAvatar(): void {
      this.imgSrc = this._imgSrc;
    }
    handleAvatarUploaded(file:any): void {
      const isFollowUploadRule =_.includes(file.raw.type,'image/');
      isFollowUploadRule ? this.getImgSrc(file) : this.showFileUploadErrorMsg()
    }
    private getImgSrc(file:any):void{
      const blob = file.raw;
      this.blobToBase64(blob).then(base64=>{
        this.splitBase64WithCommon(base64 as string);
      });
    }
    private blobToBase64(blob:File):Promise<string | ArrayBuffer | null> {
      return new Promise((resolve,reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onloadend = () => {
          resolve(reader.result)
        };
      });
    }
    private splitBase64WithCommon(base64: string): void {
      const splitBase64= _.split(base64, ','); // ç‚ºäº†æŠŠ data:image , base64 è§£æžåˆ†é–‹;
      this.syncPhotoBase64 = splitBase64[1];
      // NOTE: å› ç‚ºç›®å‰ä»¥ agentNO å–å¾— avatar æœƒå¤±æ•—,
      // æ•…加上此判斷來防範不預期顯示'取消按鈕'的狀況。 [Tomas, 2022/1/3]
      if (!this._imgSrc) {
        this._imgSrc = base64;
      }
      this.imgSrc = base64;
    }
    private showFileUploadErrorMsg():Promise<MessageBoxData>{
       return MessageBox({
          message:`<div class="message-header">上傳格式有誤</div>
                    <div class="message-content">請上傳正確圖檔</div>`,
          dangerouslyUseHTMLString: true,
          showClose:false,
          showConfirmButton:true,
          confirmButtonText:'確認',
          customClass:'pam-message-box',
          closeOnClickModal:false,
        });
    }
    get showResetAvatarBtn(): boolean {
      // NOTE: å› ç‚ºç›®å‰ä»¥ agentNO å–å¾— avatar æœƒå¤±æ•—,
      // æ•…加上此判斷來防範不預期顯示'取消按鈕'的狀況。 [Tomas, 2022/1/3]
      if (!this._imgSrc && !this.imgSrc) return false;
      return this._imgSrc !== this.imgSrc;
    }
  }
</script>
<style lang="scss" scoped>
</style>
PAMapp/components/loading.vue
@@ -45,7 +45,7 @@
    background-color: rgba(#222222, 0.5);
    width: 100%;
    height: 100%;
    z-index: 99;
    z-index: 2500;
    display: flex;
    justify-content: center;
    align-items: center;
PAMapp/components/multiSelectBtn.vue
@@ -25,7 +25,7 @@
</template>
<script lang="ts">
  import { Vue, Component, Prop, PropSync} from 'vue-property-decorator';
  import { Vue, Component, Prop, PropSync, Watch} from 'vue-property-decorator';
import { OptionBtnDto } from '~/shared/models/optionBtnDto.model';
  @Component
@@ -46,6 +46,17 @@
    @Prop({type:String,default:''})
    nameOfOtherOption!:string;
    @Prop()
    maxLength? : number;
    @Watch('syncMutiSelect')
    onMutiSelectChange(): void {
      if (!this.maxLength) return;
      if (this.syncMutiSelect.length > this.maxLength) {
        this.syncMutiSelect.shift();
      }
    }
    isSelectOtherOption=false;
    isSelectAll=false;
PAMapp/layouts/default.vue
@@ -57,13 +57,13 @@
    // format to {page}-banner or pam-no-banner tag
    private routeFormatBannerClass(route: string): string {
      const needBannerRoutes = ['recommendConsultant', 'quickFilter', 'myConsultantList-consultantList', 'myConsultantList-contactedList', 'myAppointmentList-appointmentList', 'myAppointmentList-contactedList', 'login'];
      const needBannerRoutes = ['recommendConsultant', 'quickFilter', 'myConsultantList-consultantList', 'myConsultantList-contactedList', 'myAppointmentList-appointmentList', 'myAppointmentList-contactedList', 'login', 'notification'];
      return _.includes(needBannerRoutes, route) ? route + '-banner' : 'pam-no-banner';
    };
    private bannerText: FeatureBannerTitle= {
      [FeaturePage.RECOMMEND_CONSULTANT]: ['輸入問題回答', '依照你的需求推薦嚴選顧問'],
      [FeaturePage.QUICK_FILTER]        : ['點選下方選項', '尋找你的BEST Match'],
      [FeaturePage.RECOMMEND_CONSULTANT]: ['輸入問題回答', '依照您的需求推薦嚴選顧問'],
      [FeaturePage.QUICK_FILTER]        : ['點選下方選項', '尋找您的BEST Match'],
      [FeaturePage.MY_CONSULTANT_LIST]  : [],
      [FeaturePage.QUESTIONNAIRE]       : [],
    }
@@ -114,6 +114,9 @@
  }
  @include desktop {
    .pam-banner  {
      height: 150px;
    }
    .mt-navBar {
      margin-top: calc($DESKTOP_NAV_BAR + $MOB_NAV_BAR);
    }
@@ -219,4 +222,16 @@
    }
  }
  .notification {
    &-banner {
      background-image: url('~/assets/images/notification/banner_mob.svg');
    }
    @include desktop {
      &-banner {
        background-image: url('~/assets/images/notification/banner_web.svg');
      }
    }
  }
</style>
PAMapp/middleware/errorRoute.ts
File was renamed from PAMapp/middleware/errorRouteMiddleware.ts
@@ -1,6 +1,7 @@
import { Middleware } from '@nuxt/types';
const errorRouteMiddleware: Middleware = (context) => {
const errorRoute: Middleware = (context) => {
  if (!context.route.name) {
    const isAdminLogin = context.store.getters['localStorage/isAdminLogin'];
    if (isAdminLogin) {
@@ -11,4 +12,4 @@
  }
}
export default errorRouteMiddleware
export default errorRoute;
PAMapp/middleware/getUrlQuery.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,26 @@
import { Middleware } from '@nuxt/types';
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);
    if (!isUserLogin) {
      context.redirect('/login');
    }
  }
}
export default getUrlQuery
PAMapp/nuxt.config.js
@@ -39,6 +39,7 @@
    '~/plugins/vue-scroll-picker',
    '~/plugins/filters/date.filter.ts',
    '~/plugins/filters/age.filter.ts',
    '~/plugins/filters/appointment-fail-reason.filter.ts',
  ],
  // Auto import components: https://go.nuxtjs.dev/config-components
@@ -70,6 +71,6 @@
  },
  router: {
    base: process.env.ENV === 'uat' ? '/pam/' : '',
    middleware: 'errorRouteMiddleware'
    middleware: ['getUrlQuery', 'errorRoute']
  }
}
PAMapp/pages/accountSetting/index.vue
@@ -47,6 +47,18 @@
        </div>
      </div>
    </div>
    <PopUpFrame :isOpen.sync="updateDone">
      <div class="text--center mdTxt fs-18">
        <p class="mt-20 text--center ">帳號資訊更新成功</p>
        <el-button
                type="primary"
                @click="updateDone = false"
                class="mt-20"
              >我知道了</el-button>
      </div>
    </PopUpFrame>
    <div class="pam-paragraph account-confirm">
      <el-button :disabled="isSubmitBtnDisabled"
        @click.native="updateAccountSetting">
@@ -70,6 +82,7 @@
  userPhoneDisabled = true;
  userEmailDisabled = true ;
  onEditMode        = false;
  updateDone        = false;
  userNameValue     = '';
  phoneValue        = '' ;
  emailValue        = '' ;
@@ -114,6 +127,8 @@
            accountSettingService.updateAccountSetting(editSettingInfo).then((res: any) => {
                console.log('updateRes:', res);
                this.resetSettingForm();
                this.updateDone = true;
            });
        }
PAMapp/pages/agentInfo/_agentNo.vue
@@ -3,7 +3,10 @@
      <el-row
        type="flex"
        justify="center">
        <UiAvatar :size="150" :fileName="agentInfo.img"></UiAvatar>
        <UiAvatar
          :size="150"
          :agentNo="agentInfo.agentNo">
        </UiAvatar>
      </el-row>
      <el-row
@@ -21,6 +24,15 @@
        class="pt-10"
        justify="center">
        <h3 class="mdTxt">{{ agentName }}</h3>
      </el-row>
      <el-row
        v-if="currentRole === role.ADMIN"
        type="flex"
        class="pam-paragraph">
        <UiField :span="12" icon="phone" label="手機號碼">
          {{ agentInfo.phoneNumber }}
        </UiField>
      </el-row>
      <el-row
@@ -110,6 +122,15 @@
        </el-col>
      </el-row>
      <div class="consultant-edit-btn">
        <UiField icon="flag" label="溝通風格">
          <div class="text--orange bold pr-10 "
            v-for="(communicationStyle, index) in displayCommunicationStyleList"
            :key="index">
              #{{ communicationStyle }}</div>
        </UiField>
    </div>
      <el-row
        type="flex"
        class="pam-paragraph">
@@ -125,7 +146,7 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="comment" label="個人理念">
        <UiField icon="comment" label="個人理念" class="agent-info-textarea">
          {{ agentInfo.concept }}
        </UiField>
      </el-row>
@@ -133,9 +154,9 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="school" label="個人背景">
          <span v-for="(experience, index) in agentInfo.experiences" :key="index">
            {{ experience }}<span v-if="index !== agentInfo.experiences.length - 1">, </span>
        <UiField icon="school" label="個人背景" class="agent-info-textarea">
          <span>
            {{ agentInfo.experiences }}
          </span>
        </UiField>
      </el-row>
@@ -143,7 +164,7 @@
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="trophy" label="得獎經歷">
        <UiField icon="trophy" label="得獎經歷" class="agent-info-textarea">
          {{ agentInfo.awards }}
        </UiField>
      </el-row>
@@ -182,6 +203,9 @@
            </div>
        </div>
    </PopUpFrame>
    <div class="consultant-edit-btn" v-if="currentRole === role.ADMIN">
      <el-button type="primary" @click.native="$router.push(`/agentInfo/edit/${agentInfo.agentNo}`)">編輯帳戶資訊</el-button>
    </div>
    </div>
</template>
@@ -205,12 +229,13 @@
  agentInfo!: AgentInfo;
  role = Role;
  isAlertAddSuccess = false;
  isAlertFieldInfo  = false;
  fieldInfoTitle    = '';
  fieldInfoDesc     = '';
  hideReviews       = hideReviews ;
  isAlertFieldInfo = false;
  fieldInfoTitle = '';
  fieldInfoDesc = '';
  hideReviews = hideReviews ;
//////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////
  async asyncData(context: Context) {
    const agentNo = context.route.params.agentNo;
    return {
@@ -218,11 +243,12 @@
    }
  }
//////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////
  alertAddSuccess(): void {
      this.isAlertAddSuccess = true;
  }
  alertFieldInfo(field: string): void {
    this.isAlertFieldInfo = true;
    switch(field) {
@@ -237,12 +263,14 @@
    }
  }
//////////////////////////////////////////////////////////////////////
  get agentName(): string {
    return `${this.agentInfo.name}(${this.agentInfo.role})`;
  }
  get displayCommunicationStyleList(): string[] {
    return this.agentInfo.communicationStyle.split('、').filter((item) => item);
  }
}
</script>
@@ -296,5 +324,16 @@
  flex-wrap: wrap;
  line-height: 24px;
}
.consultant-edit-btn{
  display: flex;
  justify-content: center;
}
.pam-field{
  display: flex;
}
.agent-info-textarea{
  word-break: break-all;
  word-wrap: break-word;
}
</style>
PAMapp/pages/agentInfo/edit/_agentNo.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,480 @@
<template>
    <div class="edit-agent-info-page">
      <el-row
        type="flex"
        justify="center">
        <EditConsultantAvatar
          :agentNo="agentInfo.agentNo"
          :photoBase64.sync="editInfoValue.photoBase64"/>
      </el-row>
      <el-row
        type="flex"
        class="pt-10"
        justify="center"
        align="middle" v-if="!hideReviews">
        <!-- TODO:隱藏滿意度 -->
          <i class="pam-icon icon--primary icon-star"></i>
          <h3 class="mdTxt">{{ agentInfo.avgScore }}</h3>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph"
        justify="center">
        <el-input class="mdTxt" v-model="editInfoValue.name"></el-input>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <el-col :span="24" class="pam-field">
          <div class="pam-field__label pam-progress__label">
            <div>
              <div class="pam-field__title mb-10">
                <i class="pam-icon icon-phone"
                  ></i>手機號碼
                  <span class="hint text--bold" v-show="!phoneValid">手機號碼格式有誤</span>
                  <span class="hint text--bold" v-show="editInfoValue.phoneNumber.length === 0">手機號碼為必填</span>
              </div>
            </div>
            <el-input
            v-model="editInfoValue.phoneNumber"
            :class="{'is-invalid': !phoneValid}"
            maxlength="10"
            minlength="10"></el-input>
          </div>
        </el-col>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField :span="12" icon="agent" label="頭銜">
          <el-input  v-model="editInfoValue.title"></el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="company" label="服務地區">
          <el-input  v-model="editInfoValue.serveArea"></el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="address" label="公司地址">
          <el-input  v-model="editInfoValue.companyAddress"></el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField :span="12" icon="time" label="最後上線時間">
          {{ agentInfo.latestLoginTime | formatDate }}
        </UiField>
        <UiField :span="12" icon="calender" label="服務資歷" style="display:flex">
          <el-input  v-model="editInfoValue.seniorityYear" class="seniority-input"></el-input>å¹´
          <el-select  style="width:60px" v-model="editInfoValue.seniorityMonth" class="seniority-input">
            <el-option
              v-for="(month) in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]"
              :value="month"
              :key="month">
              {{ month }}
            </el-option>
          </el-select>月
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <el-col :span="24" class="pam-field">
          <div class="pam-field__label pam-progress__label">
            <div>
              <div class="pam-field__title">
                <i class="pam-icon icon-thumbs-up"
                  ></i>諮詢度表現 <i class="pl-5 text--primary icon-information" @click="alertFieldInfo('evaluation')"></i>
              </div>
            </div>
            <div class="xsTxt">
              {{ agentInfo.evaluation }}/50 (近一個月/累計)
            </div>
          </div>
          <div class="pam-field__content pam-field-evaluation pt-10">
            <el-progress :show-text="false" :stroke-width="15" :percentage="agentInfo.evaluation * 2"></el-progress>
          </div>
        </el-col>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <el-col :span="24" class="pam-field">
          <div class="pam-field__label pam-progress__label">
            <div>
              <div class="pam-field__title">
                <i class="pam-icon icon-flag"
                  ></i>溝通風格  <span class="hint text--bold">(可複選,最多2項)</span>
              </div>
            </div>
            <MultiSelectBtn class="mt-30"
            :mutiSelect.sync="editInfoValue.communicationStyle"
            :options="agentCommunicationStyleList"
            :maxLength="2"
            >
            </MultiSelectBtn>
          </div>
        </el-col>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <el-col :span="24" class="pam-field">
          <div class="pam-field__label pam-progress__label">
            <div>
              <div class="pam-field__title">
                <i class="pam-icon icon-flag"
                  ></i>專長領域  <span class="hint text--bold">(可複選)</span>
              </div>
            </div>
            <MultiSelectBtn class="mt-30"
            :mutiSelect.sync="editInfoValue.expertise"
            :options="agentExpertList"
            >
            </MultiSelectBtn>
          </div>
        </el-col>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="comment" label="個人理念">
          <el-input type="textarea" autosize v-model="editInfoValue.concept"></el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="school" label="個人背景">
            <el-input type="textarea" autosize v-model="editInfoValue.experiences"></el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField icon="trophy" label="得獎經歷">
          <el-input type="textarea" autosize style="height:70%" v-model="editInfoValue.awards"></el-input>
        </UiField>
      </el-row>
    <PopUpFrame
      :isOpen.sync="isAlertFieldInfo"
      >
        <div class="text--center mdTxt fs-18">
            <p>{{ fieldInfoTitle }}</p>
            <p class="mt-20 text--left text--regular">{{ fieldInfoDesc }}</p>
            <div class="text--center mt-30">
              <el-button
                type="primary"
                @click="isAlertFieldInfo = false"
              >我知道了</el-button>
            </div>
        </div>
    </PopUpFrame>
    <PopUpFrame :isOpen.sync="isInfoUpdate">
      <div class="text--center mdTxt fs-18">
        <p class="mt-20 text--center ">帳號資訊更新成功</p>
        <el-button
                type="primary"
                @click="backToInfo"
                class="mt-20"
              >我知道了</el-button>
      </div>
    </PopUpFrame>
    <div class="pam-paragraph account-confirm">
      <el-button :disabled="isSubmitBtnDisabled"
        @click.native="editAgentInfoSetting">
          é€å‡º
      </el-button>
    </div>
    </div>
</template>
<script lang="ts">
import { Context } from '@nuxt/types';
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component, Prop } from 'vue-property-decorator';
import * as _ from "lodash";
import myConsultantService from '~/shared/services/my-consultant.service';
import accountSettingService from '~/shared/services/account-setting.service';
import { AgentInfo } from '~/shared/models/agent-info.model';
import { hideReviews } from '~/shared/const/hide-reviews';
import { AgentInfoSetting } from '~/shared/models/account.model';
import { Role } from '~/shared/models/enum/Role';
import { agentCommunicationStyleList } from '~/shared/const/agent-communication-style-list';
const localStorageTest = namespace('localStorage');
@Component
export default class AgentInfoComponent extends Vue {
  @localStorageTest.State('current_role')
  currentRole!:string | null;
  _agentInfoSetting!: AgentInfoSetting;
  agentInfo!      : AgentInfo
  fieldInfoDesc   : string = '';
  fieldInfoTitle  : string = '';
  hideReviews     : boolean = hideReviews ;
  isAlertFieldInfo: boolean = false;
  isInfoUpdate    : boolean = false;
  editInfoValue = {
    agentNo           : '',
    name              : '',
    expertise         : [] as string[],
    title             : '',
    serveArea         : '',
    companyAddress    : '',
    seniorityYear     : 1,
    seniorityMonth    : 0,
    concept           : '',
    experiences       : '',
    awards            : '',
    communicationStyle: [] as string[],
    photoBase64       : '',
    phoneNumber       : ''
  };
  communicationStyleList: string[] = agentCommunicationStyleList;
  role           = Role;
  agentExpertList = [
    {
        title:'健康與保障',
        label:'健康與保障'
    },
    {
        title:'子女教育',
        label:'子女教育'
    },
    {
        title:'資產規劃',
        label:'資產規劃'
    },
    {
        title:'樂活退休',
        label:'樂活退休'
    },
    {
        title:'保單健檢/規劃',
        label:'保單健檢/規劃'
    },
    {
        title:'分紅保單',
        label:'分紅保單'
    }];
  agentCommunicationStyleList = [
    {
        title:'謹慎務實',
        label:'謹慎務實'
    },
    {
        title:'明快主動',
        label:'明快主動'
    },
    {
        title:'耐心傾聽',
        label:'耐心傾聽'
    },
    {
        title:'健談風趣',
        label:'健談風趣'
    }];
  //////////////////////////////////////////////////////////////////////
  async asyncData(context: Context) {
    const agentNo = context.route.params.agentNo;
    return {
      agentInfo: await myConsultantService.getConsultantDetail(agentNo).then((res) => res)
    }
  }
  mounted(){
    this.setAgentInfo(this.agentInfo);
  }
  private setAgentInfo(agentInfo: AgentInfo): void {
    const [agentYear, _yearUnit , agentMonth, _monthUnit] =  agentInfo.seniority.split(" ");
    this._agentInfoSetting = {
      agentNo           : agentInfo.agentNo||'',
      name              : agentInfo.name || '',
      expertise         : agentInfo.expertise || [],
      title             : agentInfo.title || '',
      role              : agentInfo.role||'',
      serveArea         : agentInfo.serveArea || '',
      gender            : agentInfo.gender||'',
      phoneNumber       : agentInfo.phoneNumber||'',
      companyAddress    : agentInfo.companyAddress || '',
      seniorityYear     : agentYear? +agentYear : 0,
      seniorityMonth    : agentMonth ? +agentMonth : 0,
      concept           : agentInfo.concept || '',
      experiences       : agentInfo.experiences  || '',
      awards            : agentInfo.awards || '',
      communicationStyle: agentInfo.communicationStyle || '',
      photoBase64       : ''
    };
    this.editInfoValue = {
      ...this._agentInfoSetting,
      expertise          : _.cloneDeep(this._agentInfoSetting.expertise),
      // TODO: ç¢ºèªå¾Œç«¯æ­¤æ¬„位後端應改為以" , "隔開 [Tomas, 2021/12/28]
      communicationStyle : this._agentInfoSetting.communicationStyle.split('、'),
    }
  }
  //////////////////////////////////////////////////////////////////////
  editAgentInfoSetting(): void {
    const editSettingInfo: any = {
      ...this.editInfoValue,
      communicationStyle: this.editInfoValue.communicationStyle.join('、'),
    }
    accountSettingService.editAgentInfoSetting(editSettingInfo).then((res: AgentInfoSetting) => {
      this.isInfoUpdate = true;
    });
  }
  backToInfo() {
    this.isInfoUpdate = false
    this.$router.push(`/agentInfo/${this.agentInfo.agentNo}`);
  }
  selectCommunicationStyles(): void {
    if (this.editInfoValue.communicationStyle.length > 2) {
            this.editInfoValue.communicationStyle.shift();
        }
  }
  alertFieldInfo(field: string): void {
    this.isAlertFieldInfo = true;
    switch(field) {
      case 'suitability':
        this.fieldInfoTitle = '匹配度';
        this.fieldInfoDesc = '匹配度是透過嚴選配對或快速篩選後,將每一位保險顧問資料進行比對後排序推薦給您的媒合數值,您可以作為選擇適合顧問的參考值。';
        break;
      case 'evaluation':
        this.fieldInfoTitle = '諮詢度表現';
        this.fieldInfoDesc = '諮詢度表現是將每一位保險顧問近一個月回覆諮詢數量進行比對後排序推薦給您的媒合數值。';
        break;
    }
  }
  ////////////////////////////////////////////////////////////
  get phoneValid(): boolean {
            const rule = /^09[0-9]{8}$/;
            return this.editInfoValue.phoneNumber
            ? rule.test(this.editInfoValue.phoneNumber) && _.isEqual(this.editInfoValue.phoneNumber.length,10)
            : true;
        }
  get isSubmitBtnDisabled(): boolean {
      const isFormValid =  this.editInfoValue.name
                        && this.editInfoValue.title
                        && this.editInfoValue.companyAddress
                        && this.editInfoValue.serveArea
                        && this.editInfoValue.concept
                        && this.editInfoValue.experiences
                        && this.editInfoValue.phoneNumber.length
                        && this.editInfoValue.seniorityYear
                        && this.editInfoValue.expertise.length
                        && this.editInfoValue.communicationStyle.length;
      return !isFormValid
  }
}
</script>
<style lang="scss" >
.edit-agent-info-page{
  .el-textarea__inner{
  font-size: 15px;
}
}
.pam-icon {
  font-size    : 15px;
  padding-right: 8px;
  color        : $PRUDENTIAL_GREY;
  &.icon--primary {
    color: $PRIMARY_RED;
  }
}
.pam-field {
  display: flex;
  flex-direction: column;
  .pam-field__label {
    display: flex;
    align-items: center;
    .pam-icon {
      font-size: 12px;
    }
    .pam-field__title {
      font-size  : 16px;
      font-weight: bold;
      display    : flex;
      align-items: center;
    }
  }
}
.pam-field-suitability {
  .el-progress-bar__inner {
    background-color: $LIGHT_BLUE !important;
  }
}
.pam-field-evaluation {
  .el-progress-bar__inner {
    background-color: $TEAL_GREEN!important;
  }
}
.pam-field-experts {
  display  : flex;
  flex-wrap: wrap;
}
.pam-progress__label {
  justify-content: space-between;
  flex-wrap  : wrap;
  line-height: 24px;
}
.account-confirm{
  display: flex;
  justify-content: center;
}
.seniority-input{
  width       : 50px;
  margin-right: 5px;
}
.el-input--suffix .el-input__inner {
  padding-right: 20px;
}
.el-textarea__inner{
  font-size: 18px;
}
</style>
PAMapp/pages/appointment/_appointmentId/close/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,316 @@
<template>
  <div class="appointment-client-detail-close-page">
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField label="歸檔方式" :labelSize="20">
          <SingleSelectBtn class="mt-10"
            :singleSelected.sync="appointmentCloseInfo.selectCloseOption"
            :options="closeOptions" />
        </UiField>
      </el-row>
      <template v-if="appointmentCloseInfo.selectCloseOption === contactStatus.DONE">
        <el-row
          type="flex"
          class="pam-paragraph" style="flex-direction: column">
          <UiField label="身分證字號/居留證字號" :labelSize="20" class="required">
            <input
              class="appointment-client-detail-close__input"
              :class="{'is-invalid':!identityIdValid}"
              v-model="appointmentCloseInfo.policyholderIdentityId"
              placeholder="請輸入"
              type="text">
          </UiField>
          <div class="error mt-5 mb-5" v-show="!identityIdValid">
            <span>身分證字號格式有誤</span>
          </div>
        </el-row>
        <el-row
          type="flex"
          class="pam-paragraph">
          <UiField label="商品代碼Plan Code" :labelSize="20" class="required">
            <input
              class="appointment-client-detail-close__input"
              v-model="appointmentCloseInfo.planCode"
              placeholder="請輸入"
              type="text">
          </UiField>
        </el-row>
        <el-row
          type="flex"
          class="pam-paragraph">
          <UiField label="進件時間" :labelSize="20" class="required">
            <DateTimePicker
              :defaultValue="appointmentCloseInfo.policyEntryDate"
              @changeDateTime="appointmentCloseDate = $event"></DateTimePicker>
          </UiField>
        </el-row>
      </template>
      <template v-if="appointmentCloseInfo.selectCloseOption === contactStatus.CLOSE">
        <el-row
          class="pam-paragraph">
          <UiField label="未成交原因" :labelSize="20" class="required">
            <UiSelect :closeReason.sync="appointmentCloseInfo.closedReason"
              :options="appointmentFailReason"/>
          </UiField>
          <input
            v-if="appointmentCloseInfo.closedReason === 'other'
                || appointmentCloseInfo.closedReason === 'no_suitable_commodity'"
            class="appointment-client-detail-close__input mt-10"
            v-model="appointmentCloseInfo.closedOtherReason"
            placeholder="請輸入原因,限50字。"
            type="text">
        </el-row>
      </template>
      <el-row
        type="flex"
        class="pam-paragraph">
        <UiField label="備註" :labelSize="20">
          <el-input
            type="textarea"
            :rows="3"
            placeholder="請輸入"
            v-model="appointmentCloseInfo.remark"
            resize="none">
          </el-input>
        </UiField>
      </el-row>
      <el-row
        type="flex"
        justify="center"
        class="pam-paragraph">
        <el-button @click="$router.go(-1)">取消</el-button>
        <el-button @click="closeAppointment" :disabled="isSubmitBtnDisabled">確認</el-button>
      </el-row>
      <PopUpFrame :isOpen.sync="isShowSuccessAlert">
        <div class="text--middle invite-review">
          <div  class="mb-30 mt-10">結案成功</div>
          <el-button type="primary" @click="closeAlert">確定</el-button>
        </div>
      </PopUpFrame>
  </div>
</template>
<script lang="ts">
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component } from 'vue-property-decorator';
import { Appointment, ToCloseAppointment, ToDoneAppointment } from '~/shared/models/appointment.model';
import appointmentService from '~/shared/services/appointment.service';
import { appointmentFailReasonList } from '~/shared/const/appointment-fail-reason-list';
import { ContactStatus } from '~/shared/models/enum/contact-status';
const appointmentStore = namespace('appointment.store');
@Component
export default class AppointmentDetailCloseComponent extends Vue {
  @appointmentStore.Action
  updateAppointmentDetail!: (appointmentId: number) => Appointment;
  @appointmentStore.State('appointmentDetail')
  appointmentDetail!: Appointment;
  contactStatus = ContactStatus;
  appointmentCloseDate = '';
  isShowSuccessAlert = false;
  appointmentCloseInfo = {
    closedOtherReason     : '',
    closedReason          : 'other',
    planCode              : '',
    policyEntryDate       : '',
    policyholderIdentityId: '',
    remark                : '',
    selectCloseOption     : this.contactStatus.DONE,
  };
  closeOptions = [
    {
      title:'成交',
      label: this.contactStatus.DONE,
    },
    {
      title:'未成交',
      label: this.contactStatus.CLOSE,
    }
  ];
  appointmentFailReason = appointmentFailReasonList;
  //////////////////////////////////////////////////////////////////////
  mounted() {
    const appointmentId = +this.$route.params.appointmentId;
    const closedInfo = this.appointmentDetail.appointmentClosedInfo;
    if (this.appointmentDetail.id === appointmentId
        && (this.appointmentDetail.communicateStatus === this.contactStatus.DONE
        || this.appointmentDetail.communicateStatus === this.contactStatus.CLOSE
        || this.appointmentDetail.communicateStatus === this.contactStatus.CANCEL)
        ) {
        this.appointmentCloseInfo = {
        closedOtherReason     : closedInfo?.closedOtherReason,
        closedReason          : closedInfo?.closedReason,
        planCode              : closedInfo?.planCode,
        policyEntryDate       : closedInfo?.policyEntryDate,
        policyholderIdentityId: closedInfo?.policyholderIdentityId,
        remark                : closedInfo?.remark,
        selectCloseOption     : this.appointmentDetail.communicateStatus === this.contactStatus.DONE
                                ? this.contactStatus.DONE
                                : this.contactStatus.CLOSE
      };
      this.appointmentCloseDate = closedInfo?.policyEntryDate;
    }
  }
  //////////////////////////////////////////////////////////////////////
  closeAppointment(): void {
    const appointmentId = +this.$route.params.appointmentId;
    if (this.appointmentCloseInfo.selectCloseOption === this.contactStatus.DONE) {
      const toDoneAppointment: ToDoneAppointment = {
        appointmentId         : appointmentId,
        contactStatus         : this.contactStatus.DONE,
        planCode              : this.appointmentCloseInfo.planCode,
        policyEntryDate       : this.appointmentCloseDate,
        policyholderIdentityId: this.appointmentCloseInfo.policyholderIdentityId,
        remark                : this.appointmentCloseInfo.remark,
      }
      appointmentService.closeAppointment(toDoneAppointment).then((_) => this.updateAppointmentDetail(appointmentId));
      this.isShowSuccessAlert = true;
    } else {
      const toCloseAppointment: ToCloseAppointment = {
        appointmentId    : appointmentId,
        closedOtherReason: this.appointmentCloseInfo.closedOtherReason,
        closedReason     : this.appointmentCloseInfo.closedReason,
        contactStatus    : this.contactStatus.CLOSE,
        remark           : this.appointmentCloseInfo.remark,
      }
      appointmentService.closeAppointment(toCloseAppointment).then((_) => {
        this.updateAppointmentDetail(appointmentId);
        this.isShowSuccessAlert = true;
      });
    }
  }
  closeAlert(){
    this.isShowSuccessAlert = false ;
    this.$router.push(`/myAppointmentList/contactedList`);
  }
  checkIdentityId (id) {
    const tab = "ABCDEFGHJKLMNPQRSTUVXYWZIO";
    const A1 = [ 1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3 ];
    const A2 = [ 0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5 ];
    const Mx = [ 9,8,7,6,5,4,3,2,1,1 ];
    if ( id.length != 10 ) return false;
    let i = tab.indexOf( id.charAt(0) );
    if ( i === -1 ) return false;
    let sum = A1[i] + A2[i]*9;
    for ( i=1; i<10; i+=1 ) {
      let v = parseInt( id.charAt(i) );
      if (i === 1 && isNaN(v)) {
        switch(id.charAt(i)) {
          case 'A':
            v = 0;
            break;
          case 'B':
            v = 1;
            break;
          case 'C':
            v = 2;
            break;
          case 'D':
            v = 3;
            break;
          default:
            return false
        }
      } else if ( i === 1  && ([1,2,8,9].indexOf(v) === -1) ) {
        return false
      }
      if ( i > 1 && isNaN(v) ) return false;
      sum = sum + v * Mx[i];
    }
    if ( sum % 10 != 0 ) return false;
    return true;
  }
  get isSubmitBtnDisabled() {
    const {
      selectCloseOption,
      policyholderIdentityId,
      planCode,
      closedReason,
      closedOtherReason,
      remark
    } = this.appointmentCloseInfo;
    if (selectCloseOption === this.contactStatus.DONE) {
      return !policyholderIdentityId || !this.identityIdValid || !planCode || !this.appointmentCloseDate
    } else if (closedReason === 'other' || closedReason === 'no_suitable_commodity') {
      return !closedOtherReason
    }
    return false
  }
  get identityIdValid() {
    const identityId = this.appointmentCloseInfo.policyholderIdentityId;
    return identityId ? this.checkIdentityId(identityId) : true;
  }
}
</script>
<style lang="scss" scoped>
.appointment-close__remark,
.appointment-client-detail-close__input {
  border-radius: 5px;
  border   : 1px solid #707070;
  font-size: 20px;
  padding  : 10px 20px;
  width    : 100%;
  box-sizing: border-box;
  &::placeholder {
    color: $MID_GREY;
  }
  &.is-invalid {
    border-color: $PRIMARY_RED !important;
  }
}
.invite-review{
  display: flex;
  flex-direction: column;
  align-items: center;
}
 .error {
    @extend .smTxt_bold;
    @extend .text--primary;
    height: 16px;
  }
  .required {
    position: relative;
    &::before {
      content: '*';
      font-size: 15px;
      font-weight: bold;
      position: absolute;
      color: #FF0000;
      transform: translateX(-2px);
      z-index: 5;
    }
  }
</style>
PAMapp/pages/appointment/_appointmentId/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,294 @@
<template>
  <div class="appointment-client-detail-page">
    <div class="date-detail">
      <div>{{ appointmentDetail.appointmentDate | formatDate }}預約</div>
      <div>{{ appointmentDetail.consultantReadTime | formatDate }}
        <span v-if="appointmentDetail.consultantReadTime">已讀</span>
      </div>
    </div>
    <AppointmentProgress
      class="mt-10"
      :currentStep="appointmentProgress"
    ></AppointmentProgress>
    <section class="client-detail">
      <div class="client-detail-info mb-30">
        <div class="client-detail-info__avatar">
          <div class="circle">
            {{ appointmentDetail.name || 'NO NAME' }}
            <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>
        <div class="client-detail-info__information">
          <div>{{ appointmentDetail.age | toAgeLabel }}</div>
          <div>{{ appointmentDetail.phone }}</div>
          <div class="text--underline text--break-all">
            {{ appointmentDetail.email }}
          </div>
        </div>
      </div>
      <div class="client-detail-demand mt-10">
        <div class="client-detail-demand__demand-list mb-10">
          <div class="client-detail-demand__demand-list-label">需求</div>
          <div class="client-detail-demand__demand-list-content">{{ appointmentDetail.requirement }}</div>
        </div>
        <div class="client-detail-demand__demand-list">
          <div class="client-detail-demand__demand-list-label">聯絡<br />時段</div>
          <div class="client-detail-demand__demand-list-content">
            <div v-for="(hopeContactTime, index) in hopeContactTimeList" :key="index"
              :class="{'mt-10': index > 0, 'pb-10': true, 'hope-contact-time__line': index + 1 < hopeContactTimeList.length }">
              <div v-for="(item, index) in getHopeContactTimeContent(hopeContactTime)" :key="index" :class="{'mt-10': index < 0 }">
                {{ item }}
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="client-detail-action" v-if="showWhenAppointmentHasClosed">
        <el-button @click="inviteReview">發送滿意度</el-button>
      </div>
      <div class="client-detail-action" v-if="showWhenAppointmentHasContacted">
        <el-button @click="closeAppointment" >結案</el-button>
        <el-button @click="sendMsg" style="margin-left: 0px">通知約訪</el-button>
      </div>
    </section>
     <div class="client-detail-action btn-center" v-if="showWhenAppointmentHasCreate">
        <el-button @click="sendMsg">傳送約訪通知</el-button>
      </div>
    <template v-if="showWhenAppointmentHasClosed">
      <AppointmentClosedInfo :appointmentDetail="appointmentDetail" />
    </template>
    <InterviewMsg
      :isVisible.sync="isVisibleDialog"
      :client="appointmentDetail">
    </InterviewMsg>
    <section class="mt-30" v-if="!showWhenAppointmentHasCreate">
      <AppointmentInterviewList :interviewList="appointmentDetail.interviewRecordDTOs" />
    </section>
    <section class="mt-30" v-if="!showWhenAppointmentHasCreate">
      <AppointmentRecordList :noticeLogs="appointmentDetail.appointmentNoticeLogs" />
    </section>
    <PopUpFrame :isOpen.sync="isShowInviteReviewDialog">
          <div class="text--middle invite-review">
            <div class="mb-30 mt-10">已發送滿意度</div>
            <div class="text--primary text--middle cursor--pointer text--underline" @click="isShowInviteReviewDialog = false" :size="'250px'">我知道了</div>
          </div>
        </PopUpFrame>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';
import { namespace } from 'nuxt-property-decorator';
import { Appointment } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
import reviewsService from '~/shared/services/reviews.service';
const appointmentStore = namespace('appointment.store');
@Component
export default class AppointmentDetailComponent extends Vue {
  @appointmentStore.State('appointmentDetail')
  appointmentDetail!: Appointment;
  @appointmentStore.Getter('appointmentProgress')
  appointmentProgress!: ContactStatus;
  isVisibleDialog = false;
  isShowInviteReviewDialog = false ;
  interviewTxt = "";
  contactStatus = ContactStatus;
  //////////////////////////////////////////////////////////////////////
  closeAppointment(): void {
    this.$router.push(`/appointment/${this.appointmentDetail.id}/close`);
  }
  sendMsg():void {
    this.isVisibleDialog = true;
  }
  editAppointmentHasClosed(): void{
    this.$router.push(`/appointment/${this.appointmentDetail.id}/close`);
  }
  get showWhenAppointmentHasClosed(): boolean {
    return this.appointmentDetail.communicateStatus === this.contactStatus.DONE
        || this.appointmentDetail.communicateStatus === this.contactStatus.CLOSE
        || this.appointmentDetail.communicateStatus === this.contactStatus.CANCEL;
  }
  get showWhenAppointmentHasCreate(): boolean {
    return this.appointmentDetail.communicateStatus === this.contactStatus.PICKED
        || this.appointmentDetail.communicateStatus === this.contactStatus.RESERVED;
  }
  get showWhenAppointmentHasContacted() :boolean {
    return this.appointmentDetail.communicateStatus === this.contactStatus.CONTACTED;
  }
  get hopeContactTimeList(): any[] {
    return this.appointmentDetail.hopeContactTime.split("','")
  }
  getHopeContactTimeContent(hopeContactTimeString: string): string[] {
    const result = hopeContactTimeString.replaceAll("'", '').split('、');
    return result;
  }
  inviteReview(): void {
    reviewsService.sendSatisfactionToClient(this.appointmentDetail.id).then(res => {
        this.isShowInviteReviewDialog = true;
    });
  }
}
</script>
<style lang="scss">
.appointment-client-detail-page{
  .date-detail{
    display        : flex;
    font-size      : 16px;
    color          : #68737A;
    justify-content: space-between;
    margin-bottom  : 2px;
  }
  .client-detail{
    background-color: #fff;
    margin-top:10px;
    padding: 17px 21px;
    .client-detail-info {
      display: flex;
      .client-detail-info__avatar{
        display: flex;
        margin-right: 22px;
        .circle{
          height: 100px;
          width: 100px;
          border-radius: 50%;
          background-image: url('~/assets/images/appointment/avatar_bg.svg');
          position: relative;
          display: flex;
          justify-content: center;
          align-items: center;
          .sm-circle{
            position: absolute;
            height: 30px;
            width: 30px;
            border-radius: 50%;
            background-color: #fff;
            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;
            }
          }
        }
      }
      .client-detail-info__information{
        font-size: 20px;
        line-height: 1.6;
      }
    }
    .client-detail-demand{
      background-color: #fff;
      font-size: 20px;
      display: flex;
      flex-direction: column;
      .client-detail-demand__demand-list{
        display: flex;
      }
      .client-detail-demand__hope-contact-time{
        display: flex;
      }
      .client-detail-demand__demand-list-label {
        @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;
      }
    }
    .client-detail-action {
      margin-left: 50px;
    }
  }
}
.invite-review{
    align-items   : center;
    display       : flex;
    flex-direction: column;
  }
.close-appointment-detail{
  background-color: #fff;
  display: flex;
  margin-top: 30px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  font-size: 20px;
}
.close-appointment-detail-nav{
  display: flex;
  justify-content: space-between;
  flex: 1;
}
.hope-contact-time__line {
  border-bottom: 1px solid #CCCCCC;
}
.btn-center{
    display: flex;
    justify-content: center;
  }
</style>
PAMapp/pages/appointment/_appointmentId/interview/_interviewId/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,14 @@
<template>
  <InterviewAdd></InterviewAdd>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator';
@Component
export default class EditAppointmentInterview extends Vue {
}
</script>
<style lang="scss" scoped>
</style>
PAMapp/pages/appointment/_appointmentId/interview/new/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,55 @@
<template>
  <InterviewAdd></InterviewAdd>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator';
@Component
export default class NewAppointmentInterview extends Vue {
}
</script>
<style lang="scss" >
.edit-appointment-record {
    padding-left : 10px;
    padding-right: 10px;
    .edit-appointment-record-date{
        color          : #68737A;
        display        : flex;
        justify-content: space-between;
        margin-bottom  : 26px;
    }
}
.date-input {
    align-items     : center;
    background-color: #fff;
    border          : 1px solid #707070;
    border-radius   : 5px;
    display         : flex;
    font-size       : 20px;
    height          : 46px;
    margin-bottom   : 30px;
    padding-left    : 20px;
    padding-right   : 20px;
}
.icon {
    color          : $PRIMARY_RED;
    display        : flex;
    flex           : 1;
    justify-content: flex-end;
}
.edit-appointment-record-btn{
    margin-top: 30px;
    display: flex;
    justify-content: center;
}
.required {
  position: relative;
  &::before {
      content: '*';
      position: absolute;
      color: #FF0000;
      transform: translate(-12px, 0);
  }
}
</style>
PAMapp/pages/appointment/_appointmentId/interviewList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,38 @@
<template>
    <div>
        <div class="text--right mb-30">
            <div class="pam-link-button--lg"
            @click="addInterview">+新增</div>
        </div>
        <InterviewCard :interviewList="appointmentDetail.interviewRecordDTOs"></InterviewCard>
    </div>
</template>
<script lang="ts">
import { Component, namespace, Vue } from "nuxt-property-decorator";
import { Appointment } from "~/shared/models/appointment.model";
const appointmentStore = namespace('appointment.store');
@Component
export default class InterviewList extends Vue {
    @appointmentStore.State
    appointmentDetail!: Appointment;
    appointmentId!: number;
    ////////////////////////////////////////////////////////
    mounted() {
      this.appointmentId = +this.$route.params.appointmentId;
    }
    ////////////////////////////////////////////////////////
    addInterview(): void {
        this.$router.push(`/appointment/${this.appointmentId}/interview/new`);
    }
}
</script>
PAMapp/pages/appointment/_appointmentId/recordList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,32 @@
<template>
    <InterviewRecordCard :noticeLogsList="displayLogs"></InterviewRecordCard>
</template>
<script lang="ts">
import { Component, namespace, Vue, Watch } from "nuxt-property-decorator";
import { Appointment, NoticeLogs } from "~/shared/models/appointment.model";
const appointmentStore = namespace('appointment.store');
@Component
export default class RecordList extends Vue {
    @appointmentStore.State
    appointmentDetail!: Appointment;
    displayLogs: NoticeLogs[] = [];
    ////////////////////////////////////////////////////////
    @Watch('appointmentDetail', {immediate: true})
    onAppointmentDetailChange() {
      if (this.appointmentDetail?.appointmentNoticeLogs.length) {
        this.displayLogs = this.appointmentDetail?.appointmentNoticeLogs
                            .map((i) => ({ ...i, sortDate: new Date(i.createdDate)}))
                            .sort((preItem, nextItem) => +nextItem.sortDate - +preItem.sortDate);
      }
    }
}
</script>
PAMapp/pages/appointmentAgenda/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,68 @@
<template>
  <div>
    <div class="mdTxt">即將約訪排程(3)</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 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 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 } from 'nuxt-property-decorator';
@Component
export default class Agenda extends Vue {
    //////////////////////////////////////////////////////////////////////
}
</script>
<style lang="scss" scoped>
</style>
PAMapp/pages/consultantLogin/index.vue
@@ -61,36 +61,54 @@
  import { Role } from '~/shared/models/enum/Role';
  import messageBoxService from '~/shared/services/message-box.service';
  import loginService from '~/shared/services/login.service'
import { AgentInfo } from '~/shared/models/agent-info.model';
  const loginStore  = namespace('login.store');
  const roleStorage = namespace('localStorage');
  @Component({
    layout: 'home'
  })
  export default class ConsultantLogin extends Vue {
    @roleStorage.Mutation storageIdToken!: (token: string) => void;
    @roleStorage.Mutation storageRole!: (role: string) => void;
    @roleStorage.Mutation storageConsultantId!:(id:string) => void;
    @roleStorage.Mutation
    storageIdToken!: (token: string) => void;
    @roleStorage.Mutation
    storageRole!: (role: string) => void;
    @roleStorage.Mutation
    storageConsultantId!:(id:string) => void;
    @loginStore.Action
    getLoginConsultantDetail!: (agentNo: string) => Promise<AgentInfo>;
    consultantDto = {
      password: '',
      username: '',
    };
    imgSrc = '';
    isRememberUserName = false;
    isShowPassword = false;
    imgSrc = '';
    verificationCode='';
    consultantDto = {
      username: '',
      password: '',
    }
    ////////////////////////////////////////////////////////////////////
    mounted() {
      this.getInitUserName();
      this.regenerateImgOfVerification();
    };
    get isAlreadyDone():boolean{
      return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password);
    private getInitUserName(): void {
      const username = localStorage.getItem('consultantUserName')
      if (username) {
        this.consultantDto.username = username;
        this.isRememberUserName = true;
      }
    }
    ////////////////////////////////////////////////////////////////////
    public regenerateImgOfVerification(): void {
      loginService.getImgOfVerification().then( imgOfBase64 =>
        this.imgSrc = imgOfBase64
@@ -103,23 +121,22 @@
    }
    public sendInfo():void{
      this.isAlreadyDone ? this.verify() : messageBoxService.showErrorMessage('請確認帳號、密碼以及驗證碼是否填寫完畢');
      this.isAlreadyDone
        ? this.verify()
        : messageBoxService.showErrorMessage('請確認帳號、密碼以及驗證碼是否填寫完畢');
    }
    get isAlreadyDone():boolean{
      return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password);
    }
    ////////////////////////////////////////////////////////////////////
    private getInitUserName(): void {
      const username = localStorage.getItem('consultantUserName')
      if (username) {
        this.consultantDto.username = username;
        this.isRememberUserName = true;
      }
    }
    private verify():void{
      loginService.getVerificationStatus(this.verificationCode).then( verifySuccess => {
        if(verifySuccess.data){
          this.loginWithConsultant()
          this.loginWithConsultant();
        }else{
          this.clearValue();
          this.regenerateImgOfVerification();
@@ -130,6 +147,7 @@
    private loginWithConsultant(): void {
      loginService.logInToConsultant(this.consultantDto).then(res => {
        this.getLoginConsultantDetail(this.consultantDto.username);
        this.storageIdToken(res.data.id_token);
        this.storageRole(Role.ADMIN);
        this.storageConsultantId(this.consultantDto.username)
PAMapp/pages/index.vue
@@ -39,14 +39,117 @@
        <ConsultantSwiper :agents="recommendList"></ConsultantSwiper>
      </div>
    </div>
    <Ui-Dialog
        :isVisible.sync="isShowAppointmentDialog"
        :width="appointmentDialogWidth"
        class="pam-myDemand-dialog pam-dialog-reserved"
      >
        <div v-if="appointmentDetail">
            <h5 class="subTitle text--center mb-30">預約成功</h5>
            <p class="smTxt">{{appointmentDetail.appointmentDate | formatDate}}</p>
            <div class="reserved-info">
                <p>姓名:{{appointmentDetail.name}}</p>
                <p>電話:{{appointmentDetail.phone}}</p>
                <p>Email:{{appointmentDetail.email}}</p>
                <p>性別:{{gender}}</p>
                <p>年齡:{{appointmentDetail.age | toAgeLabel }}</p>
                <p>職業:{{appointmentDetail.job}}</p>
                <p>需求:{{appointmentDetail.requirement.split(',').join('、')}}</p>
                <p
                    v-for="(item, index) in hopeContactTime"
                    :key="index"
                >連絡時段{{index + 1 | formatNumber}}:{{ item | formatHopeContactTime }}</p>
                <div v-if="appointmentDetail.satisfactionScore">
                    <div class="mdTxt mt-10 mb-10">滿意度</div>
                    <el-rate
                    :value="appointmentDetail.satisfactionScore"
                    class="pam-myDemand-dialog__rate"
                    disabled>
                    </el-rate>
                </div>
            </div>
            <div v-if="notScoreAppointmentYet" class="reserved-btn">
                <el-button type="primary"
                    @click.native="isShowReviewDialog = true">給予滿意度評分</el-button>
            </div>
        </div>
      </Ui-Dialog>
      <PopUpFrame
        :isOpen.sync="isShowReAppointmentDialog"
        @closePopUp="removeUrlQueryParameter('notContactAppointmentIdFromMsg')"
      >
          <div class="pam-dialog-review">
              <div class="mt-30 text--middle" v-if="agentInfo">
                å¾ˆæŠ±æ­‰ï¼æ‚¨é ç´„çš„<span class="text--bold">{{ consultantName }}</span>顧問正忙碌中,請您取消預約並改選其他顧問
              </div>
                <el-row
                  type="flex"
                  class="mt-50"
                  justify="center">
                  <el-button
                      type="primary"
                      @click="reAppointment">取消預約再改選其他顧問</el-button>
                </el-row>
                <el-row
                  type="flex"
                  class="mt-20"
                  justify="center">
                  <el-button
                      class="outline_btn"
                      @click="cancelAppointment">取消預約</el-button>
                </el-row>
          </div>
      </PopUpFrame>
      <PopUpFrame
        :isOpen.sync="isShowReviewDialog"
        @closePopUp="removeUrlQueryParameter('appointmentId')"
      >
          <div class="mdTxt pam-dialog-review">
              ä¿éšªé¡§å•æ»¿æ„åº¦
              <span class="hint">選取星星</span>
              <div class="mt-30 review-content" v-if="agentInfo">
                  <UiAvatar :size="80" :agentNo="agentInfo.agentNo"></UiAvatar>
                  <div class="review-text">對於顧問
                      <span class="text--primary">{{agentInfo.name}}</span>
                      çš„æ•´é«”服務,您給予幾顆星評價?
                  </div>
              </div>
              <div class="review-score">
                  <el-rate v-model="inputScore" class="pam-rate mt-30"></el-rate>
              </div>
              <div class="review-btn">
                  <el-button
                      type="primary"
                      :disabled="!inputScore"
                      @click="userReviewsConsultants">送出</el-button>
              </div>
          </div>
      </PopUpFrame>
  </div>
</template>
<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 { 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 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');
  @Component({
    layout: 'home'
@@ -59,10 +162,14 @@
    @State('myConsultantList')
    myConsultantList!: Consultant[];
    @roleStorage.Getter
    isAdminLogin!: boolean;
    @Action
    storeRecommendList!: any;
    @Action storeConsultantList!: any;
    @Action
    storeConsultantList!: any;
    @localStorage.Mutation
    storageClearQuickFilter!: () => void;
@@ -70,18 +177,92 @@
    @localStorage.Mutation
    storageClearRecommendConsultant!: () => void;
    @localStorage.Getter
    currentSatisfactionIdFromMsg!: string;
    @localStorage.Getter
    currentNotContactAppointmentIdFromMsg!: string;
    @localStorage.Mutation
    storageClearSatisfactionIdFromMsg!: () => void;
    @localStorage.Mutation
    storageClearNotContactAppointmentIdFromMsg!: () => void;
    @localStorage.Mutation
    storageStrickQueryItem!: (strictQueryDto: StrictQueryParams) => void;
    consultantList: Consultant[] = [];
    appointmentDialogWidth    = '';
    inputScore                = 0;
    isShowAppointmentDialog   = false;
    isShowReAppointmentDialog = false;
    isShowReviewDialog        = false;
    consultantName = '';
    contactStatus = ContactStatus;
    appointmentDetail: Appointment = {
      age               : '',
      agentNo           : '',
      appointmentClosedInfo: {} as AppointmentClosedInfo,
      appointmentDate   : '',
      appointmentMemoList: [],
      appointmentNoticeLogs: [],
      communicateStatus : this.contactStatus.PICKED,
      consultantReadTime: '',
      consultantViewTime: '',
      contactTime       : '',
      contactType       : '',
      customerId        : 0,
      email             : '',
      gender            : '',
      hopeContactTime   : '',
      interviewRecordDTOs: [],
      id                : 0,
      job               : '',
      lastModifiedDate  : '',
      name              : '',
      otherRequirement  : '',
      phone             : '',
      requirement       : '',
      satisfactionScore : 0,
    };
    agentInfo: Consultant = {
      agentNo            : '',
      name               : '',
      img                : '',
      expertise          : [],
      avgScore           : 0,
      contactStatus      : '',
      createTime         : '',
      updateTime         : '',
      customerViewTime   : '',
      role               : '',
      seniority          : '',
      appointments       : []
    };
    //////////////////////////////////////////////////////////////////////
    mounted() {
      if (!this.recommendList?.length) {
        this.storeRecommendList();
      if (this.isAdminLogin) {
        this.$router.push('/myAppointmentList/appointmentList');
      } else {
        if (!this.recommendList?.length) {
          this.storeRecommendList();
        }
        this.storeConsultantList();
        this.storageClearQuickFilter();
        this.storageClearRecommendConsultant();
      }
      this.storeConsultantList();
      this.storageClearQuickFilter();
      this.storageClearRecommendConsultant();
    }
    destroyed() {
      this.removeUrlQueryParameter();
    }
    //////////////////////////////////////////////////////////////////////
@@ -89,15 +270,133 @@
    @Watch('myConsultantList')
    onMyConsultantListChange() {
      this.consultantList = (this.myConsultantList || [])
        .filter(item => item.contactStatus !== 'contacted')
        .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)}))
        .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate)
        .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate);
      if (this.currentNotContactAppointmentIdFromMsg) {
        this.autoOpenAppointmentBy('askReAppointment', +this.currentNotContactAppointmentIdFromMsg);
        return;
      }
      if (this.currentSatisfactionIdFromMsg) {
        this.autoOpenAppointmentBy('inviteReviewConsultant',+this.currentSatisfactionIdFromMsg);
        this.storageClearSatisfactionIdFromMsg();
        return;
      }
    }
    private autoOpenAppointmentBy(reason: string, targetAppointmentId: number): void {
        const setAgentInfo = new Promise((resolve, reject) => {
          this.agentInfo = this.myConsultantList.filter(item => {
            const appointmentIndex = item.appointments?.findIndex(i => i.id === targetAppointmentId);
            return appointmentIndex !== undefined && appointmentIndex > -1;
          })[0];
          if (this.agentInfo) {
            myConsultantService.getConsultantDetail(this.agentInfo.agentNo).then((res) => resolve(res));
          }
        });
        const setAppointment = new Promise((resolve, reject) => {
           appointmentService.getAppointmentDetail(targetAppointmentId).then((res) => resolve(res));
        });
        Promise.all([setAgentInfo, setAppointment]).then((values) => {
          const agentInfo = values[0] as AgentInfo;
          const appointmentInfo = values[1] as Appointment;
          this.consultantName = agentInfo.name;
          this.appointmentDetail = appointmentInfo;
          this.appointmentDialogWidth = 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(',');
        this.storeConsultantList();
        this.storageStrickQueryItem({ requirements: requirements });
        this.storageClearNotContactAppointmentIdFromMsg();
        this.removeUrlQueryParameter('notContactAppointmentIdFromMsg');
        this.$router.push('/recommendConsultant');
      });
    }
    cancelAppointment(): void {
      appointmentService.cancelAppointment(this.appointmentDetail.id).then(() => {
        this.storeConsultantList();
        this.storageClearNotContactAppointmentIdFromMsg();
        this.removeUrlQueryParameter('notContactAppointmentIdFromMsg');
        this.$router.push('');
      });
    }
    userReviewsConsultants() {
      const reviewParams: UserReviewsConsultantsParams = {
            appointmentId: this.appointmentDetail.id,
            score: this.inputScore,
        }
        this.appointmentDetail.satisfactionScore = this.inputScore;
        reviewsService.userReviewsConsultants(reviewParams).then((res) => {
            this.isShowReviewDialog = false;
        });
    }
    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);
    }
    ///////////////////////////////////////////////////////////////////////////////
    get gender() {
        if (this.appointmentDetail.gender) {
            return this.appointmentDetail.gender === 'male' ? '男性' : '女性';
        }
        return ''
    }
    get hopeContactTime() {
        const contactList = this.appointmentDetail.hopeContactTime
            .split("'").map((item: any) => item.slice(0, item.length));
        return contactList.filter((item: any) => !!item && item !== ",")
    }
    get notScoreAppointmentYet(): boolean {
      if (this.appointmentDetail.communicateStatus === 'closed' || this.appointmentDetail.communicateStatus === 'done') {
        return !this.appointmentDetail.satisfactionScore;
      };
      return false;
    }
  }
@@ -182,5 +481,4 @@
      max-width: 335px;
    }
  }
</style>
PAMapp/pages/login/index.vue
@@ -64,7 +64,7 @@
              <el-row type="flex" justify="space-between">
                  <div class="mdTxt">輸入驗證碼</div>
                  <div class="otp-count-timer">
                    {{phoneCounter}}
                    {{counterTime(otpCounterSec)}}
                  </div>
              </el-row>
@@ -115,7 +115,7 @@
              <el-row type="flex" justify="space-between">
                  <div class="mdTxt">輸入驗證碼</div>
                  <div class="otp-count-timer">
                    {{emailOtpCounter}}
                    {{counterTime(emailCounterSec)}}
                  </div>
              </el-row>
@@ -171,7 +171,7 @@
        :isOpen.sync="registerDialogVisible"
        :dialogWidth="'90%'"
        class="pam-register-dialog"
        @closePopUp="isReadContract = false"
        @closePopUp="isReadContract = false;agreeContract = false"
      >
          <div class="subTitle text--center mb-20">歡迎新使用者</div>
          <el-row>
@@ -343,13 +343,18 @@
import { RegisterInfo } from '~/shared/models/registerInfo';
import loginService from '~/shared/services/login.service';
import messageBoxService from '~/shared/services/message-box.service';
import otpService, { OtpStorageName } from '~/shared/services/otp.service';
const roleStorage = namespace('localStorage');
@Component
export default class Login extends Vue {
  @roleStorage.Mutation storageIdToken!: (token:string) => void;
  @roleStorage.Mutation storageRole!: (role:string) => void;
  @roleStorage.Mutation
  storageIdToken!: (token:string) => void;
  @roleStorage.Mutation
  storageRole!: (role:string) => void;
  @Ref('contract') readonly contract!: any;
  connectDevice: 'MOBILE' | 'EMAIL' = 'MOBILE';
@@ -360,7 +365,7 @@
  otpCounterSec = 300;
  otpResendCounter = 30;
  otpInterval: any;
  phoneOtpInfo!: OtpInfo;
  phoneOtpIndexKey!: string;
  email = '';
  onEmailVerifyResendStatus: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP';
@@ -368,7 +373,7 @@
  emailResendCounter = 30;
  emailOtpCode = '';
  emailResendInterval: any;
  emailOtpInfo!: OtpInfo;
  emailOtpIndexKey!: string;
  autoRedirectCounter = 3;
  autoRedirectInterval: any;
@@ -388,24 +393,45 @@
  previousPath = '';
  /////////////////////////////////////////////////////
  mounted() {
    const phoneOtpTime = localStorage.getItem('phoneOtpTime');
    const emailOtpTime = localStorage.getItem('emailOtpTime');
    const parsePhoneOtpTime = phoneOtpTime ? JSON.parse(phoneOtpTime) : '';
    const parseEmailOtpTime = emailOtpTime ? JSON.parse(emailOtpTime) : '';
    if (parsePhoneOtpTime && parsePhoneOtpTime.contactType === 'SMS') {
      this.phoneDiffTime(parsePhoneOtpTime);
    }
    if (parseEmailOtpTime && parseEmailOtpTime.contactType === 'EMAIL') {
      this.emailDiffTime(parseEmailOtpTime);
    }
  }
  beforeRouteEnter (to, from, next) {
      next(vm => {
        console.log(from.path, 'beforeRouteEnter');
        vm.previousPath = from.path;
      })
  }
  mounted() {
    this.parsePhoneOtpTimeFromStorage();
    this.parseEmailOtpTimeFromStorage();
  }
  private parsePhoneOtpTimeFromStorage() {
    const parsePhoneOtpTime = otpService.parseOtpTime(OtpStorageName.PHONE);
    const diffSecs = otpService.diffOtpTime(OtpStorageName.PHONE, this.otpCounterSec);
    if (parsePhoneOtpTime && diffSecs) {
      this.otpResendCounter = diffSecs < 30 ? 30 - diffSecs : 0;
      this.otpCounterSec -= diffSecs;
      this.phoneNumber = parsePhoneOtpTime.phone ? parsePhoneOtpTime.phone : '';
      this.onPhoneVerifyStep = 'INPUT_OTP';
      this.phoneOtpIndexKey = parsePhoneOtpTime.indexKey;
      this.startOtpCount('MOBILE');
    }
  }
  private parseEmailOtpTimeFromStorage() {
    const parseEmailOtpTime = otpService.parseOtpTime(OtpStorageName.EMAIL);
    const diffSecs = otpService.diffOtpTime(OtpStorageName.EMAIL, this.emailCounterSec);
    if (parseEmailOtpTime && diffSecs) {
      this.emailResendCounter =  diffSecs < 30 ? 30 - diffSecs : 0;
      this.emailCounterSec -= diffSecs;
      this.email = parseEmailOtpTime.email ? parseEmailOtpTime.email : '';
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpIndexKey = parseEmailOtpTime.indexKey;
      this.startOtpCount('EMAIL');
    }
  }
  destroyed() {
@@ -417,87 +443,28 @@
  //////////////////////////////////////////////////////////
  detectContractReadStatus(event: any): void {
    const scrollTop = Math.round(event.target.scrollTop);
    const height = event.target.scrollHeight - event.target.clientHeight;
    if (Math.floor(scrollTop/10) === (Math.floor(height/10))) {
      this.isReadContract = true;
    }
  };
  get isSubmitBtnDisabled(): boolean {
    return this.connectDevice === 'MOBILE'
      ? (!this.otpCode || !this.phoneNumber || !this.phoneValid || !this.otpCounterSec)
      : (!this.emailOtpCode || !this.email || !this.emailValid || !this.emailCounterSec)
  }
  get phoneCounter() {
    let min = Math.floor(this.otpCounterSec / 60);
    let sec = Math.floor(this.otpCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get emailOtpCounter() {
    let min = Math.floor(this.emailCounterSec / 60);
    let sec = Math.floor(this.emailCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get showPhoneOtpCodeField(): boolean {
    return this.connectDevice === 'MOBILE' && this.onPhoneVerifyStep === 'INPUT_OTP';
  };
  get showEmailVerifyField(): boolean {
    return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus === 'CAN_RESEND';
  };
  get phoneValid() {
    const rule = /^09[0-9]{8}$/;
    return this.phoneNumber ? rule.test(this.phoneNumber) : true;
  }
  get emailValid() {
    const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
    return this.email ? rule.test(this.email) : true;
  }
  applyOtpVerification(type: string): void {
    const isMobile = this.connectDevice === 'MOBILE';
    const loginInfo: LoginRequest = {
      loginType: isMobile ? 'SMS' : 'EMAIL',
      account: isMobile ? this.phoneNumber : this.email,
    }
    loginService.sendOtp(loginInfo).then(otpInfo => {
      if (otpInfo.success) {
        this.storageOtpTime(type, otpInfo);
        this.startOtpSetting(type);
        this.startOtpCount(type);
      } else {
        const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP系統錯誤';
        messageBoxService.showErrorMessage(errorMsg);
      }
    });
  };
  resentOtp(type: string) {
    this.resetOtpSetting(type);
    this.applyOtpVerification(type);
  }
  deleteOtpInfo(type: string) {
    this.resetOtpSetting(type);
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'APPLY_OTP';
      this.phoneNumber = '';
      this.otpCode = '';
    } else {
      this.onEmailVerifyResendStatus = 'APPLY_OTP';
      this.email = '';
      this.emailOtpCode = '';
    }
  //////////////////// ç™»å…¥
  login() {
    const login: LoginVerify = this.setLoginInfo();
    this.removeOtpTime();
    loginService.loginVerify(login).then(res => {
      this.storageIdToken(res.id_token);
      this.storageRole(Role.USER);
      this.phoneSuccessConfirmVisable = true;
      this.autoRedirect();
      this.storagePhoneOrEmail(this.setRegisterInfo());
    }).catch(error => {
      this.checkHttpErrorStatus(error);
    });
  }
  confirmApplySuccess(): void {
    this.phoneSuccessConfirmVisable = false;
    this.registerSuccessConfirmVisable = false;
    this.redirect();
  }
  //////////////////// è¨»å†Š
  applyAccount(): void {
    if (this.applyAccount_onAction) {
      return ;
@@ -517,49 +484,6 @@
    });
  };
  confirmApplySuccess(): void {
    this.phoneSuccessConfirmVisable = false;
    this.registerSuccessConfirmVisable = false;
    this.redirect();
  }
  login() {
    const login: LoginVerify = this.setLoginInfo();
    this.removeOtpTime();
    loginService.loginVerify(login).then(res => {
      this.storageIdToken(res.id_token);
      this.storageRole(Role.USER);
      this.phoneSuccessConfirmVisable = true;
      this.autoRedirect();
      this.storagePhoneOrEmail(this.setRegisterInfo());
    }).catch(error => {
      this.checkHttpErrorStatus(error);
    });
  }
  //////////////////////////////////////////////////////////////////
  private checkHttpErrorStatus(error:any):void{
    switch (error.response.status) {
        case 401:
          const errorMsg = OtpErrorCode[error.response?.data?.detail] ? OtpErrorCode[error.response?.data?.detail]:'OTP系統錯誤';
          messageBoxService.showErrorMessage(errorMsg);
          break;
        case 403:
          this.registerDialogVisible = true;
          setTimeout(() => {
            const isScrollBarNeedless = this.contract.scrollHeight <= this.contract.clientHeight;
            if (isScrollBarNeedless) {
              this.isReadContract = true;
            }
          }, 1000);
          break;
        default:
          messageBoxService.showErrorMessage('',error);
          break;
      }
  }
  private autoRedirect() {
    this.autoRedirectInterval = setInterval(() => {
      this.autoRedirectCounter -= 1;
@@ -578,68 +502,39 @@
    find > -1 ? this.$router.go(-1) : this.$router.push('/');
  }
  private phoneDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
    if (diffSecs < this.otpCounterSec) {
      this.otpResendCounter = diffSecs < 30 ? 30 - diffSecs : 0;
        this.otpCounterSec -= diffSecs;
        this.phoneNumber = parseOtpTime.phone;
        this.onPhoneVerifyStep = 'INPUT_OTP';
        this.phoneOtpInfo = this.setOtpInfo(parseOtpTime);
        this.startOtpCount('MOBILE');
    } else {
      localStorage.removeItem('phoneOtpTime');
  detectContractReadStatus(event: any): void {
    const scrollTop = Math.round(event.target.scrollTop);
    const height = event.target.scrollHeight - event.target.clientHeight;
    if (Math.floor(scrollTop/10) === (Math.floor(height/10))) {
      this.isReadContract = true;
    }
  }
  };
  private emailDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
  //////////////////// ç™¼é€/重發/刪除驗證碼
    if (diffSecs < this.emailCounterSec) {
      this.emailResendCounter =  diffSecs < 30 ? 30 - diffSecs : 0;
      this.emailCounterSec -= diffSecs;
      this.email = parseOtpTime.email;
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpInfo = this.setOtpInfo(parseOtpTime);
      this.startOtpCount('EMAIL');
    } else {
      localStorage.removeItem('emailOtpTime');
  applyOtpVerification(type: string): void {
    const isMobile = this.connectDevice === 'MOBILE';
    const loginInfo: LoginRequest = {
      loginType: isMobile ? 'SMS' : 'EMAIL',
      account: isMobile ? this.phoneNumber : this.email,
    }
  }
  private calcDiffSecs(parseOtpTime) {
    const currentTime = new Date().getTime();
    const storageTime = new Date(parseOtpTime).getTime();
    return Math.floor((currentTime - storageTime) / 1000);
  }
  private resetOtpSetting(type: string) {
    if (type === 'MOBILE') {
      clearInterval(this.otpInterval);
      this.otpResendCounter = 30;
      this.otpCounterSec = 300;
    } else {
      clearInterval(this.emailResendInterval);
      this.emailResendCounter = 30;
      this.emailCounterSec = 300;
    }
  }
  private setOtpInfo(parseOtpTime) {
    return {
      indexKey: parseOtpTime.indexKey,
      success: true,
      failCode: '',
      failReason: '',
    }
  }
    loginService.sendOtp(loginInfo).then(otpInfo => {
      if (otpInfo.success) {
        this.storageOtpTime(type, otpInfo);
        this.startOtpSetting(type);
        this.startOtpCount(type);
      } else {
        const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP系統錯誤';
        messageBoxService.showErrorMessage(errorMsg);
      }
    });
  };
  private storageOtpTime(type: string, otpInfo: OtpInfo) {
    type === 'MOBILE' ? this.phoneOtpInfo = otpInfo : this.emailOtpInfo = otpInfo;
    type === 'MOBILE' ? this.phoneOtpIndexKey = otpInfo.indexKey : this.emailOtpIndexKey = otpInfo.indexKey;
    const info = {...this.setRegisterInfo(), time: new Date()}
    type === 'MOBILE' ? localStorage.setItem('phoneOtpTime',JSON.stringify(info))
                      : localStorage.setItem('emailOtpTime',JSON.stringify(info));
    const storageName = type === 'MOBILE' ? OtpStorageName.PHONE : OtpStorageName.EMAIL;
    otpService.setOtpTimeToStorage(storageName, info);
  }
  private startOtpSetting(type: string) {
@@ -679,22 +574,64 @@
    }, 1000)
  }
  private setRegisterInfo(): RegisterInfo {
    return this.connectDevice === 'MOBILE'
      ? {
          phone: this.phoneNumber,
          indexKey: this.phoneOtpInfo.indexKey,
          otpCode: this.otpCode,
          name: this.name,
          contactType: 'SMS'
        }
      : {
          email: this.email,
          indexKey: this.emailOtpInfo.indexKey,
          otpCode: this.otpCode,
          name: this.name,
          contactType: 'EMAIL'
        }
  resentOtp(type: string) {
    this.resetOtpSetting(type);
    this.applyOtpVerification(type);
  }
  deleteOtpInfo(type: string) {
    this.resetOtpSetting(type);
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'APPLY_OTP';
      this.phoneNumber = '';
      this.otpCode = '';
    } else {
      this.onEmailVerifyResendStatus = 'APPLY_OTP';
      this.email = '';
      this.emailOtpCode = '';
    }
    this.removeOtpTime();
  }
  private resetOtpSetting(type: string) {
    if (type === 'MOBILE') {
      clearInterval(this.otpInterval);
      this.otpResendCounter = 30;
      this.otpCounterSec = 300;
    } else {
      clearInterval(this.emailResendInterval);
      this.emailResendCounter = 30;
      this.emailCounterSec = 300;
    }
  }
  counterTime(counterSec) {
    let min = Math.floor(counterSec / 60);
    let sec = Math.floor(counterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  //////////////////////////////////////////////////////////////////
  private checkHttpErrorStatus(error:any):void{
    switch (error.response.status) {
        case 401:
          const errorMsg = OtpErrorCode[error.response?.data?.detail] ? OtpErrorCode[error.response?.data?.detail]:'OTP系統錯誤';
          messageBoxService.showErrorMessage(errorMsg);
          break;
        case 403:
          this.registerDialogVisible = true;
          setTimeout(() => {
            const isScrollBarNeedless = this.contract.scrollHeight <= this.contract.clientHeight;
            if (isScrollBarNeedless) {
              this.isReadContract = true;
            }
          }, 1000);
          break;
        default:
          messageBoxService.showErrorMessage('',error);
          break;
      }
  }
  private storagePhoneOrEmail(registerInfo:RegisterInfo):void{
@@ -703,18 +640,61 @@
  }
  private removeOtpTime() {
    localStorage.removeItem('emailOtpTime');
    localStorage.removeItem('phoneOtpTime');
    otpService.removeOtpTimeToStorage(OtpStorageName.PHONE);
    otpService.removeOtpTimeToStorage(OtpStorageName.EMAIL);
  }
  private setLoginInfo() {
    const isMobile = this.connectDevice === 'MOBILE'
    return {
      account: isMobile ? this.phoneNumber : this.email,
      indexKey: isMobile ? this.phoneOtpInfo.indexKey : this.emailOtpInfo.indexKey,
      indexKey: isMobile ? this.phoneOtpIndexKey : this.emailOtpIndexKey,
      otpCode: isMobile ? this.otpCode : this.emailOtpCode
    }
  }
  private setRegisterInfo(): RegisterInfo {
    return this.connectDevice === 'MOBILE'
      ? {
          phone: this.phoneNumber,
          indexKey: this.phoneOtpIndexKey,
          otpCode: this.otpCode,
          name: this.name,
          contactType: 'SMS'
        }
      : {
          email: this.email,
          indexKey: this.emailOtpIndexKey,
          otpCode: this.emailOtpCode,
          name: this.name,
          contactType: 'EMAIL'
        }
  }
  get isSubmitBtnDisabled(): boolean {
    return this.connectDevice === 'MOBILE'
      ? (!this.otpCode || !this.phoneNumber || !this.phoneValid || !this.otpCounterSec)
      : (!this.emailOtpCode || !this.email || !this.emailValid || !this.emailCounterSec)
  }
  get showPhoneOtpCodeField(): boolean {
    return this.connectDevice === 'MOBILE' && this.onPhoneVerifyStep === 'INPUT_OTP';
  };
  get showEmailVerifyField(): boolean {
    return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus === 'CAN_RESEND';
  };
  get phoneValid() {
    const rule = /^09[0-9]{8}$/;
    return this.phoneNumber ? rule.test(this.phoneNumber) : true;
  }
  get emailValid() {
    const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
    return this.email ? rule.test(this.email) : true;
  }
}
</script>
PAMapp/pages/myAppointmentList.vue
@@ -1,21 +1,28 @@
<template>
    <div>
        <div class="pam-myAppointment-banner"></div>
      <div class="pam-myAppointment-banner"></div>
        <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'}"
                    @click="clickTab('appointmentList')"
                >客戶預約
                    <span class="p">({{appointmentList.length}})</span>
                >
                  <span class="smTxt">未聯絡({{ appointmentItemSum }})</span>
                </div>
                <div
                    class="cus-tab-item"
                    :class="{'is-active': activeTabName === 'contactedList'}"
                    @click="clickTab('contactedList')"
                >已聯絡
                    <span class="p">({{contactedList.length}})</span>
                >
                  <span class="smTxt">約訪中({{ contactedItemSum }})</span>
                </div>
                <div
                    class="cus-tab-item"
                    :class="{'is-active': activeTabName === 'closedList'}"
                    @click="clickTab('closedList')"
                >
                  <span class="smTxt">結案({{ closedItemSum }})</span>
                </div>
            </div>
@@ -28,7 +35,7 @@
             :isOpen.sync="showNewAppointmentHint"
        >
            <div class="text--center mdTxt">
                <p class="mb-50">你有 <span class="text--primary">{{ newAppointmentSum }}</span> å‰‡æ–°çš„預約</p>
                <p class="mb-50">您有 <span class="text--primary">{{ newAppointmentSum }}</span> å‰‡æ–°çš„預約</p>
                <div class="text--center">
                    <el-button
                        type="primary"
@@ -45,9 +52,11 @@
import * as _ from 'lodash';
import { ClientInfo } from '~/shared/models/client.model';
import { Appointment } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
const localStorage = namespace('localStorage');
const appointmentStore = namespace('appointment.store');
@Component({
    layout: 'home',
@@ -55,14 +64,23 @@
})
export default class ClientReservedList extends Vue {
    @State('myAppointmentList')
    myAppointmentList!: ClientInfo[];
    @appointmentStore.State('myAppointmentList')
    myAppointmentList!: Appointment[];
    @State('myNewAppointmentSum')
    @appointmentStore.Getter('newAppointmentSum')
    newAppointmentSum!: number;
    @Action
    storeMyAppointmentList!: () => Promise<number>;
    @appointmentStore.Getter('appointmentItemSum')
    appointmentItemSum!: number;
    @appointmentStore.Getter('contactedItemSum')
    contactedItemSum!: number;
    @appointmentStore.Getter('closedItemSum')
    closedItemSum!: number;
    @appointmentStore.Action
    getMyAppointmentList!: () => Promise<Appointment[]>;
    @localStorage.Mutation
    storageClearAppointmentIdFromMsg!: () => void;
@@ -70,16 +88,14 @@
    @localStorage.Getter
    currentAppointmentIdFromMsg!: string;
    activeTabName         : string       = 'appointmentList';
    appointmentList       : ClientInfo[] = [];
    clients               : ClientInfo[] = [];
    contactedList         : ClientInfo[] = [];
    showNewAppointmentHint: boolean      = false;
    activeTabName          : string        = 'appointmentList';
    contactStatus          = ContactStatus;
    showNewAppointmentHint: boolean        = false;
    //////////////////////////////////////////////////////////////////////
    mounted() {
      this.storeMyAppointmentList();
      this.getMyAppointmentList();
    }
    destroyed() {
@@ -90,12 +106,6 @@
    @Watch('myAppointmentList')
    onMyAppointmentListChange(): void {
        this.contactedList = this.myAppointmentList
            .filter(item => item.communicateStatus === 'contacted');
        this.appointmentList = this.myAppointmentList
            .filter(item => item.communicateStatus !== 'contacted');
        if (this.currentAppointmentIdFromMsg) {
            this.redirectAppointmentStatus();
        }
@@ -106,7 +116,13 @@
            .findIndex(item => item.id === +this.currentAppointmentIdFromMsg);
        if (currentAppointmentIndex > -1) {
            const communicateStatus = this.myAppointmentList[currentAppointmentIndex].communicateStatus;
            const pathName = communicateStatus === 'reserved' ? 'appointmentList' : 'contactedList';
            let pathName = 'closedList'
            if (communicateStatus === this.contactStatus.RESERVED || communicateStatus === this.contactStatus.PICKED) {
              pathName = 'contactedList';
            }
            if (communicateStatus === this.contactStatus.CONTACTED) {
              pathName = 'contactedList';
            }
            this.$router.push(
                {
                    path: '/myAppointmentList/' + pathName,
@@ -121,7 +137,7 @@
      this.showNewAppointmentHint = this.newAppointmentSum > 0;
    }
    @Watch('$route')
    @Watch('$route', {immediate: true})
    onRouteChange() {
        const routeFullName = this.$route.name;
        if (routeFullName) {
@@ -143,7 +159,7 @@
    // format to {page}-banner or pam-no-banner tag
    private routeFormatBannerClass(route: string): string {
        const needBannerTags = ['myAppointmentList-appointmentList', 'myAppointmentList-contactedList'];
        const needBannerTags = ['myAppointmentList-appointmentList', 'myAppointmentList-closedList'];
        return _.includes(needBannerTags, route) ? route + '-banner' : 'pam-no-banner';
    };
}
PAMapp/pages/myAppointmentList/appointmentList.vue
@@ -5,9 +5,9 @@
            placeholder="請輸入關鍵字"
            class="mb-30 pam-clientReserved-input"
            v-model="keyWord"
            @keyup.enter.native="search"
            @input="search"
        >
            <i slot="suffix" class="icon-search search cursor--pointer" @click="search"></i>
            <i slot="suffix" class="icon-search search cursor--pointer"></i>
        </el-input>
        <ClientList
@@ -24,25 +24,33 @@
</template>
<script lang="ts">
import { Vue, Component, State, Watch, namespace } from 'nuxt-property-decorator';
import { Vue, Component, Watch, namespace } from 'nuxt-property-decorator';
import { ClientInfo } from '~/shared/models/client.model';
import { Appointment } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
const localStorage = namespace('localStorage');
const localStorage     = namespace('localStorage');
const appointmentStore = namespace('appointment.store');
@Component
export default class ClientReservedList extends Vue {
    @State('myAppointmentList')
    myAppointmentList!: ClientInfo[];
    @appointmentStore.State('myAppointmentList')
    myAppointmentList!: Appointment[];
    @localStorage.Getter
    currentAppointmentIdFromMsg!: string;
    appointmentList: ClientInfo[] = [];
    filterList     : ClientInfo[] = [];
    keyWord        : string       = '';
    pageList       : ClientInfo[] = [];
    currentPage    : number = 1;
    @appointmentStore.Action
    getAppointmentDetail!: () => Promise<Appointment>;
    appointmentList: Appointment[]  = [];
    currentPage     : number        = 1;
    filterList      : Appointment[] = [];
    keyWord         : string        = '';
    pageList        : Appointment[] = [];
    contactStatus  = ContactStatus;
    //////////////////////////////////////////////////////////////////////
@@ -54,23 +62,11 @@
    @Watch('myAppointmentList')
    onMyAppointmentListChange(): void {
      const unViewList = this.myAppointmentList
          .filter((item) => item.communicateStatus !== 'contacted' && !item.consultantViewTime)
          .map((item) => ({ ...item, sortTime: new Date(item.appointmentDate)}))
      this.appointmentList = this.myAppointmentList
          .filter(item => item.communicateStatus === this.contactStatus.RESERVED)
          .map((item) => ({ ...item, sortTime: new Date(item.lastModifiedDate)}))
          .sort((preItem, nextItem) => +nextItem.sortTime - +preItem.sortTime);
      const tempViewList = this.myAppointmentList
          .filter(item => item.communicateStatus !== 'contacted' && item.consultantViewTime);
      // TODO: å¾ŒçºŒå¦‚需針對 unreadList åšæ›´ç´°ç·»çš„æŽ’序,則需請後端提供判斷依據(例如: createTime)。[Tomas, 2021/12/16];Ã¥
      const unreadList = tempViewList
                    .filter((item) => !item.consultantReadTime);
      const readList = tempViewList
                    .filter((item) => item.consultantReadTime)
                    .map((item) => ({ ...item, sortTime: new Date(item.consultantReadTime)}))
                    .sort((preItem, nextItem) => +nextItem.sortTime - +preItem.sortTime);
      this.appointmentList = [...unViewList, ...unreadList, ...readList];
      this.filterList = this.appointmentList;
      this.getCurrentPage();
@@ -86,13 +82,19 @@
    }
    //////////////////////////////////////////////////////////////////////
    search(): void {
        this.filterList = this.appointmentList.filter(item => {
            return item.name.match(this.keyWord) || item.requirement.match(this.keyWord)
        })
        if (this.keyWord) {
            this.filterList = this.appointmentList.filter(item => {
                return item.name.match(this.keyWord) || item.requirement.match(this.keyWord);
            })
        } else {
            this.filterList = this.appointmentList;
        }
    }
    changePage(pageList: ClientInfo[]): void {
    changePage(pageList: Appointment[]): void {
        this.pageList = pageList;
    }
PAMapp/pages/myAppointmentList/closedList.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,156 @@
<template>
    <div class="pam-closed-appointment-list">
        <el-input
            type="text"
            placeholder="請輸入關鍵字"
            class="mb-10 pam-clientReserved-input"
            v-model="keyWord"
            @input="search"
        >
            <i
                slot="suffix"
                class="icon-search search cursor--pointer"
            ></i>
        </el-input>
        <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>
        </div>
        <ClientList
            :clients="pageList"
            :title="'closedList'"
        ></ClientList>
        <UiPagination
            :totalList="filterList"
            :currentPage="currentPage"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Watch, namespace } from 'nuxt-property-decorator';
import { Appointment } from '~/shared/models/appointment.model';
import { ContactStatus } from '~/shared/models/enum/contact-status';
const appointmentStore = namespace('appointment.store');
const localStorage     = namespace('localStorage');
@Component
export default class ClientClosedList extends Vue {
    @appointmentStore.State('myAppointmentList')
    myAppointmentList!: Appointment[];
    @localStorage.Getter
    currentAppointmentIdFromMsg!: string;
    contactStatus= ContactStatus;
    closedItemSum = 0;
    currentPage   = 1;
    doneItemSum   = 0;
    itemSum       = 0;
    keyWord       = '';
    closedList: Appointment[] = [];
    filterList   : Appointment[] = [];
    pageList     : Appointment[] = [];
    selectedClosedCategory: 'all' | 'done' | 'closed' = 'all';
    //////////////////////////////////////////////////////////////////////
    mounted() {
        this.onMyAppointmentListChange();
    }
    //////////////////////////////////////////////////////////////////////
    @Watch('myAppointmentList')
    onMyAppointmentListChange() {
        this.closedList = (this.myAppointmentList || [])
            .filter(item => item.communicateStatus === this.contactStatus.DONE || item.communicateStatus === this.contactStatus.CLOSE || item.communicateStatus === this.contactStatus.CANCEL)
            .map((item) => ({...item, sortTime: new Date(item.lastModifiedDate)}))
            .sort((prevItem, nextItem) => +nextItem.sortTime - +prevItem.sortTime);
        this.filterList = this.closedList;
        this.itemSum = this.closedList.length;
        this.doneItemSum = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.DONE).length;
        this.closedItemSum = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.CLOSE).length;
        this.getCurrentPage();
    }
    private getCurrentPage() {
        const currentIndex = this.filterList.findIndex(item => item.id === +this.currentAppointmentIdFromMsg);
        const pageSize = 5;
        if (currentIndex > -1) {
            this.currentPage = Math.ceil((currentIndex + 1) / pageSize);
        }
    }
    @Watch('selectedClosedCategory')
    onSelectedClosedCategoryChanges() {
      this.search();
    }
    //////////////////////////////////////////////////////////////////////
    search(): void {
      if (this.selectedClosedCategory === this.contactStatus.DONE) {
        this.filterList = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.DONE);
      } else if (this.selectedClosedCategory === this.contactStatus.CLOSE) {
        this.filterList = this.closedList.filter((item) => item.communicateStatus === this.contactStatus.CLOSE);
      } else {
        this.filterList = this.closedList;
      }
      if (this.keyWord) {
        this.filterList = this.filterList.filter(item => {
            return item?.name?.match(this.keyWord) || item?.requirement?.match(this.keyWord)
        })
      }
    }
    changePage(pageList: Appointment[]): void {
        this.pageList = pageList;
    }
}
</script>
<style lang="scss">
.pam-closed-appointment-list {
  .closed-appointment__tag-filter {
    display: flex;
    .el-radio {
      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: $CORAL;
        .el-radio__label {
          color  : $PRIMARY_WHITE !important;
        }
      }
      .el-radio__input {
        display: none;
      }
      .el-radio__label {
        color  : $PRIMARY_BLACK !important;
        padding: 0px !important;
      }
    }
  }
}
</style>
PAMapp/pages/myAppointmentList/contactedList.vue
@@ -5,21 +5,30 @@
            placeholder="請輸入關鍵字"
            class="mb-30 pam-clientReserved-input"
            v-model="keyWord"
            @keyup.enter.native="search"
            @input="search"
        >
            <i
                slot="suffix"
                class="icon-search search cursor--pointer"
                @click="search"
            ></i>
        </el-input>
        <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
            v-if="togglePagination"
            :totalList="filterList"
            :currentPage="currentPage"
            @changePage="changePage"
@@ -28,25 +37,30 @@
</template>
<script lang="ts">
import { Vue, Component, Watch, State, namespace } from 'nuxt-property-decorator';
import { Vue, Component, Watch, namespace } from 'nuxt-property-decorator';
import { ClientInfo } from '~/shared/models/client.model';
import { Appointment } from '~/shared/models/appointment.model';
const localStorage = namespace('localStorage');
const appointmentStore = namespace('appointment.store');
const localStorage     = namespace('localStorage');
@Component
export default class ClientContactedList extends Vue {
    @State('myAppointmentList')
    myAppointmentList!: ClientInfo[];
    @appointmentStore.State('myAppointmentList')
    myAppointmentList!: Appointment[];
    @localStorage.Getter
    currentAppointmentIdFromMsg!: string;
    contactedList: ClientInfo[] = [];
    filterList   : ClientInfo[] = [];
    keyWord      : string       = '';
    pageList     : ClientInfo[] = [];
    currentPage  : number = 1;
    contactedList   : Appointment[]  = [];
    currentPage      : number        = 1;
    filterList       : Appointment[] = [];
    keyWord          : string        = '';
    pageList         : Appointment[] = [];
    sortType        : 'ASC' | 'DESC' = 'DESC';
    togglePagination: boolean        = true;
    //////////////////////////////////////////////////////////////////////
@@ -58,16 +72,32 @@
    @Watch('myAppointmentList')
    onMyAppointmentListChange() {
      this.setContactedList();
    }
    @Watch('sortType')
    onSortTypeChange() {
      this.togglePagination = false;
      setTimeout(() => {
      this.togglePagination = true;
        this.currentPage = 1;
        this.setContactedList();
      }, 0)
    }
    private setContactedList(): void {
        this.contactedList = (this.myAppointmentList || [])
            .filter(item => item.communicateStatus === 'contacted')
            .map((item) => ({...item, sortTime: new Date(item.contactTime)}))
            .sort((prevItem, nextItem) => +nextItem.sortTime - +prevItem.sortTime);
            .map((item) => ({...item, sortTime: new Date(item.lastModifiedDate)}))
            .sort((prevItem, nextItem) => {
              return this.sortType === 'DESC' ? +nextItem.sortTime - +prevItem.sortTime : +prevItem.sortTime - +nextItem.sortTime
            });
        this.filterList = this.contactedList;
        this.getCurrentPage();
    }
    private getCurrentPage() {
    private getCurrentPage(): void {
        const currentIndex = this.filterList.findIndex(item => item.id === +this.currentAppointmentIdFromMsg);
        const pageSize = 5;
        if (currentIndex > -1) {
@@ -78,14 +108,42 @@
    //////////////////////////////////////////////////////////////////////
    search(): void {
        this.filterList = this.contactedList.filter(item => {
            return item?.name?.match(this.keyWord) || item?.requirement?.match(this.keyWord)
        })
        if (this.keyWord) {
          this.filterList = this.contactedList.filter(item => {
              return item?.name?.match(this.keyWord) || item?.requirement?.match(this.keyWord)
          })
        } else {
          this.filterList = this.contactedList;
        }
    }
    changePage(pageList: ClientInfo[]): void {
    changePage(pageList: Appointment[]): void {
        this.pageList = pageList;
    }
    changeSortType(): void {
      if (this.sortType === 'DESC') {
        this.sortType = 'ASC';
      } else {
        this.sortType = 'DESC';
      }
    }
    get isSortType () :boolean {
      return this.sortType === 'DESC';
    }
}
</script>
<style lang="scss" scoped>
.sort-indicator-container{
  margin-bottom: 20px;
}
.sort-indicator{
  border-radius:30px;
  border: 1px solid #D0D0CE;
  background-color:#fff;
  padding: 10px 20px;
  color: #68737A;
}
</style>
PAMapp/pages/myConsultantList.vue
@@ -27,6 +27,8 @@
<script lang='ts'>
import { Vue, Component, Watch, State, Action } from 'nuxt-property-decorator';
import authService from '~/shared/services/auth.service';
import { Consultant, ConsultantWithAppointmentId } from '~/shared/models/consultant.model';
@Component
@@ -78,25 +80,27 @@
        .map((item) => ({ ...item, formatDate: new Date(item.updateTime || item.createTime)}))
        .sort((preItem, nextItem) => +nextItem.formatDate - +preItem.formatDate );
    // format contacted list
      this.myConsultantList.filter((consultant) => consultant.appointments!.length)
        .forEach((consultant) => {
          consultant.appointments!.forEach((appointment) => {
            const consultantWithAppointmentId: ConsultantWithAppointmentId = {
              ...consultant,
              appointmentId: appointment.id,
              appointmentDate: appointment.appointmentDate,
              appointmentScore: appointment.satisfactionScore,
              appointmentStatus: appointment.communicateStatus,
            };
            this.contactedList.push(consultantWithAppointmentId);
          })
        });
      if (authService.isUserLogin()) {
        this.myConsultantList.filter((consultant) => consultant.appointments!.length)
          .forEach((consultant) => {
            consultant.appointments!.forEach((appointment) => {
              const consultantWithAppointmentId: ConsultantWithAppointmentId = {
                ...consultant,
                appointmentId: appointment.id,
                appointmentDate: appointment.appointmentDate,
                appointmentScore: appointment.satisfactionScore,
                appointmentStatus: appointment.communicateStatus,
              };
              this.contactedList.push(consultantWithAppointmentId);
            })
          });
      this.contactedList = this.contactedList
        .filter((appointment) => appointment['appointmentStatus'] === 'contacted')
        .map((appointment) => ({ ...appointment, sortTime: new Date(appointment.appointmentDate)}))
        .sort((preAppointment, nextAppointment) => +nextAppointment.sortTime - +preAppointment.sortTime);
        this.contactedList = this.contactedList
          .filter((appointment) => appointment['appointmentStatus'] !== 'reserved')
          .map((appointment) => ({ ...appointment, sortTime: new Date(appointment.appointmentDate)}))
          .sort((preAppointment, nextAppointment) => +nextAppointment.sortTime - +preAppointment.sortTime);
      }
    }
    //////////////////////////////////////////////////////////////////////
PAMapp/pages/notification/index.vue
@@ -1,5 +1,125 @@
<template>
    <div>通知功能
        <el-button @click="$router.push('/notification/detail')">通知細節</el-button>
    <div>
        <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
            v-for="(item, index) in notificationList"
            :key="index"
            type="flex"
            justify="space-between"
            align="middle"
            class="notification-card"
        >
            <el-col class="unRead" :span="3" v-if="!item.readDate"></el-col>
            <el-col :span="18">
                <p class="text">{{item.content}}</p>
            </el-col>
            <el-col :span="3" class="notification-period text--right">
                <div>
                    <UiDateFormat
                        class="date"
                        :date="item.createdDate"
                        onlyShowSection="DAY" />
                </div>
                <div>
                    <UiDateFormat
                        class="time"
                        :date="item.createdDate"
                        onlyShowSection="TIME" />
                </div>
            </el-col>
        </el-row>
    </div>
</template>
</template>
<script lang="ts">
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";
import reviewsService from "~/shared/services/reviews.service";
@Component
export default class Notification extends Vue {
    @State
    unReviewLogList!: AppointmentLog[];
    @State
    notificationList!: NotificationList[];
    isUserLogin = false;
    ////////////////////////////////////////////////////////////
    mounted() {
        this.isUserLogin = authService.isUserLogin();
        reviewsService.readAllMyNotification().then(res => res);
    }
}
</script>
<style lang="scss" scoped>
    .satisfaction-banner {
        width: 100%;
        height: 60px;
        background-image: url('~/assets/images/satisfaction/satisfactionBtn_mob.svg');
        background-repeat: no-repeat;
        background-size: cover;
        background-position: center;
        border-radius: 10px;
        .satisfaction-text {
            @extend .mdTxt;
            @extend .text--PRIMARY_WHITE;
            line-height: 60px;
        }
        @include desktop {
            height: 110px;
            background-image: url('~/assets/images/satisfaction/satisfactionBtn_web.svg');
            .satisfaction-text {
                font-size: 24px;
                line-height: 110px;
            }
        }
    }
    .notification-card {
        padding: 10px;
        border-bottom: solid 1px #CCCCCC;
        .unRead {
            width: 10px;
            height: 10px;
            border-radius: 50px;
            background-color: $PRIMARY_RED;
        }
        .notification-period {
            color: #707070;
            .date {
                font-size: 10px;
                line-height: 12px;
            }
            .time {
                font-size: 12px;
                line-height: 14px;
            }
        }
    }
    .satisfaction-icon {
        font-size: 24px;
        @extend .cursor--pointer;
    }
</style>
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>
@@ -115,9 +119,23 @@
    </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>
        <!-- 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> -->
        <div class="text--center mdTxt">
          <!-- <el-button @click="closeReservePopUp">略過</el-button>
          <el-button type="primary"
            @click="closeReservePopUp">
            é€å‡º
          </el-button> -->
          <el-button type="primary"
            @click="closeReservePopUp">
            æˆ‘知道了
@@ -155,10 +173,19 @@
  const roleStorage = namespace('localStorage');
  @Component
  export default class Questionnaire extends Vue {
    @State('myConsultantList') myConsultantList!: Consultant[];
    @Action storeConsultantList!: () => Promise<number>;
    @roleStorage.Getter isUserLogin!:boolean;
    @roleStorage.State recommendConsultantItem!:string;
    @State('myConsultantList')
    myConsultantList!: Consultant[];
    @Action
    storeConsultantList!: () => Promise<number>;
    @roleStorage.Getter
    isUserLogin!:boolean;
    @roleStorage.State
    recommendConsultantItem!:string;
    score ="" ;
    genderOptions=[
      {
@@ -252,7 +279,7 @@
                  },
                  {
                      title:'分紅保單',
                      content:'分紅保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
                      content:'分紅保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓您可以同時享有壽險保障及紅利!'
                  }
    ];
@@ -279,6 +306,8 @@
    appointmentId = 0;
    appointmentTime = '';
    ////////////////////////////////////////////////////////////////////////////
    beforeRouteEnter(to: any, from: any, next: any) {
      next(vm => {
        const isUserLogin = authService.isUserLogin();
@@ -293,13 +322,10 @@
      })
    }
    async fetch() {
      if (authService.isUserLogin()) {
        await this.storeConsultantList();
      };
    }
    mounted(): void {
      if (authService.isUserLogin()) {
        this.storeConsultantList();
      };
      this.setMyRequest();
    }
@@ -328,35 +354,89 @@
      }
    }
    get phoneValid(): boolean {
      const rule = /^09[0-9]{8}$/;
      return this.myRequest.phone
            ? rule.test(this.myRequest.phone) && _.isEqual(this.myRequest.phone.length,10)
            : true;
    ////////////////////////////////////////////////////////////////////////////
    @Watch('myConsultantList')
    onMyConsultantListChange() {
      if (authService.isUserLogin() && this.myConsultantList.length > 0) {
          const editAppointment = this.getLatestReserved(this.$route.params.agentNo);
          if (editAppointment && editAppointment.agentNo) {
            this.myRequest = JSON.parse(JSON.stringify(editAppointment));
            if (!this.$route.query || this.$route.query.edit !== 'true') {
              this.isEditPopup = true;
            }
            this.isEditBtn = true;
          }
      }
    }
    get userInfo(): RegisterInfo {
      const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!);
      return initUserInfo;
    private getLatestReserved(agentNo) {
      const agentInfo = this.myConsultantList.filter(item => item.agentNo === agentNo);
      const appointmentInfo = agentInfo.length > 0 && agentInfo[0].appointments
        ? agentInfo[0].appointments!
              .filter((appointment) => appointment.communicateStatus === 'reserved')
              .map((reversedAppointment) => (
                { ...reversedAppointment,
                  sortDate: new Date(reversedAppointment.appointmentDate)
                }))
              .sort((preAppointment, nextAppointment) => +nextAppointment.sortDate - +preAppointment.sortDate)[0]
        : null;
      return this.getReservedData(appointmentInfo);
    }
    get isDisabledSubmitBtn(): boolean {
           return _.includes(this.myRequest.contactType,ContactType.PHONE)
      ? !this.isHopeContactTimeDone()
      : !this.phoneValid;
    private getReservedData(appointmentInfo) {
      if (appointmentInfo) {
        const hopeContactTime = appointmentInfo!.hopeContactTime.split("'")
              .filter(item => item && item !== ',');
        this.getAppointmentId(appointmentInfo);
        return {
            ...appointmentInfo,
            hopeContactTime: hopeContactTime.map(item => {
                const info = item.split('、');
                return {
                    selectWeekOptions: info[0].split(','),
                    selectTimesOptions: info[1].split(',')
                }
            }),
            requirement: appointmentInfo.requirement.split(',')
          }
      } else {
        return null;
      }
    }
    private isHopeContactTimeDone():boolean{
      return this.myRequest.hopeContactTime[0]?.selectWeekOptions.length >0 && this.myRequest.hopeContactTime[0]?.selectTimesOptions.length >0;
    private getAppointmentId(appointmentInfo) {
      this.appointmentId = appointmentInfo.id;
      this.appointmentTime = appointmentInfo.lastModifiedDate
                  ? appointmentInfo.lastModifiedDate
                  : appointmentInfo.appointmentDate;
    }
    ////////////////////////////////////////////////////////////////////////////
    sentDemand() {
      if (this.isEditBtn) {
        this.sentEditAppointmentDemand();
        this.editAppointmentDemand();
      } else {
        queryConsultantService.addFavoriteConsultant([this.$route.params.agentNo]).then(res => this.sentAppointmentDemand());
      }
    }
    private editAppointmentDemand() {
      const info = {
          ...this.myRequest,
          requirement: _.map(this.myRequest.requirement,o=>o).toString(),
          hopeContactTime: this.myRequest.phone && this.phoneValid ? this.getHopeContactTime() :'',
          id: this.appointmentId,
          otherRequirement: null
        }
      appointmentService.editAppointment(info).then(res => {
        this.sendReserve = true;
        this.myRequest.hopeContactTime = [];
        setRequestsToStorage(this.myRequest);
      });
    }
    private sentAppointmentDemand() {
@@ -374,22 +454,7 @@
        });
    }
    private sentEditAppointmentDemand() {
      const info = {
          ...this.myRequest,
          requirement: _.map(this.myRequest.requirement,o=>o).toString(),
          hopeContactTime: this.myRequest.phone && this.phoneValid ? this.getHopeContactTime() :'',
          id: this.appointmentId,
          otherRequirement: null
        }
        appointmentService.editAppointment(info).then(res => {
          this.sendReserve = true;
          this.myRequest.hopeContactTime = [];
          setRequestsToStorage(this.myRequest);
        });
    }
    getHopeContactTime() {
    private getHopeContactTime() {
        const selectedHopeContactTime = this.myRequest.hopeContactTime.filter((i) => i.selectWeekOptions?.length && i.selectTimesOptions?.length);
        return selectedHopeContactTime.map(i => {
            return `'${i.selectWeekOptions}、${i.selectTimesOptions}'`}
@@ -401,70 +466,35 @@
        this.$router.push('/')
    }
    private getLatestReserved(agentNo) {
      const agentInfo = this.myConsultantList.filter(item => item.agentNo === agentNo);
    ////////////////////////////////////////////////////////////////////////////
      const appointmentInfo = agentInfo.length > 0 && agentInfo[0].appointments
        ? agentInfo[0].appointments!
              .filter((appointment) => appointment.communicateStatus !== 'contacted')
              .map((reversedAppointment) => {
                return {
                  ...reversedAppointment,
                  sortDate: new Date(reversedAppointment.appointmentDate)
                }
              })
              .sort((preAppointment, nextAppointment) => +nextAppointment.sortDate - +preAppointment.sortDate)[0]
        : null;
      return this.getReservedData(appointmentInfo);
    get phoneValid(): boolean {
      const rule = /^09[0-9]{8}$/;
      return this.myRequest.phone
            ? rule.test(this.myRequest.phone) && _.isEqual(this.myRequest.phone.length,10)
            : true;
    }
    private getReservedData(appointmentInfo) {
      if (appointmentInfo) {
        const hopeContactTime = appointmentInfo!.hopeContactTime.split("'")
              .filter(item => item && item !== ',');
        this.getAppointmentId(appointmentInfo);
        return {
            age: appointmentInfo.age,
            agentNo: appointmentInfo.agentNo,
            contactType: appointmentInfo.contactType,
            email: appointmentInfo.email || '',
            gender: appointmentInfo.gender,
            hopeContactTime: hopeContactTime.map(item => {
                const info = item.split('、');
                return {
                    selectWeekOptions: info[0].split(','),
                    selectTimesOptions: info[1].split(',')
                }
            }),
            job: appointmentInfo.job,
            phone: appointmentInfo.phone || '',
            requirement: appointmentInfo.requirement.split(',')
          }
      } else {
        return null;
      }
    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;
    }
    private getAppointmentId(appointmentInfo) {
      this.appointmentId = appointmentInfo.id;
      this.appointmentTime = appointmentInfo.lastModifiedDate
                  ? appointmentInfo.lastModifiedDate
                  : appointmentInfo.appointmentDate;
    get userInfo(): RegisterInfo {
      const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!);
      return initUserInfo;
    }
    @Watch('myConsultantList') onMyConsultantListChange() {
      if (authService.isUserLogin() && this.myConsultantList.length > 0) {
          const editAppointment = this.getLatestReserved(this.$route.params.agentNo);
          if (editAppointment && editAppointment.agentNo) {
            this.myRequest = JSON.parse(JSON.stringify(editAppointment));
            if (!this.$route.query || this.$route.query.edit !== 'true') {
              this.isEditPopup = true;
            }
            this.isEditBtn = true;
            return;
          }
      }
    get isDisabledSubmitBtn(): boolean {
           return _.includes(this.myRequest.contactType,ContactType.PHONE)
      ? !this.isHopeContactTimeDone() || !this.emailValid
      : !this.phoneValid;
    }
    private isHopeContactTimeDone():boolean{
      return this.myRequest.hopeContactTime[0]?.selectWeekOptions.length >0 && this.myRequest.hopeContactTime[0]?.selectTimesOptions.length >0;
    }
  }
</script>
@@ -473,7 +503,6 @@
  display: flex;
  justify-content: center;
  margin-top: 10px;
  margin-bottom: 26px;
}
//drawer最底下文字樣式
@@ -598,6 +627,16 @@
    margin: 0px 20px;
  }
.pam-app-review{
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.pam-satisfaction-rate{
  margin-bottom: 45px;
}
  @include desktop{
  .ques-header{
@@ -625,5 +664,6 @@
  }
}
</style>
PAMapp/pages/recommendConsultant/index.vue
@@ -218,7 +218,7 @@
      },
      {
        title: '分紅保單',
        content: '分紅保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
        content: '分紅保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓您可以同時享有壽險保障及紅利!'
      }
    ];
    showDialog = false;
@@ -338,9 +338,6 @@
    }
    .down-icon {
      color: #ED1B2E;
      font-size: 25px;
      align-self: center;
      margin-right: 15px;
    }
PAMapp/pages/recommendConsultant/result.vue
@@ -6,7 +6,9 @@
            <div class="pam-rec-agent-card__content">
                <div class="pam-rec-agent-card__content-header">
                    <div class="pam-rec-agent-card__avatar">
                        <UiAvatar :fileName="info.img" ></UiAvatar>
                        <UiAvatar
                          :agentNo="info.agentNo" >
                        </UiAvatar>
                    </div>
                    <div class="pam-rec-agent-card__main-info">
                        <div class="text--middle  pt-10 rec-desktop-name">{{ info.name }}</div>
PAMapp/pages/record/index.vue
@@ -1,7 +1,7 @@
<template>
<div>
    <ReviewRecords
      :myAppointmentReviewLogList="myAppointmentReviewLogList"
      :reviewLogList="reviewLogList"
    ></ReviewRecords>
</div>
</template>
@@ -13,17 +13,8 @@
@Component
export default  class Reviews extends Vue{
    @State('myAppointmentReviewLogList')
    myAppointmentReviewLogList!: AppointmentLog[];
    @Action
    storeMyAppointmentReviewLog!: any;
    //////////////////////////////////////////////////////////////////////
    mounted() {
        this.storeMyAppointmentReviewLog();
    }
    @State('reviewLogList')
    reviewLogList!: AppointmentLog[];
}
</script>
PAMapp/pages/satisfactionList.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,161 @@
<template>
  <div>
    <div class="pam-satisfaction-banner"></div>
    <div class="pam-container">
      <div class="satisfaction-title">
        <span class="mdTxt">滿意度調查</span>
        <span class="ml-10 text--prudential_grey smTxt_bold">共 {{mapUnReviewLogList.length}} ç­†</span>
      </div>
      <template v-if="mapUnReviewLogList.length">
        <div class="satisfaction-card" v-for="(item, index) in mapUnReviewLogList" :key="index">
          <div class="satisfaction-card-content">
            <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.satisfaction"
            class="pam-satisfaction-rate mt-10 fix-chrome-click--issue"
            @change="isBtnDisabled = false"
          ></el-rate>
        </div>
        <div class="text--center mt-30" v-if="mapUnReviewLogList.length">
          <el-button type="primary" :disabled="isBtnDisabled" @click="sent">送出</el-button>
        </div>
      </template>
      <template v-else>
         <div class="satisfaction-card">
          <div class="satisfaction-card-content">
            ç›®å‰æš«ç„¡éœ€è¦æ‚¨å¡«å¯«çš„æ»¿æ„åº¦èª¿æŸ¥
          </div>
         </div>
      </template>
    </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'
})
export default class MySatisfactionList extends Vue {
  @State
  unReviewLogList!: AppointmentLog[];
  @Action
  storeMyAppointmentReviewLog!: () => void;
  mapUnReviewLogList: AppointmentReviewLog[] = [];
  showConfirmPopup = false;
  isBtnDisabled = true;
  ///////////////////////////////////////////////////////
  @Watch('unReviewLogList')
  onUnReviewLogListChange() {
      this.mapUnReviewLogList = this.unReviewLogList.map(item => {
        return {
          ...item,
          satisfaction: 0
        }
      })
  }
  ///////////////////////////////////////////////////////
  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 {
  satisfaction: number;
}
</script>
<style lang="scss" scoped>
.pam-satisfaction-banner {
    width              : 100%;
    height             : 120px;
    background-size    : cover;
    background-repeat  : no-repeat;
    background-position: center;
    position           : relative;
    background-image   : url('~/assets/images/satisfaction/banner_mob.svg');
}
@media (min-width: 768px) {
    .pam-satisfaction-banner {
        height          : 150px;
        background-image: url('~/assets/images/satisfaction/banner_web.svg');
    }
}
.pam-container {
    margin: 30px 20px;
}
@include desktop {
    .pam-container {
        width : 700px;
        margin: 30px auto;
    }
}
.satisfaction-card {
  margin-top       : 20px;
  .satisfaction-card-content {
    display        : flex;
    flex-direction : row;
    justify-content: space-between;
    .satisfaction-card-text {
      width        : 75%;
      line-height  : 28px;
      align-self: center;
      font-size: 20px;
      padding-left: 10px;
    }
    @include desktop {
        justify-content: flex-start;
        .satisfaction-card-text {
          width        : auto;
          padding-left: 30px;
        }
      }
  }
}
</style>
PAMapp/pages/userReviewsRecord/index.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <ReviewRecords
      :myAppointmentReviewLogList="myAppointmentReviewLogList"
      :reviewLogList="reviewLogList"
    ></ReviewRecords>
  </div>
</template>
@@ -11,16 +11,9 @@
@Component
export default  class UserReviewsRecord extends Vue{
    @State('myAppointmentReviewLogList')
    myAppointmentReviewLogList!: AppointmentLog[];
    @Action
    storeMyAppointmentReviewLog!: any;
    //////////////////////////////////////////////////////////////////////
    mounted() {
      this.storeMyAppointmentReviewLog();
    }
    @State('reviewLogList')
    reviewLogList!: AppointmentLog[];
}
</script>
PAMapp/plugins/filters/appointment-fail-reason.filter.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,16 @@
import Vue from 'vue'
import { appointmentFailReasonList } from '~/shared/const/appointment-fail-reason-list';
Vue.filter('toFailReasonLabel', (value: string): string => {
  if (!value ||  typeof value !== 'string') {
    return '--';
  };
  let failReasonLabel = {};
  appointmentFailReasonList.forEach((failReason) => {
    failReasonLabel[failReason.value] = failReason.key;
  });
  return  failReasonLabel[value];
})
PAMapp/plugins/filters/date.filter.ts
@@ -29,9 +29,9 @@
  if (isThisYear(date)) {
    return isToday(date)
          ? `今天 ${date.getHours()}:${minutes}`
          : `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${minutes}`;
          : `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${minutes}`;
  } else {
    return `${date.getFullYear()}å¹´${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${minutes}`;
    return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${minutes}`;
  };
})
PAMapp/shared/const/agent-communication-style-list.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1 @@
export const agentCommunicationStyleList = ['謹慎務實', '明快主動', '耐心傾聽', '健談風趣'];
PAMapp/shared/const/agent-expert-list.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1 @@
export const agentExpertList = ['健康與保障', '子女教育', '資產規劃', '樂活退休', '保單健檢/規劃', '分紅保單'];
PAMapp/shared/const/appointment-fail-reason-list.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,26 @@
export const appointmentFailReasonList = [
  {
    key: '無法聯繫客戶',
    value: 'cannot_to_contact_customer'
  },
  {
    key: '單純諮詢',
    value: 'only_consultation'
  },
  {
    key: '無合適商品',
    value: 'no_suitable_commodity'
  },
  {
    key: '核保問題- é«”況、財務、職業',
    value: 'prohibited_factors'
  },
  {
    key: '經濟因素',
    value: 'economy'
  },
  {
    key: '其他',
    value: 'other'
  },
];
PAMapp/shared/models/account.model.ts
@@ -1,5 +1,24 @@
export interface UserSetting {
    name: string;
    name  : string;
    phone?: string;
    email?: string;
}
}
export interface AgentInfoSetting{
    agentNo           : string;
    name              : string;
    expertise         : string[];
    title             : string;
    role              : string;
    serveArea         : string;
    gender            : string;
    phoneNumber       : string;
    companyAddress    : string;
    seniorityYear     : number;
    seniorityMonth    : number;
    concept           : string;
    experiences        : string;
    awards           : string;
    communicationStyle: string;
    photoBase64       : string;
}
PAMapp/shared/models/agent-info.model.ts
@@ -1,18 +1,22 @@
export interface AgentInfo {
  name            : string;
  agentNo         : string;
  role            : string;
  img             : string;
  avgScore        : number;
  title           : string;
  phoneNumber     : string;
  serveArea       : string;
  companyAddress  : string;
  seniority       : string;
  suitability     : number;
  evaluation      : number;
  expertise       : string[];
  concept         : string;
  experiences     : string[];
  awards          : string;
  agentNo           : string;
  avgScore          : number;
  awards            : string;
  communicationStyle: string;
  companyAddress    : string;
  concept           : string;
  email?            : string;
  evaluation        : number;
  experiences       : string;
  expertise         : string[];
  gender            : string,
  img               : string;
  latestLoginTime   : Date  ;
  name              : string;
  phoneNumber       : string;
  role              : string;
  seniority         : string;
  serveArea         : string;
  suitability       : number;
  title             : string;
}
PAMapp/shared/models/appointment.model.ts
@@ -1,95 +1,184 @@
import { ContactStatus } from "./enum/contact-status";
export interface AppointmentLog {
    id              : number,
    createdDate     : Date,
    lastModifiedDate: Date,
    customerId      : number,
    agentNo         : string,
    status          : 'UNFILLED' | 'FILLED',
    score           : number,
    agentName       : string,
    customerName    : string,
}
export interface Appointment {
  id                 : number;
  phone?             : string;
  email?             : string;
  contactType        : string;
  gender             : string;
  age                : string;
  job                : string;
  requirement        : string;
  communicateStatus  : string;
  hopeContactTime    : string;
  otherRequirement?  : string;
  appointmentDate    : string;
  lastModifiedDate   : string;
  agentNo            : string;
  customerId         : number;
  name               : string;
  consultantViewTime?: string;
  consultantReadTime?: string;
  satisfactionScore? : number;
};
export interface AppointmentWithConsultantInfo extends Appointment {
  consultantName      : string;
  consultantAvatar    : string;
  consultantExpertList: string[]
  updateTime          : Date | string;
  contactStatus       : string;
    agentName       : string;
    agentNo         : string;
    appointmentId   : number;
    createdDate     : string;
    customerId      : number;
    customerName    : string;
    id              : number;
    lastModifiedDate: string;
    score           : number;
    status          : 'UNFILLED' | 'FILLED';
}
export interface AppointmentDetail {
  id               : number;
  phone            : string;
  email            : string;
  contactType      : string;
  gender           : string;
  age              : string;
  job              : string;
  requirement      : string;
  communicateStatus: string;
  hopeContactTime  : string;
  otherRequirement : string;
  appointmentDate  : string;
  agentNo          : string;
  customerId       : number;
  name             : string;
export interface Appointment {
  age                  : string;
  agentNo              : string;
  appointmentClosedInfo: AppointmentClosedInfo;
  appointmentDate      : string;
  appointmentMemoList  : AppointmentMemoInfo[]
  appointmentNoticeLogs: NoticeLogs[];
  communicateStatus    : ContactStatus;
  consultantReadTime   : string;
  consultantViewTime   : string;
  contactTime          : string;
  contactType          : string;
  customerId           : number;
  email                : string;
  gender               : string;
  hopeContactTime      : string;
  id                   : number;
  interviewRecordDTOs  : InterviewRecord[];
  job                  : string;
  lastModifiedDate     : string;
  name                 : string;
  otherRequirement     : string;
  phone                : string;
  requirement          : string;
  satisfactionScore    : number;
};
export interface AppointmentClosedInfo {
  appointmentId         : number;
  closedOtherReason     : string;
  closedReason          : string;
  id                    : number;
  planCode              : string;
  policyEntryDate       : string;
  policyholderIdentityId: string;
  remark                : string;
}
export interface AppointmentMemoInfo {
  appointmentId: number;
  content      : string;
  id           : number;
}
export interface InterviewRecord {
  appointmentId   : number;
  content         : string;
  createdBy       : string;
  createdDate     : string;
  id              : number;
  interviewDate   : string;
  lastModifiedBy  : string;
  lastModifiedDate: string;
}
export interface NoticeLogs {
  appointmentId: number,
  content      : string,
  createdDate  : string
  email        : string,
  id           : number,
  phone        : string,
}
export interface AppointmentWithConsultantInfo extends Appointment {
  consultantAvatar    : string;
  consultantExpertList: string[];
  consultantName      : string;
  contactStatus       : string;
  updateTime          : string;
}
export interface AppointmentParams {
  phone          : string;
  email          : string;
  contactType    : string;
  gender         : string;
  age            : string;
  job            : string;
  requirement    : string;
  hopeContactTime: string;
  agentNo        : string;
  contactType    : string;
  email          : string;
  gender         : string;
  hopeContactTime: string;
  job            : string;
  phone          : string;
  requirement    : string;
}
export interface EditAppointmentParams {
  id              : number,
  phone           : string,
  email           : string,
  contactType     : string,
  gender          : string,
  age             : string,
  job             : string,
  requirement     : string,
  hopeContactTime : string,
  otherRequirement: null
  age             : string;
  contactType     : string;
  email           : string;
  gender          : string;
  hopeContactTime : string;
  id              : number;
  job             : string;
  otherRequirement: null;
  phone           : string;
  requirement     : string;
}
export interface AppointmentRequests {
  phone          : string,
  email          : string,
  contactType    : string,
  gender         : string,
  age            : string,
  job            : string,
  requirement    : string[],
  hopeContactTime: ContactTime[],
  agentNo        : string,
  age            : string;
  agentNo        : string;
  contactType    : string;
  email          : string;
  gender         : string;
  hopeContactTime: ContactTime[];
  job            : string;
  phone          : string;
  requirement    : string[];
}
export interface ContactTime {
selectWeekOptions : string[],
selectTimesOptions: string[]
  selectTimesOptions: string[];
  selectWeekOptions : string[];
}
export interface createdMemoInfo {
  appointmentId: number;
  content      : string;
}
export interface updatedMemoInfo {
  content: string;
  id     : number;
}
export interface ToDoneAppointment {
    appointmentId         : number;
    contactStatus         : ContactStatus;
    planCode              : string;
    policyEntryDate       : string;
    policyholderIdentityId: string;
    remark?               : string;
}
export interface ToCloseAppointment {
  appointmentId    : number;
  closedOtherReason: string;
  closedReason     : string;
  contactStatus    : ContactStatus;
  remark?          : string;
}
export interface NoticeLogs {
  appointmentId: number;
  content      : string;
  createdDate  : string;
  email        : string;
  id           : number;
  interviewDate: string;
  phone        : string;
}
export interface ToInformAppointment {
  appointmentId: number;
  email        : string;
  interviewDate: string;
  message      : string;
  phone        : string;
}
export interface InterviewRecordInfo {
  content: string;
  interviewDate: string;
  appointmentId: number;
}
export interface UpdateInterviewRecordInfo {
  /** interviewRecord id */
  id: number;
}
PAMapp/shared/models/client.model.ts
Àɮפw§R°£
PAMapp/shared/models/enum/contact-status.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,8 @@
export enum ContactStatus {
  PICKED    = 'picked',
  RESERVED  = 'reserved',
  CONTACTED = 'contacted',
  DONE      = 'done',
  CLOSE     = 'closed',
  CANCEL    = 'cancel',
}
PAMapp/shared/models/reviews.model.ts
@@ -2,3 +2,20 @@
  appointmentId: number,
  score        : number,
}
export interface NotificationList {
  id: number;
  /** æ¨™é¡Œ */
  title: string;
  /** é€šçŸ¥å…§å®¹ */
  content: string;
  /** ACTIVITY(個人活動通知)、SYSTEM(系統通知) */
  notificationType: string;
  /** CUSTOMER(客戶)、CONSULTANT(顧問) */
  ownerRole: string;
  /** ç™»å…¥è€…id */
  ownerId: number
  createdDate: string;
  /** å·²è®€æ™‚é–“ */
  readDate: string;
}
PAMapp/shared/models/strict-query.model.ts
@@ -1,14 +1,14 @@
export interface StrictQueryParams {
  gender          : string;
  avgScore        : number;
  status          : string;    //phase 1 disable
  area            : string;
  requirements    : string[];
  otherRequirement: string;
  seniority       : string;
  popularTags     : string[];
  otherPopularTags: string;
  gender?          : string;
  avgScore?        : number;
  status?          : string;    //phase 1 disable
  area?            : string;
  requirements?    : string[];
  otherRequirement?: string;
  seniority?       : string;
  popularTags?     : string[];
  otherPopularTags?: string;
}
export interface AgentOfStrictQuery {
PAMapp/shared/services/account-setting.service.ts
@@ -1,6 +1,6 @@
import { http } from "./httpClient";
import { UserSetting } from "~/shared/models/account.model";
import { AgentInfoSetting, UserSetting } from "~/shared/models/account.model";
class AccountSettingService{
@@ -13,5 +13,10 @@
    return http.put('/customer/info', params ).then(res => res.data);
  }
  //編輯顧問帳號資訊
  async editAgentInfoSetting(params:any):  Promise<AgentInfoSetting>{
    return http.post('/consultant/edit',params).then(res => res.data);
  }
}
export default new AccountSettingService();
PAMapp/shared/services/appointment.service.ts
@@ -1,14 +1,13 @@
import { http } from "./httpClient";
import { ClientInfo } from "~/shared/models/client.model";
import { AppointmentDetail, EditAppointmentParams } from "~/shared/models/appointment.model";
import { Appointment, AppointmentMemoInfo, createdMemoInfo, EditAppointmentParams,  InterviewRecordInfo, ToCloseAppointment, ToDoneAppointment, ToInformAppointment, updatedMemoInfo, UpdateInterviewRecordInfo } from "~/shared/models/appointment.model";
class AppointmentService {
  // å–得所有預約清單
  async getMyAppointmentList(): Promise<ClientInfo[]> {
  // é¡§å•å–得所有自己的預約單API
  async getMyAppointmentList(): Promise<Appointment[]> {
    return http.get('/consultant/getMyAppointment').then((res) => {
      const hasNewAppointment = res.data.find((appointment: ClientInfo) => !appointment.consultantViewTime);
      const hasNewAppointment = res.data.find((appointment: Appointment) => !appointment.consultantViewTime);
      if (hasNewAppointment) {
        this.viewAllAppointment();
      }
@@ -16,7 +15,7 @@
    });
  }
  // é¡§å•ç™»å…¥é¡¯ç¤ºæ–°é ç´„單筆數後觸發
  // é¡§å•ç€è¦½è‡ªå·±æ‰€æœ‰çš„預約單紀錄觸發API
  private viewAllAppointment(): void {
    http.post('/consultant/record/allAppointmentsView').then();
  }
@@ -27,7 +26,7 @@
  }
  // å–得預約單細節
  async getAppointmentDetail(appointmentId: number):Promise<AppointmentDetail> {
  async getAppointmentDetail(appointmentId: number):Promise<Appointment> {
    return http.get(`/appointment/getDetail/${appointmentId}`).then((res) => res.data);
  }
@@ -41,6 +40,45 @@
    return http.put('/appointment', editAppointmentParams);
  }
  // æ–°å¢žè¨»è¨˜
  async createMemo(memoInfo: createdMemoInfo): Promise<AppointmentMemoInfo> {
    return http.post('/appointment/memo/create', memoInfo).then(res => res.data);
  }
  // ç·¨è¼¯è¨»è¨˜
  async updateMemo(memoInfo: updatedMemoInfo): Promise<AppointmentMemoInfo> {
    return http.post('/appointment/memo/update', memoInfo).then(res => res.data);
  }
  // åˆªé™¤è¨»è¨˜
  deleteMemo(appointmentMemoId: number) {
    return http.delete(`/appointment/memo/${appointmentMemoId}`)
  }
  // é ç´„單結案, æ›´æ–°çµæ¡ˆæ˜Žç´°
  async closeAppointment(appointmentInfo: ToDoneAppointment | ToCloseAppointment) {
    return http.post(`/appointment/close`, appointmentInfo).then((res) => res.data);
  }
  // ç´„訪通知 API
  async informAppointment(appointmentInformation: ToInformAppointment) {
    return http.post(`/notice/send`, appointmentInformation).then((res) => res.data);
  }
  // æ–°å¢žç´„訪記錄
  async createInterviewRecord(interviewRecordInfo: InterviewRecordInfo) {
    return http.post('/interview_record/create', interviewRecordInfo).then(res => res.data);
  }
  // ä¿®æ”¹ç´„訪記錄
  async updateInterviewRecord(updateInterviewRecordInfo: UpdateInterviewRecordInfo) {
    return http.post('/interview_record/update', updateInterviewRecordInfo)
  }
  // åˆªé™¤ç´„訪記錄
  async deleteInterviewRecord(interviewRecordId) {
    return http.delete(`/interview_record/${interviewRecordId}`);
  }
}
export default new AppointmentService();
PAMapp/shared/services/httpClient.ts
@@ -16,8 +16,11 @@
  withCredentials: true
});
let apiNumber = 0;
http.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    apiNumber += 1;
    loadingStart();
    addHttpHeader(config);
    return config;
@@ -26,11 +29,17 @@
http.interceptors.response.use(
  (response: AxiosResponse) => {
    loadingFinish();
    apiNumber -= 1;
    if (apiNumber === 0) {
      loadingFinish();
    }
    return response;
  },
  (error: AxiosError) => {
    loadingFinish();
    apiNumber -= 1;
    if (apiNumber === 0) {
      loadingFinish();
    }
    showErrorMessageBox(error)
    return Promise.reject(error);
  }
PAMapp/shared/services/my-consultant.service.ts
@@ -1,8 +1,9 @@
import { http } from "./httpClient";
import _ from "lodash";
import { http } from "./httpClient";
import { AgentInfo } from '~/shared/models/agent-info.model';
import { Consultant } from "../models/consultant.model";
import { Appointment } from "../models/appointment.model";
class MyConsultantService {
  async getFavoriteConsultantList(): Promise<Consultant[]> {
@@ -30,13 +31,22 @@
  }
  // æ¨™è¨˜ç‚ºå·²è¯çµ¡
  markAsContact(appointmentId: number): Promise<void> {
    // TODO: è·Ÿå¾Œç«¯ç¢ºèªï¼Œé€™è£¡çš„ API ä¸æ‡‰è©²å‚³å›ž void, è€Œæ˜¯æ‡‰è©²æ˜¯æ›´æ–°å¾Œçš„資料 - Ben 2021/11/16
    // return http.post('/appointment/markAsContacted/'+appointmentId, undefined, {headers})
    //         .then(res => res.data)
    return http.post(`/appointment/markAsContacted/${appointmentId}`);
  markAsContact(appointmentId: number): Promise<Appointment> {
    return http.post(`/appointment/markAsContacted/${appointmentId}`).then(res => res.data);
  }
  // å–得顧問頭像
  getConsultantAvatar(agentNo:string):Promise<string>{
    return http.get(`/consultant/avatar/${agentNo}`,{ responseType : 'arraybuffer' })
      .then( response => {
        const toBase64 = window.btoa(
                          _.reduce( new Uint8Array(response.data),(data,byte) =>
                            data + String.fromCharCode(byte),'')
                        );
        const imgSrc = `data:image/png;base64,${toBase64}`;
        return imgSrc;
    });
  }
}
export default new MyConsultantService();
PAMapp/shared/services/otp.service.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,57 @@
import { RegisterInfo } from "../models/registerInfo";
class OtpService {
    setOtpTimeToStorage(name: string, info) {
        localStorage.setItem(name,JSON.stringify(info));
    }
    getOtpTime(name: string) {
        return localStorage.getItem(name);
    }
    removeOtpTimeToStorage(name: string) {
        localStorage.removeItem(name);
    }
    parseOtpTime(name): OtpTime | null {
        const otpTime = this.getOtpTime(name);
        return otpTime ? JSON.parse(otpTime) : null;
    }
    diffOtpTime(storageName: string, otpCounterSec: number) {
        const parseOtpTime = this.parseOtpTime(storageName);
        if (parseOtpTime) {
            const diffSecs = this.calcDiffSecs(parseOtpTime.time);
            if (diffSecs < otpCounterSec) {
                return diffSecs;
            } else {
                this.removeOtpTimeToStorage(storageName);
                return false;
            }
        }
        return false;
    }
    private calcDiffSecs(parseOtpTime) {
        const currentTime = new Date().getTime();
        const storageTime = new Date(parseOtpTime).getTime();
        return Math.floor((currentTime - storageTime) / 1000);
    }
}
export default new OtpService();
export interface OtpTime extends RegisterInfo {
    time: string;
}
export enum OtpStorageName {
    EMAIL = 'emailOtpTime',
    PHONE = 'phoneOtpTime'
}
PAMapp/shared/services/reviews.service.ts
@@ -1,18 +1,40 @@
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 {
  //客戶進行滿意度評分
  //客戶進行滿意度評分(單筆)
  userReviewsConsultants(data: UserReviewsConsultantsParams) {
    return http.post('/satisfaction/create', data );
    return http.post('/satisfaction/score', data);
  }
  // å®¢æˆ¶é€²è¡Œæ»¿æ„åº¦(多筆)
  allUserReviewsConsultants(data: UserReviewsConsultantsParams[]) {
    return http.post('/satisfaction/score/all', data);
  }
  //取得所有評分紀錄
  async getMyReviewLog(): Promise<AppointmentLog[]> {
    return http.get('/satisfaction/getMySatisfaction').then(res => res.data);
  }
  // é¡§å•ä¸»å‹•發送滿意度通知
  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);
  }
  // ç›®å‰ç™»å…¥è€…的所有小鈴鐺通知設定為已讀
  readAllMyNotification() {
    return http.post('/personal_notification/readAllMyNotification')
  }
}
export default new ReviewsService();
PAMapp/store/appointment.store.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,84 @@
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import appointmentService from '~/shared/services/appointment.service';
import { ContactStatus } from '~/shared/models/enum/contact-status';
import { Appointment } from '~/shared/models/appointment.model';
@Module
export default class AppointmentStore extends VuexModule {
  contactStatus = ContactStatus;
  appointmentDetail: Appointment | null = JSON.parse(localStorage.getItem('appointment')!);
  myAppointmentList: Appointment[]      = [];
  //////////////////////////////////////////////////////////////////////
  get appointmentProgress(): ContactStatus {
    return this.appointmentDetail!.communicateStatus;
  }
  get newAppointmentSum(): number {
    return this.myAppointmentList.filter(item => !item.consultantViewTime || item.consultantViewTime === null).length;
  }
  get appointmentItemSum(): number {
    return this.myAppointmentList.filter(item => item.communicateStatus === this.contactStatus.RESERVED).length;
  }
  get contactedItemSum(): number {
    return this.myAppointmentList.filter((item) => item.communicateStatus === this.contactStatus.CONTACTED).length;
  }
  get closedItemSum(): number {
    return this.myAppointmentList
    .filter(item => item.communicateStatus === this.contactStatus.DONE || item.communicateStatus === this.contactStatus.CLOSE ).length;
  }
  get isCloseAppointment(): boolean {
    const closedStatusList = [this.contactStatus.DONE, this.contactStatus.CLOSE, this.contactStatus.CANCEL];
    return closedStatusList.includes(this.appointmentDetail!.communicateStatus);
  }
  //////////////////////////////////////////////////////////////////////
  @Mutation
  SET_MY_APPOINTMENT_LIST(appointmentList: Appointment[]): void {
    this.myAppointmentList = appointmentList;
  }
  @Mutation
  SET_APPOINTMENT(appointmentDetail: Appointment): void {
    this.appointmentDetail = appointmentDetail;
    localStorage.setItem('appointment', JSON.stringify(appointmentDetail));
  }
  //////////////////////////////////////////////////////////////////////
  @Action({ commit: 'SET_MY_APPOINTMENT_LIST' })
  async getMyAppointmentList(): Promise<Appointment[]> {
    return await appointmentService.getMyAppointmentList().then((res) => res);
  }
  @Action({ commit: 'SET_MY_APPOINTMENT_LIST' })
  updateMyAppointmentList(appointment: Appointment): Appointment[] {
    const tempList = this.myAppointmentList.filter((item) => item.id !== appointment.id);
    tempList.unshift(appointment);
    return tempList;
  }
  @Action({ commit: 'SET_APPOINTMENT'})
  async getAppointmentDetail(appointmentId: number): Promise<Appointment> {
    if (this.appointmentDetail && this.appointmentDetail.id === appointmentId) {
      return this.appointmentDetail;
    } else {
      return await appointmentService.getAppointmentDetail(appointmentId).then((res) => res);
    }
  }
  @Action({ commit: 'SET_APPOINTMENT'})
  async updateAppointmentDetail(appointmentId: number): Promise<Appointment> {
    return await appointmentService.getAppointmentDetail(appointmentId).then((res) => res);
  }
}
PAMapp/store/index.ts
@@ -1,27 +1,23 @@
import { StrictQueryParams } from '~/shared/models/strict-query.model';
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { getFavoriteFromStorage, setFavoriteToStorage } from '~/shared/storageConsultant';
import myConsultantService from '~/shared/services/my-consultant.service';
import queryConsultantService from '~/shared/services/query-consultant.service';
import appointmentService from '~/shared/services/appointment.service';
import reviewsService from '~/shared/services/reviews.service';
import { Consultant } from '~/shared/models/consultant.model';
import { getFavoriteFromStorage, setFavoriteToStorage } from '~/shared/storageConsultant';
import { AppointmentLog } from '~/shared/models/appointment.model';
import { ClientInfo } from '~/shared/models/client.model';
import { AgentOfStrictQuery } from '~/shared/models/strict-query.model';
import { AgentOfStrictQuery, StrictQueryParams } from '~/shared/models/strict-query.model';
import { NotificationList } from '~/shared/models/reviews.model';
@Module
export default class Store extends VuexModule {
    recommendList: Consultant[] = [];
    strictQueryList: AgentOfStrictQuery[] = [];
    myConsultantList: Consultant[] = [];
    myAppointmentList: ClientInfo[] = [];
    myNewAppointmentSum: number = 0;
    myAppointmentReviewLogList: AppointmentLog[] = [];
    reviewLogList: AppointmentLog[] = [];
    unReviewLogList: AppointmentLog[] = [];
    notificationList: NotificationList[] = [];
    get isUserLogin() {
        return this.context.getters['localStorage/isUserLogin'];
@@ -29,7 +25,7 @@
    @Mutation
    updateRecommend(data: Consultant[]) {
        this.recommendList = data;
      this.recommendList = data;
    }
    @Mutation
@@ -43,18 +39,18 @@
    }
    @Mutation
    updateMyAppointmentList(data: ClientInfo[]) {
        this.myAppointmentList = data;
    updateReviewLog(data: AppointmentLog[]) {
        this.reviewLogList = data;
    }
    @Mutation
    updateMyNewAppointmentSum(newAppointmentSum: number) {
      this.myNewAppointmentSum = newAppointmentSum;
    updateUnReviewLog(data: AppointmentLog[]) {
        this.unReviewLogList = data;
    }
    @Mutation
    updateMyAppointmentReviewLog(data: AppointmentLog[]) {
        this.myAppointmentReviewLogList = data;
    updateNotification(data: NotificationList[]) {
        this.notificationList = data;
    }
    @Action
@@ -126,15 +122,6 @@
    }
    @Action
    storeMyAppointmentList(): void {
      appointmentService.getMyAppointmentList().then((data) => {
            const newAppointmentSum = data.filter(item => !item.consultantViewTime || item.consultantViewTime === null).length;
            this.context.commit('updateMyAppointmentList', data);
            this.context.commit('updateMyNewAppointmentSum', newAppointmentSum);
        });
    }
    @Action
    storeMyAppointmentReviewLog() {
        reviewsService.getMyReviewLog().then((data) => {
            const dataWithLatestDate = data.map((item) => {
@@ -144,15 +131,11 @@
                }
            });
            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
    updateMyAppointment(myAppointment: ClientInfo) {
        const data = this.myAppointmentList.filter(item => item.id !== myAppointment.id);
        data.unshift(myAppointment);
        this.context.commit('updateMyAppointmentList', data)
    }
    @Action
@@ -164,4 +147,13 @@
        });
    }
    @Action
    storeMyPersonalNotification() {
        reviewsService.getMyPersonalNotification().then(data => {
            const sortData = data
                .sort((preItem, nextItem) => +new Date(nextItem.createdDate) - +new Date(preItem.createdDate))
            this.context.commit('updateNotification', sortData);
        })
    }
}
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');
@@ -9,6 +10,8 @@
  quickFilterSelectedItem = localStorage.getItem('quickFilter');
  recommendConsultantItem = localStorage.getItem('recommendConsultantItem');
  appointmentIdFromMsg = localStorage.getItem('appointmentIdFromMsg');
  satisfactionIdFromMsg = localStorage.getItem('satisfactionIdFromMsg');
  notContactAppointmentIdFromMsg = localStorage.getItem('notContactAppointmentIdFromMsg');
  get idToken(): string|null {
    return this.id_token;
@@ -36,6 +39,14 @@
  get currentAppointmentIdFromMsg(): string|null {
    return this.appointmentIdFromMsg;
  }
  get currentSatisfactionIdFromMsg(): string|null {
    return this.satisfactionIdFromMsg;
  }
  get currentNotContactAppointmentIdFromMsg(): string|null {
    return this.notContactAppointmentIdFromMsg;
  }
  @Mutation storageIdToken(token: string): void {
@@ -68,12 +79,24 @@
    this.appointmentIdFromMsg = localStorage.getItem('appointmentIdFromMsg');
  }
  @Mutation storageSatisfactionIdFromMsg(id: string) {
    localStorage.setItem('satisfactionIdFromMsg', id);
    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');
    localStorage.removeItem('id_token');
    localStorage.removeItem('current_role');
    localStorage.removeItem('consultant_id');
    localStorage.removeItem('appointment');
    localStorage.removeItem('login_consultant');
    this.id_token = localStorage.getItem('id_token');
    this.current_role = localStorage.getItem('current_role');
    this.consultant_id = localStorage.getItem('consultant_id');
@@ -95,6 +118,21 @@
    this.appointmentIdFromMsg = localStorage.getItem('appointmentIdFromMsg');
  }
  @Mutation storageClearSatisfactionIdFromMsg() {
    localStorage.removeItem('satisfactionIdFromMsg');
    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");
  }
PAMapp/store/login.store.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,28 @@
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import myConsultantService from '~/shared/services/my-consultant.service';
import { AgentInfo } from '~/shared/models/agent-info.model';
@Module
export default class AppointmentStore extends VuexModule {
  loginConsultant?: AgentInfo = JSON.parse(localStorage.getItem('login_consultant')!);
  //////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////
  @Mutation
  SET_LOGIN_CONSULTANT(agentInfo: AgentInfo): void {
    this.loginConsultant = agentInfo;
    localStorage.setItem('login_consultant', JSON.stringify(agentInfo));
  }
  //////////////////////////////////////////////////////////////////////
  @Action({ commit: 'SET_LOGIN_CONSULTANT' })
  async getLoginConsultantDetail(agentNo: string): Promise<AgentInfo> {
    return await myConsultantService.getConsultantDetail(agentNo).then((res) => res);
  }
}
pamapi/src/doc/sql/20211229_j.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,10 @@
CREATE TABLE omo.appointment_memo (
     id bigserial NOT NULL,
     "content" varchar NOT NULL,
     appointment_id int8 NOT NULL,
     created_date timestamp NOT NULL,
     created_by varchar NOT NULL,
     last_modified_date timestamp NOT NULL,
     last_modified_by varchar NOT NULL,
     CONSTRAINT appointment_memo_pkey PRIMARY KEY (id)
);
pamapi/src/doc/sql/20220103_w.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,10 @@
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_AGAM11249699.jpg' WHERE agent_no='AGAM11249699';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_B282677963.jpg' WHERE agent_no='B282677963';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_AG0101234567.jpg' WHERE agent_no='AG0101234567';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_R221444250.jpg' WHERE agent_no='R221444250';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_AG0109051204.jpg' WHERE agent_no='AG0109051204';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_A183619275.jpg' WHERE agent_no='A183619275';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_X147309614.jpg' WHERE agent_no='X147309614';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_D265260662.jpg' WHERE agent_no='D265260662';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_J149388015.jpg' WHERE agent_no='J149388015';
UPDATE omo.consultant SET photo_path='C://pam_file/consultant_Z152717443.jpg' WHERE agent_no='Z152717443';
pamapi/src/doc/sql/20220112_j.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,66 @@
-- æ–°å¢žé ç´„單通知歷程table
CREATE TABLE public.appointment_notice_log (
    id bigserial NOT NULL,
    appointment_id bigserial NOT NULL,
    email varchar NULL,
    phone varchar NULL,
    "content" varchar NULL,
    created_date timestamp NULL,
    CONSTRAINT appointment_notice_log_pkey PRIMARY KEY (id)
);
-- æ–°å¢žé ç´„單備註table
CREATE TABLE public.appointment_memo (
    id bigserial NOT NULL,
    "content" varchar NOT NULL,
    appointment_id int8 NOT NULL,
    created_date timestamp NOT NULL,
    created_by varchar NOT NULL,
    last_modified_date timestamp NOT NULL,
    last_modified_by varchar NOT NULL,
    CONSTRAINT appointment_memo_pkey PRIMARY KEY (id)
);
-- æ–°å¢žç´„訪紀錄table
CREATE TABLE public.interview_record (
    id bigserial NOT NULL,
    created_date timestamp NOT NULL,
    created_by varchar NOT NULL,
    last_modified_date timestamp NOT NULL,
    last_modified_by varchar NOT NULL,
    "content" varchar NULL,
    interview_date timestamp NULL,
    appointment_id bigserial NOT NULL,
    status varchar NULL,
    CONSTRAINT interview_record_pkey PRIMARY KEY (id)
);
-- æ–°å¢žé ç´„單結案資料table
-- Drop table
-- DROP TABLE public.appointment_closed_info;
-- Drop table
-- DROP TABLE public.appointment_closed_info;
CREATE TABLE public.appointment_closed_info (
    id bigserial NOT NULL,
    policyholder_identity_id varchar NULL,
    plan_code varchar NULL,
    policy_entry_date date NULL,
    remark varchar NULL,
    closed_reason varchar NULL,
    closed_other_reason varchar NULL,
    appointment_id bigserial NOT NULL,
    CONSTRAINT appointment_closed_info_pkey PRIMARY KEY (id)
);
pamapi/src/doc/sql/20220114_j.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,2 @@
-- appointment_notice_log æ–°å¢žç´„訪時間欄位
ALTER TABLE appointment_notice_log ADD interview_date timestamp null;
pamapi/src/doc/sql/20220120_j.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
-- å»ºç«‹å°éˆ´éºé€šçŸ¥çš„table
CREATE TABLE public.personal_notification (
    id bigserial NOT NULL,
    title varchar NULL,
    "content" varchar NOT NULL,
    notification_type varchar NOT NULL,
    owner_role varchar NOT NULL,
    owner_id bigserial NOT NULL,
    created_date timestamp NOT NULL,
    read_date timestamp NULL,
    CONSTRAINT personal_notification_pkey PRIMARY KEY (id)
);
pamapi/src/doc/sql/20220121_j.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,3 @@
-- èª¿æ•´policy_entry_date åž‹æ…‹
ALTER TABLE appointment_closed_info
ALTER COLUMN policy_entry_date TYPE timestamp;
pamapi/src/doc/sql/20220122_w.sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
CREATE TABLE public.appointment_expiring_notify_record (
   id bigserial NOT NULL,
   appointment_id bigserial NOT NULL,
   send_time timestamp NULL,
   CONSTRAINT appointment_expiring_notify_record_pk PRIMARY KEY (id)
);
pamapi/src/doc/sql/²bªÅ¾ã­Ó¨t²Î¸ê®Æ(°£ÅU°Ý).sql
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
truncate public.appointment;
truncate public.appointment_closed_info;
truncate public.appointment_expiring_notify_record;
truncate public.appointment_memo;
truncate public.appointment_notice_log;
truncate public.customer;
truncate public.interview_record;
truncate public.login_record;
truncate public.otp_tmp;
truncate public.personal_notification;
truncate public.satisfaction;
truncate public.customer_favorite_consultant;
pamapi/src/doc/¤p¹aÅL³qª¾API/¨ú±oµn¤JªÌ©Ò¦³¤p¹aÅL³qª¾API
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,38 @@
http get:
http://localhost:8080/api/personal_notification/getMyPersonalNotification
title: æ¨™é¡Œ
content: é€šçŸ¥å…§å®¹
notificationType: ACTIVITY(個人活動通知)、SYSTEM(系統通知)
ownerRole: CUSTOMER(客戶)、CONSULTANT(顧問)
ownerId: ç™»å…¥è€…id
readDate: å·²è®€æ™‚é–“
request body :
[
    {
        "id": 1,
        "title": "title test",
        "content": "content test",
        "notificationType": "ACTIVITY",
        "ownerRole": "CONSULTANT",
        "ownerId": 11,
        "createdDate": "2022-01-20T10:53:53.022Z",
        "readDate": null
    },
    {
        "id": 2,
        "title": "title test",
        "content": "content test",
        "notificationType": "ACTIVITY",
        "ownerRole": "CONSULTANT",
        "ownerId": 11,
        "createdDate": "2022-01-20T10:59:17.242Z",
        "readDate": null
    }
]
pamapi/src/doc/¤p¹aÅL³qª¾API/©Ò¦³¤p¹aÅL³qª¾³]¬°¤wŪ.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
http post :
將目前登入者的所有小鈴鐺通知設定為已讀
http://localhost:8080/api/personal_notification/readAllMyNotification
pamapi/src/doc/º¡·N«×/«È¤á¶ñ¼gº¡·N«×.txt
@@ -1,8 +1,9 @@
http post :
http://localhost:8080/api/satisfaction/create
http://localhost:8080/api/satisfaction/score
填寫一筆:
request body:
@@ -21,4 +22,33 @@
    "agentNo": "admin",
    "status": "UNFILLED",
    "score": 4
}
}
=====================================================
填寫多筆滿意度:
http post :
http://localhost:8080/api/satisfaction/score/all
request body:
[{
    "appointmentId": 67,
    "score":4
}]
response body:
[{
    "id": 3,
    "customerId": 2,
    "agentNo": "admin",
    "status": "UNFILLED",
    "score": 4
}]
pamapi/src/doc/º¡·N«×/ÅU°Ý¥D°Êµo°eº¡·N«×³qª¾API.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1 @@
http://localhost:8080/api/consultant/sendSatisfactionToClient/{appointmentId}
pamapi/src/doc/¬ù³X¬ö¿ýAPI/­×§ï¬ù³X¬ö¿ý.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,31 @@
http post :
http://localhost:8080/api/interview_record/update
id : interviewRecord id
content : ç´„訪紀錄內容
interviewDate: é è¨ˆç´„訪時間
appointmentId: é ç´„å–®id
request body :
{
    "id":5,
    "content":"test record content2",
    "interviewDate":"2021-01-01T08:00:00.000",
    "appointmentId": 385
}
response body :
{
    "id": 5,
    "content": "test record content2",
    "interviewDate": "2021-01-01T08:00:00.000+00:00",
    "appointmentId": 385
}
pamapi/src/doc/¬ù³X¬ö¿ýAPI/§R°£¬ù³X¬ö¿ý.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
http delete :
http://localhost:8080/api/interview_record/{interviewRecordId}
http://localhost:8080/api/interview_record/5
pamapi/src/doc/¬ù³X¬ö¿ýAPI/·s¼W¬ù³X¬ö¿ý.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,27 @@
http post :
http://localhost:8080/api/interview_record/create
interviewDate: é è¨ˆç´„訪日期
content : ç´€éŒ„內容
appointmentId: é ç´„å–®id
request body :
{
    "content":"test record content",
    "interviewDate":"2021-01-01T08:00:00.000",
    "appointmentId": 385
}
response body :
{
    "id": 5,
    "content": "test record content",
    "interviewDate": "2021-01-01T08:00:00.000+00:00",
    "appointmentId": 385
}
pamapi/src/doc/¬ù³X³qª¾API/µo°e¬ù³X³qª¾API.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,17 @@
http post :
http://localhost:8080/api/notice/send
message: ç™¼é€çš„æ–‡å­—內容
appointmentId : ç´„訪通知的預約單id
interviewDate: é è¨ˆç´„訪時段
request body:
{
    "message":"notice customer invterview time",
    "email":"pollex@gmail.com",
    "phone":"0912345678",
    "appointmentId": 406,
    "interviewDate":"2022-11-01T08:00:00.000"
}
pamapi/src/doc/¹w¬ù³æ/§R°£¹w¬ù³æ³Æµù.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
http delete:
http://localhost:8080/api/appointment/memo/{appointmentMemoId}
http://localhost:8080/api/appointment/memo/1
pamapi/src/doc/¹w¬ù³æ/¨ú±o¹w¬ù³æ²Ó¸`API.txt
@@ -3,26 +3,62 @@
http://localhost:8080/api/appointment/getDetail/{appointmentId}
appointmentNoticeLogs: é ç´„單發送通知的歷程
appointmentClosedInfo: é ç´„單結案資料
response body:
{
    "id": 110,
    "phone": "09123456789",
    "email": "",
    "contactType": "mobile",
    "gender": "male",
    "age": "30",
    "id": 385,
    "phone": "0911223344",
    "email": "SDD",
    "contactType": "phone",
    "gender": "female",
    "age": "21-30",
    "job": "內勤",
    "requirement": "子女教育,資產規劃,防疫保單相關",
    "communicateStatus": "contacted",
    "hopeContactTime": "'禮拜三,禮拜五、12:00~14:00,14:00~18:00'",
    "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-02T09:58:58.932Z",
    "lastModifiedDate": "2021-12-02T09:58:58.932Z",
    "agentNo": "AG0109051204",
    "customerId": 70,
    "name": null,
    "consultantViewTime": "2021-12-02T09:58:12.066Z",
    "consultantReadTime": "2021-12-02T09:58:58.930Z",
    "contactTime": "2021-12-02T09:58:58.930Z",
    "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
}
    "appointmentDate": "2021-12-16T07:11:05.400Z",
    "lastModifiedDate": "2022-01-19T10:57:51.380Z",
    "agentNo": "A568420",
    "customerId": 139,
    "name": "Angula-test",
    "consultantViewTime": "2021-12-27T02:02:18.711Z",
    "consultantReadTime": "2021-12-28T07:16:01.295Z",
    "contactTime": "2021-12-28T07:16:37.004Z",
    "satisfactionScore": null,
    "appointmentMemoList": [],
    "interviewRecordDTOs": [],
    "appointmentNoticeLogs": [
        {
            "id": 4,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-11T09:33:57.754Z",
            "interviewDate": null
        },
        {
            "id": 6,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-19T10:38:42.187Z",
            "interviewDate": "2022-11-01T08:00:00.000+00:00"
        }
    ],
    "appointmentClosedInfo": {
        "id": 9,
        "policyholderIdentityId": "A123456789",
        "planCode": "ATMdd",
        "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
        "remark": "test remark",
        "closedReason": "other2",
        "closedOtherReason": "心情不好不想買2",
        "appointmentId": 385
    }
}
pamapi/src/doc/¹w¬ù³æ/«È¤á¨ú±o³Ì·s¹w¬ùªº¥¼³B²z¹w¬ù³æ.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,61 @@
http get :
http://localhost:8080/api/appointment/customer/expiring/newest
簡訊及email會以該網址進入首頁 -> http://localhost:3000?notContactAppointmentId={最新一筆未處理預約單}
response body: è‹¥æœ‰æœƒå‚³200並給以下資料,若無(未有任何逾期未處理預約單,則會回null)
{
    "id": 385,
    "phone": "0911223344",
    "email": "SDD",
    "contactType": "phone",
    "gender": "female",
    "age": "21-30",
    "job": "內勤",
    "requirement": "健康與保障",
    "communicateStatus": "done",
    "hopeContactTime": "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
    "otherRequirement": null,
    "appointmentDate": "2021-12-16T07:11:05.400Z",
    "lastModifiedDate": "2022-01-19T10:57:51.380Z",
    "agentNo": "A568420",
    "customerId": 139,
    "name": "Angula-test",
    "consultantViewTime": "2021-12-27T02:02:18.711Z",
    "consultantReadTime": "2021-12-28T07:16:01.295Z",
    "contactTime": "2021-12-28T07:16:37.004Z",
    "satisfactionScore": null,
    "appointmentMemoList": [],
    "interviewRecordDTOs": [],
    "appointmentNoticeLogs": [
        {
            "id": 4,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-11T09:33:57.754Z",
            "interviewDate": null
        },
        {
            "id": 6,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-19T10:38:42.187Z",
            "interviewDate": "2022-11-01T08:00:00.000+00:00"
        }
    ],
    "appointmentClosedInfo": {
        "id": 9,
        "policyholderIdentityId": "A123456789",
        "planCode": "ATMdd",
        "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
        "remark": "test remark",
        "closedReason": "other2",
        "closedOtherReason": "心情不好不想買2",
        "appointmentId": 385
    }
}
pamapi/src/doc/¹w¬ù³æ/·s¼W¹w¬ù³æ³Æµù.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,14 @@
http post :
http://localhost:8080/api/appointment/memo/create
content : é ç´„單備註內容
appointmentId : é ç´„å–®id
request body:
{
    "content":"test",
    "appointmentId":368
}
pamapi/src/doc/¹w¬ù³æ/§ó·s¹w¬ù³æ³Æµù.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
http post :
http://localhost:8080/api/appointment/memo/update
id : memo id
request body:
{
    "id": 1 ,
    "content":"test4"
}
pamapi/src/doc/¹w¬ù³æ/¼Ð°O¬°¤wÁpµ¸API.txt
@@ -1,2 +1,58 @@
http post :
http post :
http://localhost:8080/api/appointment/markAsContacted/{appointmentId}
response body:
{
    "id": 385,
    "phone": "0911223344",
    "email": "SDD",
    "contactType": "phone",
    "gender": "female",
    "age": "21-30",
    "job": "內勤",
    "requirement": "健康與保障",
    "communicateStatus": "done",
    "hopeContactTime": "'星期一,星期二,星期三,星期四,星期五,星期六,星期日、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00'",
    "otherRequirement": null,
    "appointmentDate": "2021-12-16T07:11:05.400Z",
    "lastModifiedDate": "2022-01-19T10:57:51.380Z",
    "agentNo": "A568420",
    "customerId": 139,
    "name": "Angula-test",
    "consultantViewTime": "2021-12-27T02:02:18.711Z",
    "consultantReadTime": "2021-12-28T07:16:01.295Z",
    "contactTime": "2021-12-28T07:16:37.004Z",
    "satisfactionScore": null,
    "appointmentMemoList": [],
    "interviewRecordDTOs": [],
    "appointmentNoticeLogs": [
        {
            "id": 4,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-11T09:33:57.754Z",
            "interviewDate": null
        },
        {
            "id": 6,
            "phone": "0912345678",
            "email": "pollex@gmail.com",
            "appointmentId": 385,
            "content": "notice customer invterview time",
            "createdDate": "2022-01-19T10:38:42.187Z",
            "interviewDate": "2022-11-01T08:00:00.000+00:00"
        }
    ],
    "appointmentClosedInfo": {
        "id": 9,
        "policyholderIdentityId": "A123456789",
        "planCode": "ATMdd",
        "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
        "remark": "test remark",
        "closedReason": "other2",
        "closedOtherReason": "心情不好不想買2",
        "appointmentId": 385
    }
}
pamapi/src/doc/¹w¬ù³æ/µ²®×API.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,32 @@
http post :
新增結案明細, æ›´æ–°çµæ¡ˆæ˜Žç´°:
http://localhost:8080/api/appointment/close
request body :
成交
contactStatus: done
{
    "policyholderIdentityId":"A123456789",
    "planCode":"ATM",
    "policyEntryDate":"2022-01-12",
    "contactStatus":"done",
    "appointmentId": 385,
    "remark":"test remark"
}
未成交
contactStatus: closed
{
    "contactStatus":"closed",
    "closedReason":"other",
    "closedOtherReason":"心情不好不想買",
    "appointmentId": 385,
    "remark":"test remark"
}
pamapi/src/doc/¹w¬ù³æ/ÅU°Ý¨ú±o©Ò¦³¦Û¤vªº¹w¬ù³æAPI.txt
@@ -2,139 +2,62 @@
http://localhost:8080/api/consultant/getMyAppointment
appointmentMemoList : å‚™è¨»è³‡æ–™
interviewRecordDTOs : ç´„訪紀錄
[
    {
        "id": 110,
        "phone": "09123456789",
        "email": "",
        "contactType": "mobile",
        "gender": "male",
        "age": "30",
        "job": "內勤",
        "requirement": "子女教育,資產規劃,防疫保單相關",
        "communicateStatus": "contacted",
        "hopeContactTime": "'禮拜三,禮拜五、12:00~14:00,14:00~18:00'",
        "otherRequirement": null,
        "appointmentDate": "2021-12-02T09:58:58.932Z",
        "lastModifiedDate": "2021-12-02T09:58:58.932Z",
        "agentNo": "AG0109051204",
        "customerId": 70,
        "name": null,
        "consultantViewTime": "2021-12-02T09:58:12.066Z",
        "consultantReadTime": "2021-12-02T09:58:58.930Z",
        "contactTime": "2021-12-02T09:58:58.930Z",
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
    },
    {
        "id": 109,
        "phone": "09123456789",
        "email": "",
        "contactType": "mobile",
        "gender": "male",
        "age": "20",
        "job": "內勤",
        "requirement": "健康與保障,子女教育,保單健檢/規劃,防疫保單相關",
        "communicateStatus": "contacted",
        "hopeContactTime": "'禮拜一,禮拜二,禮拜三,禮拜四,禮拜五,禮拜六,禮拜日、12:00~14:00,14:00~18:00'",
        "otherRequirement": null,
        "appointmentDate": "2021-12-02T10:12:24.613Z",
        "lastModifiedDate": "2021-12-02T10:12:24.613Z",
        "agentNo": "AG0109051204",
        "customerId": 67,
        "name": null,
        "consultantViewTime": "2021-12-02T09:58:12.066Z",
        "consultantReadTime": "2021-12-02T10:12:24.612Z",
        "contactTime": null,
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
    },
    {
        "id": 114,
        "phone": "09123456789",
        "email": "",
        "contactType": "mobile",
        "gender": "male",
        "age": "70",
        "job": "內勤",
        "requirement": "健康與保障,子女教育,資產規劃,樂活退休,防疫保單相關",
        "communicateStatus": "contacted",
        "hopeContactTime": "'禮拜一,禮拜二,禮拜三,禮拜四,禮拜五,禮拜六,禮拜日、12:00~14:00,14:00~18:00'",
        "otherRequirement": null,
        "appointmentDate": "2021-12-02T09:58:12.248Z",
        "lastModifiedDate": "2021-12-02T09:58:12.248Z",
        "agentNo": "AG0109051204",
        "customerId": 71,
        "name": null,
        "consultantViewTime": "2021-12-02T09:58:12.066Z",
        "consultantReadTime": null,
        "contactTime": null,
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
    },
    {
        "id": 121,
        "phone": "09123456789",
        "email": "",
        "contactType": "mobile",
        "gender": "male",
        "age": "20",
        "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-02T09:58:12.248Z",
        "lastModifiedDate": "2021-12-02T09:58:12.248Z",
        "agentNo": "AG0109051204",
        "customerId": 76,
        "name": "李哲維",
        "consultantViewTime": "2021-12-02T09:58:12.066Z",
        "consultantReadTime": "2021-12-02T09:54:20.872Z",
        "contactTime": null,
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
    },
    {
        "id": 118,
        "phone": "09123456789",
        "email": "",
        "contactType": "mobile",
        "gender": "male",
        "age": "20",
        "job": "內勤",
        "requirement": "健康與保障",
        "communicateStatus": "contacted",
        "hopeContactTime": "'禮拜一,禮拜二,禮拜三,禮拜四,禮拜五,禮拜六,禮拜日、12:00~14:00,14:00~18:00'",
        "otherRequirement": null,
        "appointmentDate": "2021-12-02T10:02:52.797Z",
        "lastModifiedDate": "2021-12-02T10:02:52.797Z",
        "agentNo": "AG0109051204",
        "customerId": 72,
        "name": null,
        "consultantViewTime": "2021-12-02T09:58:12.066Z",
        "consultantReadTime": "2021-12-02T10:02:52.796Z",
        "contactTime": null,
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
    },
    {
        "id": 180,
        "phone": "0911111111",
        "email": "",
        "id": 385,
        "phone": "0911223344",
        "email": "SDD",
        "contactType": "phone",
        "gender": "female",
        "age": "31-40",
        "age": "21-30",
        "job": "內勤",
        "requirement": "子女教育,健康與保障",
        "communicateStatus": "contacted",
        "hopeContactTime": "'禮拜二,禮拜三,禮拜四,禮拜五,禮拜六、9:00~12:00,12:00~14:00,14:00~18:00,18:00~21:00','禮拜一,禮拜二,禮拜三,禮拜四,禮拜五,禮拜日、12:00~14:00,14:00~18:00,18:00~21:00','禮拜一,禮拜四,禮拜六,禮拜日、14:00~18:00,18:00~21:00'",
        "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-02T10:10:53.341Z",
        "lastModifiedDate": "2021-12-03T03:40:16.344Z",
        "agentNo": "AG0109051204",
        "customerId": 77,
        "name": "111",
        "consultantViewTime": "2021-12-02T10:10:46.358Z",
        "consultantReadTime": "2021-12-02T10:10:53.340Z",
        "contactTime": "2021-12-03T03:40:16.215Z",
        "satisfactionScore":3 // null ä»£è¡¨è©²ç­†é ç´„單尚未填寫滿意度
        "appointmentDate": "2021-12-16T07:11:05.400Z",
        "lastModifiedDate": "2022-01-19T10:57:51.380Z",
        "agentNo": "A568420",
        "customerId": 139,
        "name": "Angula-test",
        "consultantViewTime": "2021-12-27T02:02:18.711Z",
        "consultantReadTime": "2021-12-28T07:16:01.295Z",
        "contactTime": "2021-12-28T07:16:37.004Z",
        "satisfactionScore": null,
        "appointmentMemoList": [],
        "interviewRecordDTOs": [],
        "appointmentNoticeLogs": [
            {
                "id": 4,
                "phone": "0912345678",
                "email": "pollex@gmail.com",
                "appointmentId": 385,
                "content": "notice customer invterview time",
                "createdDate": "2022-01-11T09:33:57.754Z",
                "interviewDate": null
            },
            {
                "id": 6,
                "phone": "0912345678",
                "email": "pollex@gmail.com",
                "appointmentId": 385,
                "content": "notice customer invterview time",
                "createdDate": "2022-01-19T10:38:42.187Z",
                "interviewDate": "2022-11-01T08:00:00.000+00:00"
            }
        ],
        "appointmentClosedInfo": {
            "id": 9,
            "policyholderIdentityId": "A123456789",
            "planCode": "ATMdd",
            "policyEntryDate": "2022-01-12T00:00:00.000+00:00",
            "remark": "test remark",
            "closedReason": "other2",
            "closedOtherReason": "心情不好不想買2",
            "appointmentId": 385
        }
    }
]
pamapi/src/doc/¹w¬ù³æ/ÅU°Ý¨ú±o¥¼³B²z¹w¬ù³æ¼Æ¶q³qª¾.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,5 @@
http get :
http://localhost:8080/api/appointment/consultant/pending/sum
response body:
2
pamapi/src/doc/ÅU°ÝAPI/¨ú±oÅU°ÝÀY¹³.txt
@@ -1,8 +1,8 @@
http get: http://localhost:8080/api/consultant/avatar/{fileName}
根據從顧問資料裡的img欄位的檔案名稱,取得顧問頭像的jpg檔案
http get: http://localhost:8080/api/consultant/avatar/{agentNo}
根據顧問的agentNo å–得顧問的照片
example request:
http://localhost:8080/api/consultant/avatar/avatar1.jpg
http://localhost:8080/api/consultant/avatar/A568420
response body: äºŒé€²åˆ¶æª”案
pamapi/src/doc/ÅU°ÝAPI/«ü©wÅU°Ý¸Ô²Ó¸ê°T.txt
@@ -2,27 +2,23 @@
response body:
{
    "name": "測試推薦業務員",
    "agentNo": "12345",
    "role": "保險經紀人",
    "img": "",
    "avgScore": 4.7,
    "title": "專案經理",
    "phoneNumber": "0912345678",
    "serveArea": "台北市地區",
    "companyAddress": "台北市信義區忠孝東路一段1號",
    "latestLoginTime": "2021-11-29T07:39:22.135Z",      // è‹¥ç„¡æœ€å¾Œç™»å…¥ç´€éŒ„則會帶null
    "seniority": "4å¹´2個月",
    "suitability": 0,
    "evaluation": 0,
    "expertise": [
        "財務規劃",
        "資產轉移"
    ],
    "concept": "壽險路上沒有捷徑,唯有給客戶信任感、安全感,才是最好的方法。從業以來,我一直秉持著「助人為快樂之本」的信念堅持著,她相信,一個好的業務人員,必須抱持著一顆熱心助人的心,才是永續經營壽險事業的不二法門。",
    "experiences": [
        "台大財金系",
        "美莓有精算師執政"
    ],
    "awards": "入選:2020年伯樂十大最佳業務員 æ“æœ‰è­‰ç…§ï¼šäººèº«ä¿éšªæ¥­å‹™å“¡è­‰ç…§ã€å¤–幣收付保險證照、人身保險代理人證照、財產保險代理人證照"
}
  "name" : "崔寨",
  "agentNo" : "R221444250",
  "role" : "台名保險經紀人",
  "img" : "avatar10.jpg",
  "avgScore" : 3.1,
  "title" : "處經理(DM)",
  "phoneNumber" : "0987168787",
  "serveArea" : "全台",
  "companyAddress" : "花蓮縣玉里鎮中正路30號9樓",
  "latestLoginTime" : "2021-12-24T08:48:21.497Z",
  "seniority" : "38 å¹´ ",
  "suitability" : 50,
  "evaluation" : 50,
  "expertise" : [ "健康與保障", "子女教育", "資產規劃", "樂活退休", "保單健檢/規劃", "分紅保單" ],
  "concept" : "每份保單規劃從「心」出發\r\n用心、熱心、貼心對待每位客戶\r\n\r\n我的三大信念\r\n◆ é«˜cp值 ã€Œä½Žä¿è²»ã€é«˜ä¿éšœã€\r\n◆ ç°¡å–®åŒ– ã€Œè¼•鬆、易懂保險」\r\n◆ å®¢è£½åŒ– ã€Œé¸ä¸€ä»½é©åˆçš„規劃」\r\n\r\n提供完善的規劃保護您及所愛的人",
  "experiences" : "麻省理工學院投資與金融學系,政大EMBA,中原大學財金系,人身保險證照,財產保險證照,投資型保單證照,外幣收付保險證照,退休金類精算師證照,產物保險理賠人員考試證書,美國壽險管理師證書,美國壽險理賠師證書,美國財產和意外險承保師證書",
  "awards" : "20次國際繼續率獎(IQA)\r\n30次百萬圓桌協會(MDRT)會員\r\n理財規劃顧問認證(CFP)",
  "gender" : "female",
  "communicationStyle" : "謹慎務實、明快主動、耐心傾聽、健談風趣"
}
pamapi/src/doc/ÅU°ÝAPI/½s¿è­×§ïÅU°Ý¸ê®Æ.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,26 @@
需要顧問本人登入才可以修改自己的資料
http post :
http://localhost:8080/api/consultant/edit
request body :
{
    "agentNo":"A568420",
    "name":"謝霆風",
    "expertise":["健康與保障","子女教育"],
    "title":"業務專員 (SC)",
    "role":"台名保險經紀人",
    "serveArea":"台北市、屏東縣",
    "gender":"male",
    "phoneNumber":"09123456789",
    "companyAddress":"宜蘭縣五結鄉三興路3號3樓",
    "seniorityYear":40,
    "seniorityMonth":10,
    "concept":"我秉持:❤不強迫推銷並且持續的服務!❤立場優勢替客戶捍衛應有的權益!❤天底下沒有最好的保險 åªæœ‰æœ€é©åˆè‡ªå·±çš„保險!",
    "experiences":"健康與保障,子女教育,資產規劃",
    "awards":"26次國際繼續率獎(IQA)",
    "communicationStyle":"聰明",
    "photoBase64":"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"
}
pamapi/src/main/java/com/pollex/pam/appointment/process/AppointmentProcess.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,57 @@
package com.pollex.pam.appointment.process;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.repository.AppointmentClosedInfoRepository;
import com.pollex.pam.repository.AppointmentRepository;
import com.pollex.pam.service.AppointmentClosedInfoService;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.dto.AbstractAppointmentProcessDTO;
import com.pollex.pam.service.dto.DoneProcessDTO;
@Service
public class AppointmentProcess{
    @Autowired
    List<AppointmentProcessInterface> processList;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    AppointmentRepository appointmentRepository;
    @Autowired
    AppointmentClosedInfoRepository appointmentClosedInfoRepository;
    public void process(AbstractAppointmentProcessDTO dto) {
        processList.stream().forEach(process ->{
            if(process.getProcessType() == dto.getContactStatus()) {
                Optional<AppointmentClosedInfo> closedInfoOP = appointmentClosedInfoRepository.findByAppointmentId(dto.getAppointmentId());
                if(closedInfoOP.isPresent()) {
                    process.editClosedInfo(dto, closedInfoOP.get());
                }else {
                    process.create(dto);
                }
            }
        });
        changeAppointmentCommunicateStatus(dto.getAppointmentId(), dto.getContactStatus());
    }
    private void changeAppointmentCommunicateStatus(Long appointmentId, ContactStatusEnum contactStatus) {
        Appointment appointment = appointmentService.findById(appointmentId);
        appointment.setCommunicateStatus(contactStatus);
        appointmentRepository.save(appointment);
    }
}
pamapi/src/main/java/com/pollex/pam/appointment/process/AppointmentProcessInterface.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,14 @@
package com.pollex.pam.appointment.process;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.service.dto.AbstractAppointmentProcessDTO;
public interface AppointmentProcessInterface {
    AppointmentClosedInfo create(AbstractAppointmentProcessDTO dto);
    AppointmentClosedInfo editClosedInfo(AbstractAppointmentProcessDTO dto
            , AppointmentClosedInfo closedInfo);
    ContactStatusEnum getProcessType();
}
pamapi/src/main/java/com/pollex/pam/appointment/process/ClosedProcess.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,65 @@
package com.pollex.pam.appointment.process;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.repository.AppointmentClosedInfoRepository;
import com.pollex.pam.service.AppointmentClosedInfoService;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.SatisfactionService;
import com.pollex.pam.service.dto.AbstractAppointmentProcessDTO;
import com.pollex.pam.service.dto.ClosedProcessDTO;
@Service
@Transactional
public class ClosedProcess implements AppointmentProcessInterface{
    @Autowired
    AppointmentClosedInfoRepository appointmentClosedInfoRepository;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    AppointmentClosedInfoService appointmentClosedInfoService;
    @Autowired
    SatisfactionService satisfactionService;
    @Override
    public AppointmentClosedInfo create(AbstractAppointmentProcessDTO processDTO) {
        ClosedProcessDTO closeProcess = toClosedProcessDTO(processDTO);
        AppointmentClosedInfo closedInfo = new AppointmentClosedInfo();
        BeanUtils.copyProperties(closeProcess, closedInfo);
        Appointment appointment = appointmentService.findById(processDTO.getAppointmentId());
        satisfactionService.createSatisfaction(appointment);
        return appointmentClosedInfoRepository.save(closedInfo);
    }
    private ClosedProcessDTO toClosedProcessDTO(AbstractAppointmentProcessDTO processDTO) {
        ClosedProcessDTO closeProcess = (ClosedProcessDTO)processDTO;
        BeanUtils.copyProperties(processDTO, closeProcess);
        return closeProcess;
    }
    @Override
    public ContactStatusEnum getProcessType() {
        return ContactStatusEnum.CLOSED;
    }
    @Override
    public AppointmentClosedInfo editClosedInfo(
            AbstractAppointmentProcessDTO abstractDTO
            , AppointmentClosedInfo closedInfo) {
        ClosedProcessDTO closeProcess =  toClosedProcessDTO(abstractDTO);
        BeanUtils.copyProperties(closeProcess, closedInfo);
        return appointmentClosedInfoRepository.save(closedInfo);
    }
}
pamapi/src/main/java/com/pollex/pam/appointment/process/DoneProcess.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,64 @@
package com.pollex.pam.appointment.process;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.repository.AppointmentClosedInfoRepository;
import com.pollex.pam.service.AppointmentClosedInfoService;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.SatisfactionService;
import com.pollex.pam.service.dto.AbstractAppointmentProcessDTO;
import com.pollex.pam.service.dto.DoneProcessDTO;
@Service
@Transactional
public class DoneProcess implements AppointmentProcessInterface{
    @Autowired
    AppointmentClosedInfoRepository appointmentClosedInfoRepository;
    @Autowired
    AppointmentClosedInfoService appointmentClosedInfoService;
    @Autowired
    SatisfactionService satisfactionService;
    @Autowired
    AppointmentService appointmentService;
    @Override
    public AppointmentClosedInfo create(AbstractAppointmentProcessDTO processDTO) {
        DoneProcessDTO doneProcess = toDoneProcessDTO(processDTO);
        AppointmentClosedInfo closedInfo = new AppointmentClosedInfo();
        BeanUtils.copyProperties(doneProcess, closedInfo);
        Appointment appointment = appointmentService.findById(processDTO.getAppointmentId());
        satisfactionService.createSatisfaction(appointment);
        return appointmentClosedInfoRepository.save(closedInfo);
    }
    @Override
    public ContactStatusEnum getProcessType() {
        return ContactStatusEnum.DONE;
    }
    @Override
    public AppointmentClosedInfo editClosedInfo(
            AbstractAppointmentProcessDTO abstractDTO
            , AppointmentClosedInfo closedInfo) {
        DoneProcessDTO doneProcess =  toDoneProcessDTO(abstractDTO);
        BeanUtils.copyProperties(doneProcess, closedInfo);
        return appointmentClosedInfoRepository.save(closedInfo);
    }
    private DoneProcessDTO toDoneProcessDTO(AbstractAppointmentProcessDTO abstractDTO) {
        DoneProcessDTO doneProcess = (DoneProcessDTO)abstractDTO;
        BeanUtils.copyProperties(abstractDTO, doneProcess);
        return doneProcess;
    }
}
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,9 +20,9 @@
    private String eServiceLoginFunc;
    private String eServiceLoginSys;
    private String frontEndDomain;
    private boolean sendNotifyMsg;
    private SMS sms;
    private Email email;
    private String fileFolderPath;
    public boolean isMockLogin() {
        return mockLogin;
@@ -87,14 +88,6 @@
        this.frontEndDomain = frontEndDomain;
    }
    public boolean isSendNotifyMsg() {
        return sendNotifyMsg;
    }
    public void setSendNotifyMsg(boolean sendNotifyMsg) {
        this.sendNotifyMsg = sendNotifyMsg;
    }
    public SMS getSms() {
        return sms;
    }
@@ -117,6 +110,7 @@
        private String sender;
        private String smsType;
        private String subject;
        private boolean sendNotifyMsg;
        public String getUrl() {
            return url;
@@ -157,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;
@@ -187,5 +191,29 @@
        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;
    }
    public void setFileFolderPath(String fileFolderPath) {
        this.fileFolderPath = fileFolderPath;
    }
}
pamapi/src/main/java/com/pollex/pam/config/Constants.java
@@ -11,5 +11,26 @@
    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;
    public static final String SPRING_PROFILE_POLLEX_DEVELOPMENT = "pollex";
    private Constants() {}
}
pamapi/src/main/java/com/pollex/pam/domain/Appointment.java
@@ -2,14 +2,29 @@
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import javax.persistence.*;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
@EntityListeners(AuditingEntityListener.class)
@Entity
@@ -61,6 +76,7 @@
    private Instant appointmentDate = Instant.now();
    @Column(name = "last_modified_date")
    @LastModifiedDate
    private Instant lastModifiedDate = Instant.now();
    @Column(name = "agent_no")
@@ -81,6 +97,19 @@
    @Enumerated(value = EnumType.STRING)
    @Column(name = "status")
    private AppointmentStatusEnum status;
    @JoinColumn(name = "appointment_id")
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    private List<AppointmentMemo> appointmentMemoList;
//    @OneToOne(cascade = CascadeType.REMOVE,fetch=FetchType.EAGER)
////    @JoinColumn(name = "form_authority_id", referencedColumnName = "id")
//    @JoinColumn(name = "appointment_id", referencedColumnName = "id")
//    private AppointmentClosedInfo closedInfo;
//    @OneToOne(cascade = CascadeType.REMOVE
//            , mappedBy = "appointment", fetch=FetchType.LAZY)
//    private AppointmentClosedInfo closedInfo;
    public Long getId() {
        return id;
@@ -233,4 +262,24 @@
    public void setLastModifiedDate(Instant lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
    public List<AppointmentMemo> getAppointmentMemoList() {
        return appointmentMemoList;
    }
    public void setAppointmentMemoList(List<AppointmentMemo> appointmentMemoList) {
        this.appointmentMemoList = appointmentMemoList;
    }
//    public AppointmentClosedInfo getClosedInfo() {
//        return closedInfo;
//    }
//
//    public void setClosedInfo(AppointmentClosedInfo closedInfo) {
//        this.closedInfo = closedInfo;
//    }
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentClosedInfo.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,112 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "appointment_closed_info")
public class AppointmentClosedInfo implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "policyholder_identity_id")
    private String policyholderIdentityId;
    @Column(name = "plan_code")
    private String planCode;
    @Column(name = "policy_entry_date")
    private Date policyEntryDate;
    @Column(name = "remark")
    private String remark;
    @Column(name = "closed_reason")
    private String closedReason;
    @Column(name = "closed_other_reason")
    private String closedOtherReason;
    @Column(name = "appointment_id")
    private Long appointmentId;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getPolicyholderIdentityId() {
        return policyholderIdentityId;
    }
    public void setPolicyholderIdentityId(String policyholderIdentityId) {
        this.policyholderIdentityId = policyholderIdentityId;
    }
    public String getPlanCode() {
        return planCode;
    }
    public void setPlanCode(String planCode) {
        this.planCode = planCode;
    }
    public Date getPolicyEntryDate() {
        return policyEntryDate;
    }
    public void setPolicyEntryDate(Date policyEntryDate) {
        this.policyEntryDate = policyEntryDate;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public String getClosedReason() {
        return closedReason;
    }
    public void setClosedReason(String closedReason) {
        this.closedReason = closedReason;
    }
    public String getClosedOtherReason() {
        return closedOtherReason;
    }
    public void setClosedOtherReason(String closedOtherReason) {
        this.closedOtherReason = closedOtherReason;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentCustomerView.java
@@ -2,12 +2,17 @@
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import com.pollex.pam.enums.AppointmentStatusEnum;
@@ -84,6 +89,10 @@
    @Enumerated(value = EnumType.STRING)
    @Column(name = "status")
    private AppointmentStatusEnum status;
    @JoinColumn(name = "appointment_id")
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
    private List<AppointmentMemo> appointmentMemoList;
    public Long getId() {
        return id;
@@ -243,4 +252,14 @@
    public void setStatus(AppointmentStatusEnum status) {
        this.status = status;
    }
    public List<AppointmentMemo> getAppointmentMemoList() {
        return appointmentMemoList;
    }
    public void setAppointmentMemoList(List<AppointmentMemo> appointmentMemoList) {
        this.appointmentMemoList = appointmentMemoList;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentExpiringNotifyRecord.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,46 @@
package com.pollex.pam.domain;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@Entity
@Table(name = "appointment_expiring_notify_record")
public class AppointmentExpiringNotifyRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "appointment_id")
    private Long appointmentId;
    @Column(name = "send_time")
    private Instant sendTime;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
    public Instant getSendTime() {
        return sendTime;
    }
    public void setSendTime(Instant sendTime) {
        this.sendTime = sendTime;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentMemo.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,57 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "appointment_memo")
public class AppointmentMemo extends AbstractAuditingEntity implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "content")
    private String content;
    @Column(name = "appointment_id")
    private Long appointmentId;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/AppointmentNoticeLog.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,135 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = "appointment_notice_log")
public class AppointmentNoticeLog implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "phone")
    private String phone;
    @Column(name = "email")
    private String email;
    @Column(name = "appointment_id")
    private Long appointmentId;
//    @Column(name = "type")
//    private String type;
    @Column(name = "content")
    private String content;
//    @CreatedBy
//    @Column(name = "created_by", nullable = false, length = 50, updatable = false)
//    @JsonIgnore
//    private String createdBy;
    @CreatedDate
    @Column(name = "created_date", updatable = false)
    private Instant createdDate = Instant.now();
    @Column(name = "interview_date")
    private Date interviewDate;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
//    public String getType() {
//        return type;
//    }
//
//    public void setType(String type) {
//        this.type = type;
//    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
//    public String getCreatedBy() {
//        return createdBy;
//    }
//
//    public void setCreatedBy(String createdBy) {
//        this.createdBy = createdBy;
//    }
    public Instant getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }
    public Date getInterviewDate() {
        return interviewDate;
    }
    public void setInterviewDate(Date interviewDate) {
        this.interviewDate = interviewDate;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/Consultant.java
@@ -167,7 +167,7 @@
    }
    public void setCompanyAddress(String companyAddress) {
        companyAddress = companyAddress;
        this.companyAddress = companyAddress;
    }
    public Long getSeniorityYear() {
pamapi/src/main/java/com/pollex/pam/domain/InterviewRecord.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,87 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.pollex.pam.enums.InterviewRecordStatusEnum;
@Entity
@Table(name = "interview_record")
public class InterviewRecord extends AbstractAuditingEntity implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "content")
    private String content;
    @Column(name = "interview_date")
    private Date interviewDate;
    @Column(name = "appointment_id")
    private Long appointmentId;
    @Enumerated(EnumType.STRING)
    @Column(name = "status")
    private InterviewRecordStatusEnum status;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Date getInterviewDate() {
        return interviewDate;
    }
    public void setInterviewDate(Date interviewDate) {
        this.interviewDate = interviewDate;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
    public InterviewRecordStatusEnum getStatus() {
        return status;
    }
    public void setStatus(InterviewRecordStatusEnum status) {
        this.status = status;
    }
}
pamapi/src/main/java/com/pollex/pam/domain/PersonalNotification.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,123 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedDate;
import com.pollex.pam.enums.NotificationTypeEnum;
import com.pollex.pam.enums.PersonalNotificationRoleEnum;
@Entity
@Table(name = "personal_notification")
public class PersonalNotification implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "title")
    private String title;
    @Column(name = "content")
    private String content;
    @Enumerated(EnumType.STRING)
    @Column(name = "notification_type")
    private NotificationTypeEnum notificationType;
    @Enumerated(EnumType.STRING)
    @Column(name = "owner_role")
    private PersonalNotificationRoleEnum ownerRole;
    @Column(name = "owner_id")
    private Long ownerId;
    @CreatedDate
    @Column(name = "created_date", updatable = false)
    private Instant createdDate = Instant.now();
    @Column(name = "read_date")
    private Instant readDate;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public NotificationTypeEnum getNotificationType() {
        return notificationType;
    }
    public void setNotificationType(NotificationTypeEnum notificationType) {
        this.notificationType = notificationType;
    }
    public PersonalNotificationRoleEnum getOwnerRole() {
        return ownerRole;
    }
    public void setOwnerRole(PersonalNotificationRoleEnum ownerRole) {
        this.ownerRole = ownerRole;
    }
    public Long getOwnerId() {
        return ownerId;
    }
    public void setOwnerId(Long ownerId) {
        this.ownerId = ownerId;
    }
    public Instant getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }
    public Instant getReadDate() {
        return readDate;
    }
    public void setReadDate(Instant readDate) {
        this.readDate = readDate;
    }
}
pamapi/src/main/java/com/pollex/pam/enums/ContactStatusEnum.java
@@ -10,5 +10,14 @@
    RESERVED,
    @JsonProperty("contacted")
    CONTACTED
    CONTACTED,
    @JsonProperty("done")
    DONE,
    @JsonProperty("closed")
    CLOSED,
    @JsonProperty("cancel")
    CANCEL
}
pamapi/src/main/java/com/pollex/pam/enums/InterviewRecordStatusEnum.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum InterviewRecordStatusEnum {
    AVAILABLE,
    DELETED
}
pamapi/src/main/java/com/pollex/pam/enums/NotificationTypeEnum.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum NotificationTypeEnum {
    SYSTEM,
    ACTIVITY
}
pamapi/src/main/java/com/pollex/pam/enums/PersonalNotificationRoleEnum.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum PersonalNotificationRoleEnum {
    CUSTOMER,
    CONSULTANT
}
pamapi/src/main/java/com/pollex/pam/enums/SendEmailMsgMethod.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum SendEmailMsgMethod {
    PAM_EMAIL_SERVICE,
    POLLEX_GMAIL
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentClosedInfoRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,15 @@
package com.pollex.pam.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.AppointmentClosedInfo;
@Repository
public interface AppointmentClosedInfoRepository extends JpaRepository<AppointmentClosedInfo, Long>{
    Optional<AppointmentClosedInfo> findByAppointmentId(Long apId);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentCustomerViewRepository.java
@@ -2,6 +2,9 @@
import java.util.List;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -11,4 +14,5 @@
public interface AppointmentCustomerViewRepository extends JpaRepository<AppointmentCustomerView, Long>{
    List<AppointmentCustomerView> findByAgentNo(String agentNo);
    List<AppointmentCustomerView> findByAgentNoAndCustomerId(String agentNo, Long customerId);
    List<AppointmentCustomerView> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentExpiringNotifyRecordRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
package com.pollex.pam.repository;
import com.pollex.pam.domain.AppointmentExpiringNotifyRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AppointmentExpiringNotifyRecordRepository extends JpaRepository<AppointmentExpiringNotifyRecord, Long> {
    List<AppointmentExpiringNotifyRecord> findAllByAppointmentId(Long appointmentId);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentMemoRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
package com.pollex.pam.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.AppointmentMemo;
@Repository
public interface AppointmentMemoRepository extends JpaRepository<AppointmentMemo, Long>{
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentNoticeLogRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,15 @@
package com.pollex.pam.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.AppointmentNoticeLog;
@Repository
public interface AppointmentNoticeLogRepository extends JpaRepository<AppointmentNoticeLog, Long>{
    List<AppointmentNoticeLog> findByAppointmentId(Long appointmentId);
}
pamapi/src/main/java/com/pollex/pam/repository/AppointmentRepository.java
@@ -3,6 +3,8 @@
import java.util.List;
import java.util.Optional;
import com.pollex.pam.enums.AppointmentStatusEnum;
import com.pollex.pam.enums.ContactStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -16,4 +18,6 @@
    List<Appointment> findByAgentNoAndCustomerId(String agentNo, Long customerId);
    Optional<Appointment> findTopByAgentNoAndCustomerIdOrderByAppointmentDateDesc(String agentNo, Long customerId);
    List<Appointment> findAllByCommunicateStatusAndStatus(ContactStatusEnum contactStatus, AppointmentStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/repository/InterviewRecordRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,18 @@
package com.pollex.pam.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.enums.InterviewRecordStatusEnum;
@Repository
public interface InterviewRecordRepository extends JpaRepository<InterviewRecord, Long>{
    List<InterviewRecord> findByAppointmentId(Long appointmentId);
    List<InterviewRecord> findByAppointmentIdAndStatus(Long appointmentId, InterviewRecordStatusEnum status);
}
pamapi/src/main/java/com/pollex/pam/repository/PersonalNotificationRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,16 @@
package com.pollex.pam.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.PersonalNotification;
import com.pollex.pam.enums.PersonalNotificationRoleEnum;
@Repository
public interface PersonalNotificationRepository extends JpaRepository<PersonalNotification, Long>{
    List<PersonalNotification> findAllByOwnerRoleAndOwnerId(PersonalNotificationRoleEnum role, Long ownerId);
}
pamapi/src/main/java/com/pollex/pam/repository/SatisfactionRepository.java
@@ -3,6 +3,7 @@
import java.util.List;
import java.util.Optional;
import com.pollex.pam.enums.SatisfactionStatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -19,8 +20,10 @@
    Optional<Satisfaction> findOneByAppointmentId(Long appointmentId);
    List<Satisfaction> findAllByStatus(SatisfactionStatusEnum status);
    @Query(value = "SELECT avg(score) FROM satisfaction where agent_no=:agent_no"
            + " and score is not null"
            , nativeQuery = true)
    Float getAgentScoreAvg(@Param("agent_no") String agentNo);
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentClosedInfoService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,22 @@
package com.pollex.pam.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.repository.AppointmentClosedInfoRepository;
import com.pollex.pam.web.rest.errors.AppointmentClosedInfoNotFoundException;
@Service
@Transactional
public class AppointmentClosedInfoService {
    @Autowired
    AppointmentClosedInfoRepository appointmentClosedInfoRepository;
    public AppointmentClosedInfo findByAppointmentId(Long apId) {
        return appointmentClosedInfoRepository.findByAppointmentId(apId)
                .orElseThrow(AppointmentClosedInfoNotFoundException::new);
    }
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentMemoService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,60 @@
package com.pollex.pam.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentMemo;
import com.pollex.pam.repository.AppointmentMemoRepository;
import com.pollex.pam.repository.AppointmentRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.AppointmentMemoCreateDTO;
import com.pollex.pam.service.dto.AppointmentMemoUpdateDTO;
import com.pollex.pam.service.mapper.AppointmentMemoMapper;
import com.pollex.pam.web.rest.errors.AppointmentMemoNotFoundException;
import com.pollex.pam.web.rest.errors.AppointmentNotFoundException;
@Service
@Transactional
public class AppointmentMemoService {
    @Autowired
    AppointmentMemoRepository appointmentMemoRepository;
    @Autowired
    AppointmentMemoMapper appointmentMemoMapper;
    @Autowired
    AppointmentRepository appointmentRepository;
    public AppointmentMemo create(AppointmentMemoCreateDTO memoDTO) {
        AppointmentMemo memo = appointmentMemoMapper.toAppointmentMemo(memoDTO);
        return appointmentMemoRepository.save(memo);
    }
    public void checkPermission(Long appointmentId) {
        Appointment appointment = appointmentRepository.findById(appointmentId)
                .orElseThrow(AppointmentNotFoundException::new);
        if(!appointment.getAgentNo().equals(SecurityUtils.getAgentNo())) {
            throw new IllegalAccessError("not have permission");
        }
    }
    public AppointmentMemo update(AppointmentMemoUpdateDTO memoDTO) {
        AppointmentMemo memo = appointmentMemoRepository
                .findById(memoDTO.getId())
                .orElseThrow(AppointmentMemoNotFoundException::new);
        checkPermission(memo.getAppointmentId());
        appointmentMemoMapper.copyToAppointmentMemo(memoDTO, memo);
        return appointmentMemoRepository.save(memo);
    }
    public void delete(Long memoId) {
        AppointmentMemo memo = appointmentMemoRepository
                .findById(memoId)
                .orElseThrow(AppointmentMemoNotFoundException::new);
        checkPermission(memo.getAppointmentId());
        appointmentMemoRepository.delete(memo);
    }
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentNoticeLogService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,33 @@
package com.pollex.pam.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.repository.AppointmentNoticeLogRepository;
import com.pollex.pam.service.dto.AppointmentNoticeSendDTO;
import com.pollex.pam.service.mapper.AppointmentNoticeSendMapper;
@Service
@Transactional
public class AppointmentNoticeLogService {
    @Autowired
    AppointmentNoticeLogRepository appointmentNoticeLogRepository;
    @Autowired
    AppointmentNoticeSendMapper appointmentNoticeSendMapper;
    public AppointmentNoticeLog create(AppointmentNoticeSendDTO noticeSendDTO) {
        AppointmentNoticeLog appointmentNoticeLog =
                appointmentNoticeSendMapper.toAppointmentNoticeLog(noticeSendDTO);
        return appointmentNoticeLogRepository.save(appointmentNoticeLog);
    }
    public List<AppointmentNoticeLog> findByAppointmentId(Long appointmentId){
        return appointmentNoticeLogRepository.findByAppointmentId(appointmentId);
    }
}
pamapi/src/main/java/com/pollex/pam/service/AppointmentService.java
@@ -1,18 +1,25 @@
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 java.util.stream.Collectors;
import com.pollex.pam.appointment.process.AppointmentProcess;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.service.dto.AppointmentUpdateDTO;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -20,11 +27,10 @@
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentCustomerView;
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.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;
@@ -70,6 +76,15 @@
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    InterviewRecordService interviewRecordService;
    @Autowired
    AppointmentProcess abstractAppointmentProcess;
    @Autowired
    PersonalNotificationService personalNotificationService;
    public Appointment customerCreateAppointment(AppointmentCreateDTO appointmentCreateDTO) {
        Appointment appointment = appointmentDTOMapper.toAppointment(appointmentCreateDTO);
        appointment.setStatus(AVAILABLE);
@@ -78,9 +93,9 @@
        return appointmentRepository.save(appointment);
    }
    public void updateAppointment(AppointmentUpdateDTO updateAppointmentDTO) {
    public Appointment updateAppointment(AppointmentUpdateDTO updateAppointmentDTO) {
        Appointment appointment = appointmentRepository.findById(updateAppointmentDTO.getId()).get();
        BeanUtils.copyProperties(updateAppointmentDTO, appointment);
        appointment.setPhone(updateAppointmentDTO.getPhone());
        appointment.setEmail(updateAppointmentDTO.getEmail());
        appointment.setContactType(updateAppointmentDTO.getContactType());
@@ -92,14 +107,17 @@
        appointment.setOtherRequirement(updateAppointmentDTO.getOtherRequirement());
        appointment.setLastModifiedDate(Instant.now());
        appointmentRepository.save(appointment);
        return appointmentRepository.save(appointment);
    }
    public void markAppointmentDeleted(Long appointmentId) {
        Appointment appointment = appointmentRepository.findById(appointmentId).get();
        appointment.setStatus(DELETED);
        appointment.setLastModifiedDate(Instant.now());
        appointment.setCommunicateStatus(ContactStatusEnum.CANCEL);
        appointmentRepository.save(appointment);
        personalNotificationService.createMarkAppointmentDeletedToConsultant(appointment);
    }
    public List<Appointment> findByAgentNo(String agentNo) {
@@ -120,10 +138,16 @@
        AppointmentCustomerViewDTO dto = appointmentCustomerViewMapper.toAppointmentCustomerViewDTO(appointment);
        setSatisfactionScore(dto, appointmentId);
//        setInterviewRecordDTO(dto);
        return dto;
    }
    public List<AppointmentCustomerViewDTO> getConsultantAvailableAppointments(String agentNo) {
    public void setInterviewRecordDTO(AppointmentCustomerViewDTO dto) {
        List<InterviewRecordDTO> interviewRecords = interviewRecordService.findByAppointmentIdAndStatus(dto.getId(), InterviewRecordStatusEnum.AVAILABLE);
        dto.setInterviewRecordDTOs(interviewRecords);
    }
    public List<AppointmentCustomerViewDTO> getConsultantAvailableAppointments(String agentNo) {
        return appointmentCustomerViewRepository.findByAgentNo(agentNo).stream()
            .filter(appointment -> appointment.getStatus() == AVAILABLE)
            .map(appointmentCustomerView -> {
@@ -176,12 +200,12 @@
    public void sendAppointmentNotify(Appointment appointment) {
        Assert.notNull(appointment, "appointment entity cannot be null");
        log.debug("is need send appointment notify msg? = {}", applicationProperties.isSendNotifyMsg());
        if(applicationProperties.isSendNotifyMsg()) {
            log.debug("sending appointment notify, appointmentId = {}", appointment.getId());
            sendAppointmentNotifyBySMS(appointment);
            sendAppointmentNotifyByHtmlEmail(appointment);
        }
        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);
        sendAppointmentNotifyByHtmlEmail(appointment);
    }
    private void sendAppointmentNotifyBySMS(Appointment appointment) {
@@ -213,8 +237,7 @@
    }
    private void sendAppointmentNotifyByHtmlEmail(Appointment appointment) {
        String senderEmail = applicationProperties.getEmail().getSenderEmail();
        String consultantEmail = consultantService.findByAgentNo(appointment.getAgentNo()).getEmail();
       String consultantEmail = consultantService.findByAgentNo(appointment.getAgentNo()).getEmail();
        String customerMobile = appointment.getPhone();
        String normalContent;
@@ -235,7 +258,7 @@
                throw new SendEmailFailException("the consultant does not have email!");
            }
            sendMsgService.sendMsgByEmail(senderEmail, consultantEmail, NOTIFY_EMAIL_SUBJECT, content, true);
            sendMsgService.sendMsgByEmail(consultantEmail, NOTIFY_EMAIL_SUBJECT, content, true);
        } catch (SendEmailFailException e) {
            log.warn("send appointment notify by email was fail, appointment Id = {}", appointment.getId(), e);
        }
@@ -244,4 +267,53 @@
    public String getAppointmentDetailUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "/myAppointmentList/contactedList?appointmentId=" + appointmentId;
    }
    public Appointment findById(Long id) {
        return appointmentRepository.findById(id)
                .orElseThrow(AppointmentNotFoundException::new);
    }
    public void closeAppointment(AppointmentCloseDTO closeDTO) {
        if(closeDTO.getContactStatus() == ContactStatusEnum.DONE) {
            DoneProcessDTO dto = new DoneProcessDTO();
            BeanUtils.copyProperties(closeDTO, dto);
            abstractAppointmentProcess.process(dto);
        }else if(closeDTO.getContactStatus() == ContactStatusEnum.CLOSED){
            ClosedProcessDTO dto = new ClosedProcessDTO();
            BeanUtils.copyProperties(closeDTO, dto);
            abstractAppointmentProcess.process(dto);
        }
    }
    public Long getConsultantPendingAppointmentSum(String agentNo) {
        return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE)
                .stream()
                .filter(appointment -> agentNo.equals(appointment.getAgentNo()))
                .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_PENDING_PHONE_INTERVAL, Constants.APPOINTMENT_PENDING_EMAIL_INTERVAL))
                .count();
    }
    public AppointmentCustomerViewDTO getCustomerNewestExpiringAppointment(Long customerId) {
        return appointmentCustomerViewRepository.findAllByCommunicateStatusAndStatus(ContactStatusEnum.RESERVED, AVAILABLE)
                .stream()
                .filter(appointment -> customerId.equals(appointment.getCustomerId()))
                .filter(appointment -> isAppointmentDateNotInIntervalFromNow(appointment, Constants.APPOINTMENT_EXPIRING_PHONE_INTERVAL, Constants.APPOINTMENT_EXPIRING_EMAIL_INTERVAL))
                .max(Comparator.comparing(AppointmentCustomerView::getAppointmentDate))
                .map(appointmentCustomerView -> appointmentCustomerViewMapper.toAppointmentCustomerViewDTO(appointmentCustomerView))
                .orElse(null);
    }
    public boolean isAppointmentDateNotInIntervalFromNow(AppointmentCustomerView appointment, int phoneInterval, int emailInterval) {
        final boolean isHavePhone = StringUtils.hasText(appointment.getPhone());
        final boolean isHaveEmail = StringUtils.hasText(appointment.getEmail());
        LocalDate appointmentDate = appointment.getAppointmentDate().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate nowDate = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate();
        long intervalDays = nowDate.toEpochDay() - appointmentDate.toEpochDay();
        final boolean isAppointmentExpiringByPhone = isHavePhone && intervalDays >= phoneInterval;
        final boolean isAppointmentExpiringByEmail = isHaveEmail && intervalDays >= emailInterval;
        return isAppointmentExpiringByPhone || isAppointmentExpiringByEmail;
    }
}
pamapi/src/main/java/com/pollex/pam/service/ConsultantService.java
@@ -1,23 +1,38 @@
package com.pollex.pam.service;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentCustomerView;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.domain.CustomerFavoriteConsultant;
import com.pollex.pam.domain.Satisfaction;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.enums.LoginResult;
import com.pollex.pam.repository.ConsultantRepository;
import com.pollex.pam.repository.CustomerFavoriteConsultantRepository;
import com.pollex.pam.repository.SatisfactionRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.*;
import com.pollex.pam.service.mapper.AppointmentCustomerViewMapper;
import com.pollex.pam.service.mapper.ConsultantDTOMapper;
import com.pollex.pam.service.mapper.ConsultantMapper;
import com.pollex.pam.service.util.FileUtil;
import com.pollex.pam.web.rest.errors.ConsultantNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
@@ -27,6 +42,7 @@
import static com.pollex.pam.enums.ContactStatusEnum.*;
@Service
@Transactional
public class ConsultantService {
    private static final Logger log = LoggerFactory.getLogger(ConsultantService.class);
@@ -52,6 +68,30 @@
    @Autowired
    SatisfactionService satisfactionService;
    @Autowired
    ConsultantDTOMapper consultantDTOMapper;
    @Autowired
    ApplicationProperties applicationProperty;
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    ApplicationProperties applicationProperties;
    @Autowired
    ConsultantService consultantService;
    @Autowired
    SatisfactionRepository satisfactionRepository;
    @Autowired
    PersonalNotificationService personalNotificationService;
    public List<CustomerFavoriteConsultantDTO> getMyConsultantList() {
        Long customerId = SecurityUtils.getCustomerDBId();
@@ -60,49 +100,52 @@
            .map(relation -> {
                Consultant consultant = relation.getConsultant();
                CustomerFavoriteConsultantDTO dto = consultantMapper.toCustomerFavoriteConsultantDto(consultant);
                dto.setContactStatus(ContactStatusEnum.PICKED);
                dto.setCreateTime(relation.getCreatedDate());
                dto.setUpdateTime(relation.getCreatedDate());
                dto.setCustomerViewTime(relation.getViewTime());
                setAvailableAppointmentInfo(
                setInfoByAvailableAppointment(
                    dto,
                    appointmentService.findAvailableByAgentNoAndCustomerId(consultant.getAgentNo(), customerId)
                );
                appointmentService.findLatestAppointmentByAgentNoAndCustomerId(consultant.getAgentNo(), customerId)
                    .ifPresent(latestAppointment -> {
                        dto.setUpdateTime(latestAppointment.getLastModifiedDate());
                    });
                if(dto.getUpdateTime().isBefore(relation.getCreatedDate())) {
                    dto.setUpdateTime(relation.getCreatedDate());
                }
                setFavoriteConsultantUpdatedTime(relation, dto);
                return dto;
            }).collect(Collectors.toList());
    }
    private void setAvailableAppointmentInfo(CustomerFavoriteConsultantDTO customerFavoriteConsultantDTO, List<AppointmentCustomerView> appointmentList) {
    public void setFavoriteConsultantUpdatedTime(CustomerFavoriteConsultant relation,
            CustomerFavoriteConsultantDTO dto) {
        Consultant consultant = relation.getConsultant();
        appointmentService.findLatestAppointmentByAgentNoAndCustomerId(consultant.getAgentNo(), relation.getCustomerId())
            .ifPresent(latestAppointment -> {
                dto.setUpdateTime(latestAppointment.getLastModifiedDate());
            });
        if(dto.getUpdateTime().isBefore(relation.getCreatedDate())) {
            dto.setUpdateTime(relation.getCreatedDate());
        }
    }
    private void setInfoByAvailableAppointment(CustomerFavoriteConsultantDTO customerFavoriteConsultantDTO, List<AppointmentCustomerView> appointmentList) {
        List<AppointmentCustomerView> appointments = appointmentList.stream()
            .sorted(Comparator.comparing(AppointmentCustomerView::getAppointmentDate).reversed())
            .collect(Collectors.toList());
        List<AppointmentCustomerViewDTO> appointmentCustomerViewDTOS = appointmentCustomerViewMapper.toAppointmentCustomerViewDTO(appointments);
        appointmentCustomerViewDTOS.forEach(appointmentCustomerViewDTO -> {
            appointmentService.setSatisfactionScore(appointmentCustomerViewDTO, appointmentCustomerViewDTO.getId());
        });
        customerFavoriteConsultantDTO.setAppointments(appointmentCustomerViewDTOS);
        if (!appointments.isEmpty()) {
            AppointmentCustomerView latestAvailableAppointment = appointments.get(0);
            if(latestAvailableAppointment.getCommunicateStatus() == RESERVED)
                customerFavoriteConsultantDTO.setContactStatus(RESERVED);
            ContactStatusEnum latestStatus = latestAvailableAppointment.getCommunicateStatus();
            if(latestStatus != ContactStatusEnum.DONE && latestStatus != ContactStatusEnum.CLOSED)
                customerFavoriteConsultantDTO.setContactStatus(latestStatus);
            else
                customerFavoriteConsultantDTO.setContactStatus(PICKED);
        }else {
            customerFavoriteConsultantDTO.setContactStatus(PICKED);
        }
    }
@@ -201,4 +244,79 @@
    public Consultant findByAgentNo(String agentNo) {
        return consultantRepository.findOneByAgentNo(agentNo).get();
    }
    public Consultant editConsultant(ConsultantEditDTO editDTO) {
        Consultant consultant = consultantRepository.findOneByAgentNo(editDTO.getAgentNo())
                .orElseThrow(ConsultantNotFoundException::new);
        consultantDTOMapper.copyToConsultant(editDTO, consultant);
        FileUtil.base64ToFile(editDTO.getPhotoBase64(), editDTO.getPhotoFileName(), applicationProperty.getFileFolderPath());
        consultantRepository.save(consultant);
        personalNotificationService.createEditConsultantToConsultant(consultant);
        return consultant;
    }
    public InputStream getAvatarImage(String agentNo) {
        Consultant consultant = consultantRepository.findOneByAgentNo(agentNo)
                .orElseThrow(ConsultantNotFoundException::new);
        File file = new File(consultant.getPhotoPath());
        try {
            InputStream in = new FileInputStream(file);
            return in;
        } catch (FileNotFoundException e) {
            log.error("agent photo not found , agentNo:"+agentNo,e);
            return null;
        }
    }
    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);
        String contsultantName = consultant.getName();
        String content = contsultantName+"顧問請您填寫保誠媒合平台的滿意度評比"+getSendSatisfactionToClientUrl(appointment.getId());
        return content;
    }
    private String genSendSatisfactionEmailContent(Appointment appointment) {
        String agentNo = appointment.getAgentNo();
        Consultant consultant = consultantService.findByAgentNo(agentNo);
        Context context = new Context();
        context.setVariable("consultantName", consultant.getName());
        context.setVariable("appointmentUrl", getSendSatisfactionToClientUrl(appointment.getId()));
        String content = springTemplateEngine.process("mail/writeSatisfactionNotice", context);
        return content;
    }
    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())
                .get();
        consultant.setAvgScore(avgScore);
        consultantRepository.save(consultant);
    }
    public float getAgentAvgScore(String agentNo) {
        Float avgScore = satisfactionRepository.getAgentScoreAvg(agentNo);
        if(avgScore==null)return 0;
        BigDecimal bigDecimal = new BigDecimal(avgScore);
        return avgScore = bigDecimal.setScale(1,BigDecimal.ROUND_HALF_UP).floatValue();
    }
}
pamapi/src/main/java/com/pollex/pam/service/InterviewRecordService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,84 @@
package com.pollex.pam.service;
import java.util.List;
import org.hibernate.boot.model.naming.IllegalIdentifierException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.enums.InterviewRecordStatusEnum;
import com.pollex.pam.repository.InterviewRecordRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.InterviewRecordDTO;
import com.pollex.pam.service.mapper.InterviewRecordMapper;
import com.pollex.pam.web.rest.errors.InterviewRecordNotFoundException;
@Service
@Transactional
public class InterviewRecordService {
    @Autowired
    InterviewRecordRepository interviewRecordRepository;
    @Autowired
    InterviewRecordMapper interviewRecordMapper;
    @Autowired
    AppointmentService appointmentService;
    public InterviewRecord create(InterviewRecordDTO dto) {
        if(dto.getId()!=null) {
            throw new IllegalArgumentException();
        }
        InterviewRecord record = interviewRecordMapper.toInterviewRecord(dto);
        checkAuth(record);
        record.setStatus(InterviewRecordStatusEnum.AVAILABLE);
        interviewRecordRepository.save(record);
        return record;
    }
    public InterviewRecord update(InterviewRecordDTO dto) {
        if(dto.getId()==null) {
            throw new IllegalArgumentException();
        }
        InterviewRecord record = findById(dto.getId());
        checkAuth(record);
        interviewRecordMapper.copyToInterviewRecord(dto, record);
        interviewRecordRepository.save(record);
        return record;
    }
    public void checkAuth(InterviewRecord record) {
        Appointment appointment = appointmentService.findById(record.getAppointmentId());
        if(!appointment.getAgentNo().equals(SecurityUtils.getAgentNo())) {
            throw new IllegalAccessError("The account can not edit the appointment");
        }
    }
    public void delete(Long interviewRecordId) {
        InterviewRecord record = findById(interviewRecordId);
        record.setStatus(InterviewRecordStatusEnum.DELETED);
        interviewRecordRepository.save(record);
    }
    public InterviewRecord findById(Long id) {
        return interviewRecordRepository.findById(id)
        .orElseThrow(InterviewRecordNotFoundException::new);
    }
    public List<InterviewRecordDTO> findByAppointmentId(Long appointmentId) {
        List<InterviewRecord> records = interviewRecordRepository.findByAppointmentId(appointmentId);
        return interviewRecordMapper.toInterviewRecordDTO(records);
    }
    public List<InterviewRecordDTO> findByAppointmentIdAndStatus(Long appointmentId, InterviewRecordStatusEnum status) {
        List<InterviewRecord> records = interviewRecordRepository.findByAppointmentIdAndStatus(appointmentId, status);
        return interviewRecordMapper.toInterviewRecordDTO(records);
    }
}
pamapi/src/main/java/com/pollex/pam/service/NoticeService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,57 @@
package com.pollex.pam.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.enums.ContactStatusEnum;
import com.pollex.pam.repository.AppointmentRepository;
import com.pollex.pam.service.dto.AppointmentNoticeSendDTO;
@Service
@Transactional
public class NoticeService {
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    AppointmentNoticeLogService appointmentNoticeLogService;
    @Autowired
    AppointmentRepository appointmentRepository;
    @Autowired
    PersonalNotificationService personalNotificationService;
    public void sendNotice(AppointmentNoticeSendDTO dto) {
        String subject = "保誠媒合平台系統通知:預約通知";
        if(StringUtils.hasText(dto.getEmail())) {
            sendMsgService.sendMsgByEmail(dto.getEmail(), subject, dto.getMessage(), true);
        }if(StringUtils.hasText(dto.getPhone())) {
            sendMsgService.sendMsgBySMS(dto.getPhone(), dto.getMessage());
        }
        List<AppointmentNoticeLog> noticeLogs =
                appointmentNoticeLogService.findByAppointmentId(dto.getAppointmentId());
        if(noticeLogs.size()==0) {
            Appointment appointment = appointmentService.findById(dto.getAppointmentId());
            appointment.setCommunicateStatus(ContactStatusEnum.CONTACTED);
            appointmentRepository.save(appointment);
        }
        appointmentNoticeLogService.create(dto);
        personalNotificationService.createSendNoticeToCustomer(dto.getAppointmentId());
    }
}
pamapi/src/main/java/com/pollex/pam/service/PersonalNotificationService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,159 @@
package com.pollex.pam.service;
import java.time.Instant;
import java.util.List;
import javax.management.Notification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.domain.Customer;
import com.pollex.pam.domain.PersonalNotification;
import com.pollex.pam.domain.Satisfaction;
import com.pollex.pam.enums.NotificationTypeEnum;
import com.pollex.pam.enums.PersonalNotificationRoleEnum;
import com.pollex.pam.repository.CustomerRepository;
import com.pollex.pam.repository.PersonalNotificationRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.AppointmentUpdateDTO;
@Service
@Transactional
public class PersonalNotificationService {
    @Autowired
    PersonalNotificationRepository personalNotificationRepository;
    @Autowired
    ConsultantService consultantService;
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    CustomerService customerService;
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    SatisfactionService satisfactionService;
    public List<PersonalNotification> getMyPersonalNotification(Long ownerId, PersonalNotificationRoleEnum role) {
        return personalNotificationRepository.findAllByOwnerRoleAndOwnerId(role, ownerId);
    }
    public void createSendSatisfactionToClientToCustomer(Appointment appointment) {
        PersonalNotification entity = new PersonalNotification();
        Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
        String content = consultant.getName()+"顧問請您填寫滿意度評比";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(appointment.getCustomerId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CUSTOMER);
        entity.setTitle("填寫滿意度");
        personalNotificationRepository.save(entity);
    }
    public void createSendNoticeToCustomer(Long appointmentId) {
        Appointment appointment = appointmentService.findById(appointmentId);
        PersonalNotification entity = new PersonalNotification();
        Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
        String content = "您有 "+consultant.getName()+"顧問的約訪通知";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(appointment.getCustomerId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CUSTOMER);
        entity.setTitle("顧問約訪通知");
        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 = "您的個人帳號設定已進行更新";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(consultant.getId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CONSULTANT);
        entity.setTitle("變更帳號資料");
        personalNotificationRepository.save(entity);
    }
    public void createMarkAppointmentDeletedToConsultant(Appointment appointment) {
        PersonalNotification entity = new PersonalNotification();
        Customer customer = customerRepository.findById(appointment.getCustomerId()).get();
        Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
        String content = customer.getName()+"客戶已取消您的預約";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(consultant.getId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CONSULTANT);
        entity.setTitle("取消預約提醒");
        personalNotificationRepository.save(entity);
    }
    public void createUpdateAppointmentToConsultant(Appointment appointment) {
        PersonalNotification entity = new PersonalNotification();
        Customer customer = customerRepository.findById(appointment.getCustomerId()).get();
        Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
        String content = customer.getName()+"客戶已更新您的預約資訊";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(consultant.getId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CONSULTANT);
        entity.setTitle("更新預約提醒");
        personalNotificationRepository.save(entity);
    }
    public void createScorefactionToConsultant(Satisfaction satisfaction) {
        PersonalNotification entity = new PersonalNotification();
        Appointment appointment = appointmentService.findById(satisfaction.getAppointmentId());
        Customer customer = customerRepository.findById(appointment.getCustomerId()).get();
        Consultant consultant = consultantService.findByAgentNo(appointment.getAgentNo());
        String content = customer.getName()+"客戶已對您進行滿意度評比";
        entity.setContent(content);
        entity.setNotificationType(NotificationTypeEnum.ACTIVITY);
        entity.setOwnerId(consultant.getId());
        entity.setOwnerRole(PersonalNotificationRoleEnum.CONSULTANT);
        entity.setTitle("客戶滿意度");
        personalNotificationRepository.save(entity);
    }
    public void readAllMyNotification() {
        if(StringUtils.hasText(SecurityUtils.getAgentNo())) {
            Long consultantId = consultantService.findByAgentNo(SecurityUtils.getAgentNo()).getId();
            readAllNotification(PersonalNotificationRoleEnum.CONSULTANT, consultantId);
        }else if(SecurityUtils.getCustomerDBId()!=null){
            readAllNotification(PersonalNotificationRoleEnum.CUSTOMER, SecurityUtils.getCustomerDBId());
        }
    }
    public void readAllNotification(PersonalNotificationRoleEnum ownerRole
            , Long ownerId) {
        List<PersonalNotification> allNotification = personalNotificationRepository.findAllByOwnerRoleAndOwnerId(ownerRole, ownerId);
        Instant today = Instant.now();
        allNotification.stream()
        .filter(notification ->  notification.getReadDate()==null)
        .forEach(notification ->{
            notification.setReadDate(today);
            personalNotificationRepository.saveAll(allNotification);
        });
    }
}
pamapi/src/main/java/com/pollex/pam/service/SatisfactionService.java
@@ -1,6 +1,6 @@
package com.pollex.pam.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -9,16 +9,18 @@
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.domain.Satisfaction;
import com.pollex.pam.enums.SatisfactionStatusEnum;
import com.pollex.pam.repository.ConsultantRepository;
import com.pollex.pam.repository.CustomerRepository;
import com.pollex.pam.repository.SatisfactionRepository;
import com.pollex.pam.service.dto.SatisfactionCustomerCreateDTO;
import com.pollex.pam.service.dto.SatisfactionCustomerScoreDTO;
import com.pollex.pam.service.dto.SatisfactionDTO;
import com.pollex.pam.service.mapper.AppointmentMapper;
import com.pollex.pam.service.mapper.SatisfactionDTOMapper;
import com.pollex.pam.service.mapper.SatisfactionMapper;
import com.pollex.pam.web.rest.errors.SatisfactionAlreadyExistException;
import com.pollex.pam.web.rest.errors.SatisfactionNotFoundException;
@Service
@Transactional
@@ -38,47 +40,45 @@
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    ConsultantRepository consultantRepository;
    public Satisfaction createSatisfaction(Satisfaction satisfaction) {
    @Autowired
    ConsultantService consultantService;
    @Autowired
    PersonalNotificationService personalNotificationService;
    public Satisfaction save(Satisfaction satisfaction) {
        satisfaction = satisfactionRepository.save(satisfaction);
        setConsultantAvgScore(satisfaction);
        consultantService.setConsultantAvgScore(satisfaction);
        return satisfaction;
    }
    private void setConsultantAvgScore(Satisfaction satisfaction) {
        float avgScore = getAgentAvgScore(satisfaction);
        Consultant consultant = consultantRepository.findOneByAgentNo(satisfaction.getAgentNo())
                .get();
        consultant.setAvgScore(avgScore);
        consultantRepository.save(consultant);
    public Satisfaction scorefaction(SatisfactionCustomerScoreDTO scoreDTO) {
        Optional<Satisfaction> satisfactionOP = getByAppointmentId(scoreDTO.getAppointmentId());
        Satisfaction satisfaction = satisfactionOP.orElseThrow(SatisfactionNotFoundException::new);
        satisfaction.setScore(scoreDTO.getScore());
        satisfaction.setStatus(SatisfactionStatusEnum.FILLED);
        save(satisfaction);
        personalNotificationService.createScorefactionToConsultant(satisfaction);
        return satisfaction;
    }
    private float getAgentAvgScore(Satisfaction satisfaction) {
        Float avgScore = satisfactionRepository.getAgentScoreAvg(satisfaction.getAgentNo());
        BigDecimal bigDecimal = new BigDecimal(avgScore);
        return avgScore = bigDecimal.setScale(1,BigDecimal.ROUND_HALF_UP).floatValue();
    }
    public Satisfaction createSatisfaction(Appointment appointment) {
        boolean isexist = getByAppointmentId(appointment.getId()).isPresent();
        if(isexist) {
            throw new SatisfactionAlreadyExistException();
        }
        Satisfaction satisfaction = appointmentMapper.toSatisfaction(appointment);
        return createSatisfaction(satisfaction);
        return save(satisfaction);
    }
    public Satisfaction createSatisfaction(SatisfactionCustomerCreateDTO createDTO) {
        // todo : å°šæœªæ¨™è¨˜å·²è¯çµ¡çš„預約單不該可以新增滿意度評分
        // todo : éžè‡ªå·±çš„預約單不該可以進行評分
        Satisfaction satisfaction = satisfactionDTOMapper.toSatisfaction(createDTO);
        return createSatisfaction(satisfaction);
    }
//
//    public Satisfaction createSatisfaction(SatisfactionCustomerScoreDTO createDTO) {
//        Satisfaction satisfaction = satisfactionDTOMapper.toSatisfaction(createDTO);
//        return save(satisfaction);
//    }
    public List<SatisfactionDTO> getByAgentNo(String agentNo) {
        List<Satisfaction> satisfactionList = satisfactionRepository.findByAgentNo(agentNo);
@@ -93,4 +93,16 @@
    public Optional<Satisfaction> getByAppointmentId(Long appointmentId) {
        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 ->{
            satisfactionList.add(scorefaction(dto));
        });
        return satisfactionList;
    }
}
pamapi/src/main/java/com/pollex/pam/service/ScheduleTaskService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,163 @@
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);
            Optional<String> optionalPhone = Optional.ofNullable(consultant.getPhoneNumber()).filter(StringUtils::hasText);
            Optional<String> optionalEmail = Optional.ofNullable(consultant.getEmail()).filter(StringUtils::hasText);
            String emailContent = getAppointmentPendingNotifyEmailContent(pendingAppointmentsSum);
            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");
    }
    @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::isAppointmentExpiringNotifyNotOnLimit)
                .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(), getAppointmentExpiringNotifyUrl(appointment.getId())))
            );
            optionalEmail.ifPresent(email ->
                sendMsgService.sendMsgByEmail(email, NOT_CONTACTED_NOTIFY_SUBJECT, getAppointmentExpiringNotifyEmail(consultant.getName(), getAppointmentExpiringNotifyUrl(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 30 8 * * *")
    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 isAppointmentExpiringNotifyNotOnLimit(AppointmentCustomerView appointment) {
        int sendNotifyToCustomerRecordSum =
            appointmentExpiringNotifyRecordRepository.findAllByAppointmentId(appointment.getId()).size();
        return sendNotifyToCustomerRecordSum < Constants.SEND_EXPIRING_NOTIFY_LIMIT;
    }
    private String getAppointmentExpiringNotifyUrl(Long appointmentId) {
        return applicationProperties.getFrontEndDomain() + "?notContactAppointmentId=" + appointmentId;
    }
    private String getAppointmentPendingNotifyEmailContent(int sum) {
        Context context = new Context();
        context.setVariable("pendingAppointmentSum", sum);
        return springTemplateEngine.process("mail/appointmentPendingNotifyEmail", context);
    }
    private String getAppointmentExpiringNotifyEmail(String consultantName, String notifyUrl) {
        Context context = new Context();
        context.setVariable("consultantName", consultantName);
        context.setVariable("notifyUrl", notifyUrl);
        return springTemplateEngine.process("mail/appointmentExpiringNotifyEmail", context);
    }
}
pamapi/src/main/java/com/pollex/pam/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,8 +44,19 @@
    @Autowired
    SpringTemplateEngine springTemplateEngine;
    @Autowired
    Environment environment;
    @Autowired
    MailService mailService;
    public SendSMSResponse sendMsgBySMS(String toMobile, String content) throws SendSMSFailException {
        SMS smsProperties = applicationProperties.getSms();
        if(!smsProperties.isSendNotifyMsg()) {
//            return getMockSMSResponse();
            return null;
        }
        SendSMSRequest sendSMSRequest = new SendSMSRequest();
        sendSMSRequest.setpKey(UUID.randomUUID().toString());
@@ -76,14 +93,20 @@
        }
    }
    public String sendMsgByEmail(String from, String to, String subject, String content, boolean htmlFormat) throws SendEmailFailException{
        return sendMsgByEmail(from, to, subject, content, htmlFormat, Collections.emptyList(), Collections.emptyList());
//    private SendSMSResponse getMockSMSResponse() {
//        SendSMSResponse mock = new SendSMSResponse();
//        mock.set
//        return null;
//    }
    public String sendMsgByEmail(String to, String subject, String content, boolean htmlFormat) throws SendEmailFailException{
        return sendMsgByEmail(to, subject, content, htmlFormat, Collections.emptyList(), Collections.emptyList());
    }
    public String sendMsgByEmail(
        String fromAddress, String toAddress, String subject, String content, boolean htmlFormat, List<String> toCCAddress,
        List<String> attachments) throws SendEmailFailException
    {
    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);
@@ -98,22 +121,48 @@
    }
    public String sendMsgByEmail(SendMailRequest sendMailRequest) throws SendEmailFailException{
        final Email emailProperties = applicationProperties.getEmail();
        if(!emailProperties.isSendNotifyMsg()) {
            return null;
        }
        if(emailProperties.getMethod() == SendEmailMsgMethod.POLLEX_GMAIL) {
            return sendMsgByPollexGmail(sendMailRequest);
        }
        else if(emailProperties.getMethod() == SendEmailMsgMethod.PAM_EMAIL_SERVICE) {
            return sendMsgByPamEmailService(sendMailRequest);
        }
        return null;
    }
    private String sendMsgByPollexGmail(SendMailRequest sendMailRequest) {
        String subject = sendMailRequest.getSubject();
        String content = sendMailRequest.getContent();
        boolean isHtml = sendMailRequest.isHtmlFormat();
        sendMailRequest.getSendMailAddresses().forEach(receiver -> mailService.sendEmail(receiver, subject, content, false, isHtml));
        return null;
    }
    private String sendMsgByPamEmailService(SendMailRequest sendMailRequest) {
        final Email emailProperties = applicationProperties.getEmail();
        try {
            ResponseEntity<String> responseEntity =
                HttpRequestUtil.postWithJson( applicationProperties.getEmail().getUrl(), sendMailRequest, String.class);
                HttpRequestUtil.postWithJson(emailProperties.getUrl(), sendMailRequest, String.class);
            log.debug("responseEntity = {}", responseEntity);
            String rawResponseString = responseEntity.getBody();
            SendMailResponse sendMailResponse = new ObjectMapper().readValue(rawResponseString, SendMailResponse.class);
            log.debug("sendMailResponse = {}", sendMailResponse);
            if(sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) {
            if (sendMailResponse == null || sendMailResponse.getData() == null || !"ADDED".equalsIgnoreCase(sendMailResponse.getData().getMessageStatus())) {
                throw new SendEmailFailException("send email service return error msg! raw response string= " + rawResponseString);
            }
            return responseEntity.getBody();
        }
        catch (SendEmailFailException e) {
        } catch (SendEmailFailException e) {
            throw e;
        } catch (Exception e) {
            log.warn("send email fail by other reason", e);
pamapi/src/main/java/com/pollex/pam/service/dto/AbstractAppointmentProcessDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,37 @@
package com.pollex.pam.service.dto;
import com.pollex.pam.enums.ContactStatusEnum;
public abstract class AbstractAppointmentProcessDTO{
    private ContactStatusEnum contactStatus;
    private Long appointmentId;
//    private Long closedInfoId;
    public ContactStatusEnum getContactStatus() {
        return contactStatus;
    }
    public void setContactStatus(ContactStatusEnum contactStatus) {
        this.contactStatus = contactStatus;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
//    public Long getClosedInfoId() {
//        return closedInfoId;
//    }
//
//    public void setClosedInfoId(Long closedInfoId) {
//        this.closedInfoId = closedInfoId;
//    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentCloseDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,77 @@
package com.pollex.pam.service.dto;
import java.util.Date;
import com.pollex.pam.enums.ContactStatusEnum;
public class AppointmentCloseDTO{
//    private Long closedInfoId;
    private String policyholderIdentityId;
    private String planCode;
    private Date policyEntryDate;
    private String remark;
    private String closedReason;
    private String closedOtherReason;
    private ContactStatusEnum contactStatus;
    private Long appointmentId;
    public String getPolicyholderIdentityId() {
        return policyholderIdentityId;
    }
    public void setPolicyholderIdentityId(String policyholderIdentityId) {
        this.policyholderIdentityId = policyholderIdentityId;
    }
    public String getPlanCode() {
        return planCode;
    }
    public void setPlanCode(String planCode) {
        this.planCode = planCode;
    }
    public Date getPolicyEntryDate() {
        return policyEntryDate;
    }
    public void setPolicyEntryDate(Date policyEntryDate) {
        this.policyEntryDate = policyEntryDate;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public String getClosedReason() {
        return closedReason;
    }
    public void setClosedReason(String closedReason) {
        this.closedReason = closedReason;
    }
    public String getClosedOtherReason() {
        return closedOtherReason;
    }
    public void setClosedOtherReason(String closedOtherReason) {
        this.closedOtherReason = closedOtherReason;
    }
    public ContactStatusEnum getContactStatus() {
        return contactStatus;
    }
    public void setContactStatus(ContactStatusEnum contactStatus) {
        this.contactStatus = contactStatus;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
//    public Long getClosedInfoId() {
//        return closedInfoId;
//    }
//    public void setClosedInfoId(Long closedInfoId) {
//        this.closedInfoId = closedInfoId;
//    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentCustomerViewDTO.java
@@ -1,7 +1,12 @@
package com.pollex.pam.service.dto;
import java.time.Instant;
import java.util.List;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.domain.AppointmentMemo;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.enums.ContactStatusEnum;
public class AppointmentCustomerViewDTO {
@@ -26,6 +31,10 @@
    private Instant consultantReadTime;
    private Instant contactTime;
    private Float satisfactionScore;
    private List<AppointmentMemo> appointmentMemoList;
    private List<InterviewRecordDTO> interviewRecordDTOs;
    private List<AppointmentNoticeLog> appointmentNoticeLogs;
    private AppointmentClosedInfo appointmentClosedInfo;
    public Long getId() {
        return id;
@@ -147,4 +156,32 @@
    public void setSatisfactionScore(Float satisfactionScore) {
        this.satisfactionScore = satisfactionScore;
    }
    public List<AppointmentMemo> getAppointmentMemoList() {
        return appointmentMemoList;
    }
    public void setAppointmentMemoList(List<AppointmentMemo> appointmentMemoList) {
        this.appointmentMemoList = appointmentMemoList;
    }
    public List<InterviewRecordDTO> getInterviewRecordDTOs() {
        return interviewRecordDTOs;
    }
    public void setInterviewRecordDTOs(List<InterviewRecordDTO> interviewRecordDTOs) {
        this.interviewRecordDTOs = interviewRecordDTOs;
    }
    public List<AppointmentNoticeLog> getAppointmentNoticeLogs() {
        return appointmentNoticeLogs;
    }
    public void setAppointmentNoticeLogs(List<AppointmentNoticeLog> appointmentNoticeLogs) {
        this.appointmentNoticeLogs = appointmentNoticeLogs;
    }
    public AppointmentClosedInfo getAppointmentClosedInfo() {
        return appointmentClosedInfo;
    }
    public void setAppointmentClosedInfo(AppointmentClosedInfo appointmentClosedInfo) {
        this.appointmentClosedInfo = appointmentClosedInfo;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentMemoCreateDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,22 @@
package com.pollex.pam.service.dto;
public class AppointmentMemoCreateDTO {
    private String content;
    private Long appointmentId;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentMemoUpdateDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,27 @@
package com.pollex.pam.service.dto;
public class AppointmentMemoUpdateDTO{
    private Long id;
    private String content;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/AppointmentNoticeSendDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,53 @@
package com.pollex.pam.service.dto;
import java.util.Date;
public class AppointmentNoticeSendDTO {
    private String message;
    private Long appointmentId;
    private String email;
    private String phone;
//    private String noticeType;
    private Date interviewDate;
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
//    public String getNoticeType() {
//        return noticeType;
//    }
//    public void setNoticeType(String noticeType) {
//        this.noticeType = noticeType;
//    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public Date getInterviewDate() {
        return interviewDate;
    }
    public void setInterviewDate(Date interviewDate) {
        this.interviewDate = interviewDate;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/ClosedProcessDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,29 @@
package com.pollex.pam.service.dto;
public class ClosedProcessDTO extends AbstractAppointmentProcessDTO{
    private String remark;
    private String closedReason;
    private String closedOtherReason;
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public String getClosedReason() {
        return closedReason;
    }
    public void setClosedReason(String closedReason) {
        this.closedReason = closedReason;
    }
    public String getClosedOtherReason() {
        return closedOtherReason;
    }
    public void setClosedOtherReason(String closedOtherReason) {
        this.closedOtherReason = closedOtherReason;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantDTO.java
@@ -20,7 +20,6 @@
    private Instant updateTime;
    private String role;
    private String seniority;
    private Long latestAppointmentId;
    public boolean isNewConsultant() {
        if(updateTime != null){
@@ -104,13 +103,5 @@
    public void setNewConsultant(boolean newConsultant) {
        this.newConsultant = newConsultant;
    }
    public Long getLatestAppointmentId() {
        return latestAppointmentId;
    }
    public void setLatestAppointmentId(Long latestAppointmentId) {
        this.latestAppointmentId = latestAppointmentId;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantDetailDTO.java
@@ -3,6 +3,8 @@
import java.time.Instant;
import java.util.List;
import com.pollex.pam.enums.GenderEnum;
public class ConsultantDetailDTO {
    private String name;
@@ -20,8 +22,11 @@
    private Number evaluation;
    private List<String> expertise;
    private String concept;
    private List<String> experiences;
    private String experiences;
    private String awards;
    private GenderEnum gender;
    private String communicationStyle;
    private String email;
    public String getName() {
        return name;
@@ -143,11 +148,11 @@
        this.concept = concept;
    }
    public List<String> getExperiences() {
    public String getExperiences() {
        return experiences;
    }
    public void setExperiences(List<String> experiences) {
    public void setExperiences(String experiences) {
        this.experiences = experiences;
    }
@@ -158,4 +163,31 @@
    public void setAwards(String awards) {
        this.awards = awards;
    }
    public GenderEnum getGender() {
        return gender;
    }
    public void setGender(GenderEnum gender) {
        this.gender = gender;
    }
    public String getCommunicationStyle() {
        return communicationStyle;
    }
    public void setCommunicationStyle(String communicationStyle) {
        this.communicationStyle = communicationStyle;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/ConsultantEditDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,139 @@
package com.pollex.pam.service.dto;
import java.util.List;
import com.pollex.pam.enums.GenderEnum;
public class ConsultantEditDTO {
//    private Long id;
    private String name;
    private List<String> expertise;
    private String title;
    private String role;
    private String serveArea;
    private GenderEnum gender;
    private String phoneNumber;
    private String companyAddress;
    private Long seniorityYear;
    private Long seniorityMonth;
    private String concept;
    private String experiences;
    private String awards;
    private String communicationStyle;
    private String photoBase64;
    private String photoFileName;
    private String agentNo;
//    public Long getId() {
//        return id;
//    }
//    public void setId(Long id) {
//        this.id = id;
//    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<String> getExpertise() {
        return expertise;
    }
    public void setExpertise(List<String> expertise) {
        this.expertise = expertise;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getRole() {
        return role;
    }
    public void setRole(String role) {
        this.role = role;
    }
    public String getServeArea() {
        return serveArea;
    }
    public void setServeArea(String serveArea) {
        this.serveArea = serveArea;
    }
    public GenderEnum getGender() {
        return gender;
    }
    public void setGender(GenderEnum gender) {
        this.gender = gender;
    }
    public String getPhoneNumber() {
        return phoneNumber;
    }
    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
    public String getCompanyAddress() {
        return companyAddress;
    }
    public void setCompanyAddress(String companyAddress) {
        this.companyAddress = companyAddress;
    }
    public Long getSeniorityYear() {
        return seniorityYear;
    }
    public void setSeniorityYear(Long seniorityYear) {
        this.seniorityYear = seniorityYear;
    }
    public Long getSeniorityMonth() {
        return seniorityMonth;
    }
    public void setSeniorityMonth(Long seniorityMonth) {
        this.seniorityMonth = seniorityMonth;
    }
    public String getConcept() {
        return concept;
    }
    public void setConcept(String concept) {
        this.concept = concept;
    }
    public String getExperiences() {
        return experiences;
    }
    public void setExperiences(String experiences) {
        this.experiences = experiences;
    }
    public String getAwards() {
        return awards;
    }
    public void setAwards(String awards) {
        this.awards = awards;
    }
    public String getCommunicationStyle() {
        return communicationStyle;
    }
    public void setCommunicationStyle(String communicationStyle) {
        this.communicationStyle = communicationStyle;
    }
    public String getPhotoBase64() {
        return photoBase64;
    }
    public void setPhotoBase64(String photoBase64) {
        this.photoBase64 = photoBase64;
    }
    public String getPhotoFileName() {
        return "consultant_"+this.getAgentNo()+".jpg";
    }
    public String getAgentNo() {
        return agentNo;
    }
    public void setAgentNo(String agentNo) {
        this.agentNo = agentNo;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/DoneProcessDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,39 @@
package com.pollex.pam.service.dto;
import java.util.Date;
public class DoneProcessDTO extends AbstractAppointmentProcessDTO{
    private String policyholderIdentityId;
    private String planCode;
    private Date policyEntryDate;
    private String remark;
    public String getPolicyholderIdentityId() {
        return policyholderIdentityId;
    }
    public void setPolicyholderIdentityId(String policyholderIdentityId) {
        this.policyholderIdentityId = policyholderIdentityId;
    }
    public String getPlanCode() {
        return planCode;
    }
    public void setPlanCode(String planCode) {
        this.planCode = planCode;
    }
    public Date getPolicyEntryDate() {
        return policyEntryDate;
    }
    public void setPolicyEntryDate(Date policyEntryDate) {
        this.policyEntryDate = policyEntryDate;
    }
    public String getRemark() {
        return remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/InterviewRecordDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,69 @@
package com.pollex.pam.service.dto;
import java.time.Instant;
import java.util.Date;
public class InterviewRecordDTO {
    private Long id;
    private String content;
    private Instant createdDate;
    private Instant lastModifiedDate;
    private String createdBy;
    private String lastModifiedBy;
    private Date interviewDate;
    private Long appointmentId;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public Instant getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }
    public Instant getLastModifiedDate() {
        return lastModifiedDate;
    }
    public void setLastModifiedDate(Instant lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
    public String getCreatedBy() {
        return createdBy;
    }
    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }
    public String getLastModifiedBy() {
        return lastModifiedBy;
    }
    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }
    public Date getInterviewDate() {
        return interviewDate;
    }
    public void setInterviewDate(Date interviewDate) {
        this.interviewDate = interviewDate;
    }
    public Long getAppointmentId() {
        return appointmentId;
    }
    public void setAppointmentId(Long appointmentId) {
        this.appointmentId = appointmentId;
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/SatisfactionCustomerScoreDTO.java
File was renamed from pamapi/src/main/java/com/pollex/pam/service/dto/SatisfactionCustomerCreateDTO.java
@@ -1,6 +1,6 @@
package com.pollex.pam.service.dto;
public class SatisfactionCustomerCreateDTO {
public class SatisfactionCustomerScoreDTO {
    
    private Long appointmentId;
    private Float score;
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentCustomerViewMapper.java
@@ -3,19 +3,49 @@
import static java.util.stream.Collectors.toList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.domain.AppointmentClosedInfo;
import com.pollex.pam.domain.AppointmentCustomerView;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.repository.AppointmentClosedInfoRepository;
import com.pollex.pam.service.AppointmentClosedInfoService;
import com.pollex.pam.service.AppointmentNoticeLogService;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.dto.AppointmentCustomerViewDTO;
@Service
public class AppointmentCustomerViewMapper {
    @Autowired
    AppointmentService appointmentService;
    @Autowired
    AppointmentNoticeLogService appointmentNoticeLogService;
    @Autowired
    AppointmentClosedInfoRepository appointmentClosedInfoRepository;
    @Transactional
    public AppointmentCustomerViewDTO toAppointmentCustomerViewDTO(AppointmentCustomerView source) {
        AppointmentCustomerViewDTO target = new AppointmentCustomerViewDTO();
        BeanUtils.copyProperties(source, target);
        target.setAppointmentMemoList(source.getAppointmentMemoList());
        appointmentService.setInterviewRecordDTO(target);
        List<AppointmentNoticeLog> noticeLogs = appointmentNoticeLogService.findByAppointmentId(source.getId());
        target.setAppointmentNoticeLogs(noticeLogs);
        Optional<AppointmentClosedInfo> appointmentClosedInfoOP = appointmentClosedInfoRepository
                .findByAppointmentId(source.getId());
        if(appointmentClosedInfoOP.isPresent()) {
            target.setAppointmentClosedInfo(appointmentClosedInfoOP.get());
        }
        appointmentService.setSatisfactionScore(target, source.getId());
        return target;
    }
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentMapper.java
@@ -13,7 +13,7 @@
import com.pollex.pam.enums.SatisfactionStatusEnum;
import com.pollex.pam.repository.AppointmentRepository;
import com.pollex.pam.service.dto.AppointmentDTO;
import com.pollex.pam.service.dto.SatisfactionCustomerCreateDTO;
import com.pollex.pam.service.dto.SatisfactionCustomerScoreDTO;
@Service
public class AppointmentMapper {
@@ -37,6 +37,7 @@
        target.setAppointmentId(appointment.getId());
        target.setAgentNo(appointment.getAgentNo());
        target.setCustomerId(appointment.getCustomerId());
        target.setStatus(SatisfactionStatusEnum.UNFILLED);
        return target;
    }
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentMemoMapper.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,23 @@
package com.pollex.pam.service.mapper;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.AppointmentMemo;
import com.pollex.pam.service.dto.AppointmentMemoCreateDTO;
import com.pollex.pam.service.dto.AppointmentMemoUpdateDTO;
@Service
public class AppointmentMemoMapper {
    public AppointmentMemo toAppointmentMemo(AppointmentMemoCreateDTO source) {
        AppointmentMemo target = new AppointmentMemo();
        BeanUtils.copyProperties(source, target);
        return target;
    }
    public AppointmentMemo copyToAppointmentMemo(AppointmentMemoUpdateDTO source, AppointmentMemo target) {
        BeanUtils.copyProperties(source, target);
        return target;
    }
}
pamapi/src/main/java/com/pollex/pam/service/mapper/AppointmentNoticeSendMapper.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,21 @@
package com.pollex.pam.service.mapper;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.service.dto.AppointmentNoticeSendDTO;
@Service
public class AppointmentNoticeSendMapper {
    public AppointmentNoticeLog toAppointmentNoticeLog(AppointmentNoticeSendDTO source) {
        AppointmentNoticeLog target = new AppointmentNoticeLog();
        BeanUtils.copyProperties(source, target);
//        target.setAppointmentId(source.getAppointmentId());
        target.setContent(source.getMessage());
//        target.setEmail(source.getEmail());
//        target.setPhone(source.getPhone());
        return target;
    }
}
pamapi/src/main/java/com/pollex/pam/service/mapper/ConsultantDTOMapper.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,30 @@
package com.pollex.pam.service.mapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.service.dto.ConsultantEditDTO;
import com.pollex.pam.service.util.FileUtil;
import com.pollex.pam.service.util.StringUtils;
@Service
public class ConsultantDTOMapper {
    @Autowired
    ApplicationProperties applicationProperty;
    public void copyToConsultant(ConsultantEditDTO source, Consultant target) {
        BeanUtils.copyProperties(source, target);
        target.setPhotoPath(FileUtil.toPath(source.getPhotoFileName(), applicationProperty.getFileFolderPath()));
        String expertise = StringUtils.convertToString(source.getExpertise(), ",");
        target.setExpertise(expertise);
        target.setAward(source.getAwards());
        target.setExperience(source.getExperiences());
    }
}
pamapi/src/main/java/com/pollex/pam/service/mapper/ConsultantMapper.java
@@ -5,6 +5,7 @@
import com.pollex.pam.service.dto.ConsultantDetailDTO;
import com.pollex.pam.service.dto.CustomerFavoriteConsultantDTO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.*;
@@ -43,13 +44,14 @@
        consultantDTO.setContactStatus(null);
        consultantDTO.setUpdateTime(null);
        consultantDTO.setLatestAppointmentId(null);
//        consultantDTO.setLatestAppointmentId(null);
        return consultantDTO;
    }
    public ConsultantDetailDTO toDetailDto(Consultant source) {
        ConsultantDetailDTO consultantDetailDTO = new ConsultantDetailDTO();
        BeanUtils.copyProperties(source, consultantDetailDTO);
        consultantDetailDTO.setName(source.getName());
        consultantDetailDTO.setAgentNo(source.getAgentNo());
        consultantDetailDTO.setRole(source.getRole());
@@ -63,7 +65,7 @@
        consultantDetailDTO.setAwards(source.getAward());
        consultantDetailDTO.setImg(source.getPhotoPath());
        consultantDetailDTO.setExpertise(splitStringWithChar(source.getExpertise()));
        consultantDetailDTO.setExperiences(splitStringWithChar(source.getExperience()));
        consultantDetailDTO.setExperiences(source.getExperience());
        // todo æ±ºå®šåŒ¹é…ç¨‹åº¦
        consultantDetailDTO.setSuitability(50);
pamapi/src/main/java/com/pollex/pam/service/mapper/InterviewRecordMapper.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,37 @@
package com.pollex.pam.service.mapper;
import static java.util.stream.Collectors.toList;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.service.dto.InterviewRecordDTO;
@Service
public class InterviewRecordMapper {
    public InterviewRecord toInterviewRecord(InterviewRecordDTO source) {
        InterviewRecord target = new InterviewRecord();
        BeanUtils.copyProperties(source, target);
        return target;
    }
    public void copyToInterviewRecord(InterviewRecordDTO source, InterviewRecord target) {
        BeanUtils.copyProperties(source, target);
    }
    public List<InterviewRecordDTO> toInterviewRecordDTO(List<InterviewRecord> records) {
        return records.stream()
                .map(s-> toInterviewRecordDTO(s))
                .collect(toList());
    }
    public InterviewRecordDTO toInterviewRecordDTO(InterviewRecord source) {
        InterviewRecordDTO target = new InterviewRecordDTO();
        BeanUtils.copyProperties(source, target);
        return target;
    }
}
pamapi/src/main/java/com/pollex/pam/service/mapper/SatisfactionDTOMapper.java
@@ -5,7 +5,7 @@
import com.pollex.pam.domain.Satisfaction;
import com.pollex.pam.enums.SatisfactionStatusEnum;
import com.pollex.pam.service.dto.SatisfactionCustomerCreateDTO;
import com.pollex.pam.service.dto.SatisfactionCustomerScoreDTO;
@Service
public class SatisfactionDTOMapper {
@@ -13,7 +13,7 @@
    @Autowired
    AppointmentMapper appointmentMapper;
    
    public Satisfaction toSatisfaction(SatisfactionCustomerCreateDTO source) {
    public Satisfaction toSatisfaction(SatisfactionCustomerScoreDTO source) {
        Satisfaction satisfaction = appointmentMapper.toSatisfaction(source.getAppointmentId());
        satisfaction.setScore(source.getScore());
        if(satisfaction.getScore()!=null) {
pamapi/src/main/java/com/pollex/pam/service/util/FileUtil.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,69 @@
package com.pollex.pam.service.util;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FileUtil {
    private static final Logger log = LoggerFactory.getLogger(FileUtil.class);
    public static File base64ToFile(String base64, String fileName, String folderPath) {
        if(base64 == null){
            return new File(folderPath);
        }
        File  dir=new File(folderPath);
        dir.setWritable(true);
        dir.setReadable(true);
        if (!dir.exists() && !dir.isDirectory()) {
                dir.mkdirs();
        }
        return base64ToFile(base64, toPath(fileName, folderPath));
    }
    public static File base64ToFile(String base64, String path) {
        if(base64 == null){
            return new File(path);
        }
        BufferedOutputStream bos = null;
        try {
            byte[] bytes = Base64.getDecoder().decode(base64);
            File file=new File(path);
            file.setWritable(true);
            file.setReadable(true);
            bos = new BufferedOutputStream(new FileOutputStream(file));
            bos.write(bytes);
            return file;
        } catch (Exception e) {
            log.error("upload file base64 to file error",e);
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    log.error("upload file base64 to file error",e);
                }
            }
        }
        return null;
    }
    public static boolean isFileExisted(String fileName, String folderPath) {
        return new File(toPath(fileName, folderPath)).isFile();
    }
    public static String toPath(String fileName, String folderPath) {
        return folderPath + "/" + fileName;
    }
}
pamapi/src/main/java/com/pollex/pam/service/util/StringUtils.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,18 @@
package com.pollex.pam.service.util;
import java.util.List;
public class StringUtils {
    public static String convertToString(List<String> source
            , String separator) {
        StringBuilder result = new StringBuilder();
        for (String sourceString : source) {
            result.append(sourceString+separator);
        }
        result.deleteCharAt(result.length()-1);
        return result.toString();
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentMemoResource.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,45 @@
package com.pollex.pam.web.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pollex.pam.domain.AppointmentMemo;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.AppointmentMemoService;
import com.pollex.pam.service.dto.AppointmentMemoCreateDTO;
import com.pollex.pam.service.dto.AppointmentMemoUpdateDTO;
@RestController
@RequestMapping("/api/appointment/memo")
public class AppointmentMemoResource {
    @Autowired
    AppointmentMemoService appointmentMemoService;
    @PostMapping("/create")
    public ResponseEntity<AppointmentMemo> createMemo(@RequestBody AppointmentMemoCreateDTO memoDTO) {
        appointmentMemoService.checkPermission(memoDTO.getAppointmentId());
        AppointmentMemo memo = appointmentMemoService.create(memoDTO);
        return new ResponseEntity<>(memo, HttpStatus.OK);
    }
    @PostMapping("/update")
    public ResponseEntity<AppointmentMemo> updateMemo(@RequestBody AppointmentMemoUpdateDTO memoDTO) {
        AppointmentMemo memo = appointmentMemoService.update(memoDTO);
        return new ResponseEntity<>(memo, HttpStatus.OK);
    }
    @DeleteMapping("/{memoId}")
    public void deleteMemo(@PathVariable Long memoId) {
        appointmentMemoService.delete(memoId);
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/AppointmentResource.java
@@ -1,16 +1,24 @@
package com.pollex.pam.web.rest;
import com.pollex.pam.appointment.process.AppointmentProcess;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.SendMsgService;
import com.pollex.pam.service.dto.AppointmentUpdateDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.PersonalNotificationService;
import com.pollex.pam.service.SatisfactionService;
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")
@@ -25,9 +33,16 @@
    @Autowired
    SendMsgService sendMsgService;
    @Autowired
    AppointmentProcess abstractAppointmentProcess;
    @Autowired
    PersonalNotificationService personalNotificationService;
    @PutMapping("")
    public ResponseEntity<Void> updateAppointment(@RequestBody AppointmentUpdateDTO appointment) {
        appointmentService.updateAppointment(appointment);
    public ResponseEntity<Void> updateAppointment(@RequestBody AppointmentUpdateDTO dto) {
        Appointment appointment = appointmentService.updateAppointment(dto);
        personalNotificationService.createUpdateAppointmentToConsultant(appointment);
        return ResponseEntity.noContent().build();
    }
@@ -44,9 +59,10 @@
    }
    @PostMapping("/markAsContacted/{appointmentId}")
    public void markAsContacted(@PathVariable Long appointmentId) {
    public AppointmentCustomerViewDTO markAsContacted(@PathVariable Long appointmentId) {
        appointmentService.markAsContacted(appointmentId);
    }
        return appointmentService.getAppointmentDetail(appointmentId);
    }
    @GetMapping("/getDetail/{appointmentId}")
    public AppointmentCustomerViewDTO getAppointmentDetail(@PathVariable Long appointmentId) {
@@ -58,4 +74,42 @@
        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);
        return new ResponseEntity<>(customerNewestExpiringAppointment, HttpStatus.OK);
    }
    @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);
//            abstractAppointmentProcess.editClosedInfo(dto);
//        }else if(closeDTO.getContactStatus() == ContactStatusEnum.CLOSED){
//            ClosedProcessDTO dto = new ClosedProcessDTO();
//            BeanUtils.copyProperties(closeDTO, dto);
//            abstractAppointmentProcess.editClosedInfo(dto);
//        }else {
//            return ResponseEntity.notFound().build();
//        }
//
//        return ResponseEntity.noContent().build();
//    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/ConsultantResource.java
@@ -1,5 +1,9 @@
package com.pollex.pam.web.rest;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.ConsultantService;
import com.pollex.pam.service.dto.*;
import org.apache.commons.compress.utils.IOUtils;
@@ -11,9 +15,12 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
@@ -22,6 +29,9 @@
    @Autowired
    ConsultantService consultantService;
    @Autowired
    AppointmentService appointmentService;
    @GetMapping("/favorite")
    public ResponseEntity<List<CustomerFavoriteConsultantDTO>> getMyConsultantList() {
@@ -77,15 +87,23 @@
        return consultantService.getMyAppointment();
    }
    @GetMapping(value = "/avatar/{fileName}", produces = MediaType.IMAGE_JPEG_VALUE)
    public ResponseEntity<byte[]> getAvatarImage(@PathVariable String fileName) throws IOException {
        try {
            Resource resource = new ClassPathResource("static/consultant/" + fileName);
            InputStream in = resource.getInputStream();
            return new ResponseEntity<>(IOUtils.toByteArray(in), HttpStatus.OK);
        } catch (FileNotFoundException e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    @GetMapping(value = "/avatar/{agentNo}", produces = MediaType.IMAGE_JPEG_VALUE)
    public ResponseEntity<byte[]> getAvatarImage(@PathVariable String agentNo) throws IOException {
//        try {
//            Resource resource = new ClassPathResource("static/consultant/" + fileName);
//            InputStream in = resource.getInputStream();
//            return new ResponseEntity<>(IOUtils.toByteArray(in), HttpStatus.OK);
//        } catch (FileNotFoundException e) {
//            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
//        }
        InputStream in = consultantService.getAvatarImage(agentNo);
        if(in!=null) {
            return new ResponseEntity<>(IOUtils.toByteArray(in), HttpStatus.OK);
        }else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
    @PostMapping("/record/allAppointmentsView")
@@ -93,4 +111,23 @@
        consultantService.recordAllAppointmentsView();
        return ResponseEntity.noContent().build();
    }
    @PostMapping("/edit")
    public ResponseEntity<Consultant> editConsultant(@RequestBody ConsultantEditDTO editDTO) {
        if(!editDTO.getAgentNo().equals(SecurityUtils.getAgentNo())) {
            throw new IllegalAccessError();
        }
        Consultant editResult = consultantService.editConsultant(editDTO);
        return new ResponseEntity<>(editResult, HttpStatus.OK);
    }
    @PostMapping("/sendSatisfactionToClient/{appointmentId}")
    public ResponseEntity<Void> sendSatisfactionToClient(@PathVariable Long appointmentId) {
        Appointment appointment = appointmentService.findById(appointmentId);
        if(!appointment.getAgentNo().equals(SecurityUtils.getAgentNo())) {
            throw new IllegalAccessError();
        }
        consultantService.sendSatisfactionToClient(appointment);
        return ResponseEntity.noContent().build();
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/InterviewRecordResource.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,36 @@
package com.pollex.pam.web.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pollex.pam.domain.InterviewRecord;
import com.pollex.pam.service.InterviewRecordService;
import com.pollex.pam.service.dto.InterviewRecordDTO;
@RestController
@RequestMapping("/api/interview_record")
public class InterviewRecordResource {
    @Autowired
    InterviewRecordService interviewRecordService;
    @PostMapping("/create")
    public InterviewRecord create(@RequestBody InterviewRecordDTO dto) {
        return interviewRecordService.create(dto);
    }
    @PostMapping("/update")
    public InterviewRecord update(@RequestBody InterviewRecordDTO dto) {
        return interviewRecordService.update(dto);
    }
    @DeleteMapping("/{interviewRecordId}")
    public void delete(@PathVariable Long interviewRecordId) {
        interviewRecordService.delete(interviewRecordId);
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/NoticeResource.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,35 @@
package com.pollex.pam.web.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pollex.pam.domain.Appointment;
import com.pollex.pam.domain.AppointmentNoticeLog;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.AppointmentService;
import com.pollex.pam.service.NoticeService;
import com.pollex.pam.service.dto.AppointmentNoticeSendDTO;
@RestController
@RequestMapping("/api/notice")
public class NoticeResource {
    @Autowired
    NoticeService noticeService;
    @Autowired
    AppointmentService appointmentService;
    @PostMapping("/send")
    public void sendNotice(@RequestBody AppointmentNoticeSendDTO dto) {
        Appointment appointment = appointmentService.findById(dto.getAppointmentId());
        if(!appointment.getAgentNo().equals(SecurityUtils.getAgentNo())) {
            throw new IllegalAccessError("The user do not have access permission");
        }
        noticeService.sendNotice(dto);
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/PersonalNotificationResource.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,55 @@
package com.pollex.pam.web.rest;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.pollex.pam.domain.Consultant;
import com.pollex.pam.domain.PersonalNotification;
import com.pollex.pam.enums.NotificationTypeEnum;
import com.pollex.pam.enums.PersonalNotificationRoleEnum;
import com.pollex.pam.repository.PersonalNotificationRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.ConsultantService;
import com.pollex.pam.service.PersonalNotificationService;
@RestController
@RequestMapping("/api/personal_notification")
public class PersonalNotificationResource {
    @Autowired
    PersonalNotificationService personalNotificationService;
    @Autowired
    PersonalNotificationRepository personalNotificationRepository;
    @Autowired
    ConsultantService consultantService;
    @GetMapping("/getMyPersonalNotification")
    public ResponseEntity<List<PersonalNotification>> getMyPersonalNotification() {
        List<PersonalNotification> personalNotificationList = new ArrayList<>();
        if(StringUtils.hasText(SecurityUtils.getAgentNo())) {
            Consultant consultant = consultantService.findByAgentNo(SecurityUtils.getAgentNo());
            personalNotificationList = personalNotificationService.getMyPersonalNotification(consultant.getId(), PersonalNotificationRoleEnum.CONSULTANT);
        }else if(SecurityUtils.getCustomerDBId()!=null){
            personalNotificationList = personalNotificationService.getMyPersonalNotification(SecurityUtils.getCustomerDBId(), PersonalNotificationRoleEnum.CUSTOMER);
        }
        return new ResponseEntity<>(personalNotificationList, HttpStatus.OK);
    }
    @PostMapping("/readAllMyNotification")
    public void readAll() {
        personalNotificationService.readAllMyNotification();
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/SatisfactionResource.java
@@ -17,7 +17,7 @@
import com.pollex.pam.domain.Satisfaction;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.SatisfactionService;
import com.pollex.pam.service.dto.SatisfactionCustomerCreateDTO;
import com.pollex.pam.service.dto.SatisfactionCustomerScoreDTO;
import com.pollex.pam.service.dto.SatisfactionDTO;
import com.pollex.pam.service.dto.SatisfactionUpdateDTO;
@@ -31,9 +31,14 @@
    @Autowired
    SatisfactionService satisfactionService;
    
    @PostMapping("/create")
    public Satisfaction createSatisfaction(@RequestBody SatisfactionCustomerCreateDTO createDTO) {
        return satisfactionService.createSatisfaction(createDTO);
    @PostMapping("/score")
    public Satisfaction scorefaction(@RequestBody SatisfactionCustomerScoreDTO scoreDTO) {
        return satisfactionService.scorefaction(scoreDTO);
    }
    @PostMapping("/score/all")
    public List<Satisfaction> scoreAllfaction(@RequestBody List<SatisfactionCustomerScoreDTO> scoreDTO) {
        return satisfactionService.scoreAllfaction(scoreDTO);
    }
    
    @GetMapping("/getMySatisfaction")
pamapi/src/main/java/com/pollex/pam/web/rest/TestSendMsgResource.java
@@ -30,31 +30,31 @@
        return ResponseEntity.ok(sendMsgService.sendMsgBySMS(toMobile, content));
    }
    @GetMapping("/byEmail")
    public ResponseEntity<String> byEmail(
        @RequestParam String from,
        @RequestParam String to,
        @RequestParam String subject,
        @RequestParam String content,
        @RequestParam boolean htmlFormat
    ) {
        return ResponseEntity.ok(sendMsgService.sendMsgByEmail(from, to, subject, content, htmlFormat));
    }
//    @GetMapping("/byEmail")
//    public ResponseEntity<String> byEmail(
//        @RequestParam String from,
//        @RequestParam String to,
//        @RequestParam String subject,
//        @RequestParam String content,
//        @RequestParam boolean htmlFormat
//    ) {
//        return ResponseEntity.ok(sendMsgService.sendMsgByEmail(from, to, subject, content, htmlFormat));
//    }
//
//    @GetMapping("/byHtmlEmail")
//    public ResponseEntity<String> byHtmlEmail(
//        @RequestParam String from,
//        @RequestParam String to
//    ) {
//        return ResponseEntity.ok(testSendMsgByHtmlTemplateEmail(from, to));
//    }
    @GetMapping("/byHtmlEmail")
    public ResponseEntity<String> byHtmlEmail(
        @RequestParam String from,
        @RequestParam String to
    ) {
        return ResponseEntity.ok(testSendMsgByHtmlTemplateEmail(from, to));
    }
    private String testSendMsgByHtmlTemplateEmail(String from, String to) {
        Context context = new Context();
        context.setVariable("content", "親愛的顧問您好,您有一筆來自保誠媒合平台的新預約單\n");
        context.setVariable("urlHint", appointmentService.getAppointmentDetailUrl(0L));
        String content = springTemplateEngine.process("mail/appointmentNotifyEmail", context);
        return sendMsgService.sendMsgByEmail(from, to, NOTIFY_EMAIL_SUBJECT, content, true);
    }
//    private String testSendMsgByHtmlTemplateEmail(String from, String to) {
//        Context context = new Context();
//        context.setVariable("content", "親愛的顧問您好,您有一筆來自保誠媒合平台的新預約單\n");
//        context.setVariable("urlHint", appointmentService.getAppointmentDetailUrl(0L));
//
//        String content = springTemplateEngine.process("mail/appointmentNotifyEmail", context);
//        return sendMsgService.sendMsgByEmail(from, to, NOTIFY_EMAIL_SUBJECT, content, true);
//    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/AppointmentClosedInfoNotFoundException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
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 = "Appointment close info not found")
public class AppointmentClosedInfoNotFoundException extends RuntimeException{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/AppointmentMemoNotFoundException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
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 = "Appointment memo not found")
public class AppointmentMemoNotFoundException extends RuntimeException{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/InterviewRecordNotFoundException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,14 @@
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 = "InterviewRecord not found")
public class InterviewRecordNotFoundException extends RuntimeException{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/NotFoundExpiringAppointmentException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,8 @@
package com.pollex.pam.web.rest.errors;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "not found any expiring appointment")
public class NotFoundExpiringAppointmentException extends RuntimeException {
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/SatisfactionAlreadyExistException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
package com.pollex.pam.web.rest.errors;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Satisfaction already exist")
public class SatisfactionAlreadyExistException extends RuntimeException{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
}
pamapi/src/main/java/com/pollex/pam/web/rest/errors/SatisfactionNotFoundException.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
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 = "Satisfaction not found")
public class SatisfactionNotFoundException extends RuntimeException{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
}
pamapi/src/main/resources/config/application-dev.yml
@@ -32,8 +32,8 @@
      indent-output: true
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:postgresql://dev.pollex.com.tw:5433/pam
    #url: jdbc:postgresql://localhost:5432/omo?currentSchema=omo
    url: jdbc:postgresql://dev.pollex.com.tw:5433/pam_p2
    #url: jdbc:postgresql://localhost:5432/omo?currentSchema=public
    username: pamadmin
    password: pamadmin
    hikari:
@@ -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,14 +126,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: http://localhost:3000
  send-notify-msg: false
  sms:
    send-notify-msg: false
    url: https://localhost:8081/testSMS
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: false
    url: https://localhost:8081/testEmail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'POLLEX_GMAIL'
  file-folder-path: C://pam_file
pamapi/src/main/resources/config/application-pollex.yml
@@ -43,10 +43,16 @@
    # Remove 'faker' if you do not want the sample data to be loaded automatically
    contexts: pollex, faker
  mail:
    host: localhost
    port: 25
    username:
    password:
    host: smtp.gmail.com
    port: 587
    username: pollex.testing@gmail.com
    password: ilismmmhtscppxft
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  messages:
    cache-duration: PT1S # 1 second, see the ISO 8601 standard
  thymeleaf:
@@ -118,14 +124,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: http://dev.pollex.com.tw:5566/pam
  send-notify-msg: false
  sms:
    send-notify-msg: false
    url: https://localhost:8081/testSMS
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://localhost:8081/testEmail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'POLLEX_GMAIL'
  file-folder-path: C://pam_file
pamapi/src/main/resources/config/application-prod.yml
@@ -131,4 +131,26 @@
# https://www.jhipster.tech/common-application-properties/
# ===================================================================
# application:
application:
  mock-login: false
  otp-web-service-url: https://vtwlifeopensysuat.pru.intranet.asia/pcalife-otp/ws/otpWebService?wsdl
  otp-web-service-password: es20!%Pass
  otp-web-service-system-type: epos
  e-service-login-url: https://eserviceuat.pcalife.com.tw/sso/chatbotValidate
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: https://vtwlifeopensysuat.pru.intranet.asia/pam
  sms:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'PAM_EMAIL_SERVICE'
  file-folder-path: /sfs_omo/vtwlifewpsfs01/SensitiveData4AP$/OMO
pamapi/src/main/resources/config/application-sit.yml
@@ -118,14 +118,17 @@
  e-service-login-func: ValidateUsrLogin
  e-service-login-sys: epos
  front-end-domain: https://vtwlifeopensyssit.pru.intranet.asia/pam
  send-notify-msg: true
  sms:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/MesgQueueMgmnt/rest/smsSendMsgResource
    source-code: ePos
    sender: POS
    sms-type: '0017'
    subject: '媒合平台通知'
  email:
    send-notify-msg: true
    url: https://vtwlifeopensysuat.pru.intranet.asia/tsgw/mq/mqSendMail
    function-id: epos
    sender-email: noreply@pcalife.com.tw
    method: 'PAM_EMAIL_SERVICE'
  file-folder-path: /sfs_omo/vtwlifewuftp66/sensitivedata4ap$/OMOSIT
pamapi/src/main/resources/config/application-uat.yml
@@ -118,14 +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
pamapi/src/main/resources/i18n/messages.properties
@@ -19,3 +19,12 @@
email.reset.greeting=Dear {0}
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
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/i18n/messages_zh_TW.properties
@@ -19,3 +19,13 @@
email.reset.greeting=\u89AA\u611B\u7684 {0}
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
email.write.satisfaction.content={0}\u9867\u554F\u8ACB\u60A8\u586B\u5BEB\u4FDD\u8AA0\u5A92\u5408\u5E73\u53F0\u7684\u6EFF\u610F\u5EA6\u8A55\u6BD4{1}
# appointment pending notify email
email.write.appointment.pending.content=\u60a8\u6709{0}\u5247\u9810\u7d04\u55ae\u672a\u9032\u884c\u806f\u7e6b\uff0c\u8acb\u76e1\u901f\u8655\u7406
# appointment expiring notify email
email.write.appointment.expiring.content=\u5f88\u62b1\u6b49\uff01\u60a8\u9810\u7d04{0}\u9867\u554f\u6b63\u5fd9\u788c\u4e2d\uff0c\u8acb\u60a8\u53d6\u6d88\u9810\u7d04\u4e26\u6539\u9078\u5176\u4ed6\u9867\u554f\uff0c\u8acb\u9ede\u64ca\u7db2\u5740\uff1a{1}
pamapi/src/main/resources/static/consultant/consultant_A183619275.jpg

pamapi/src/main/resources/static/consultant/consultant_AG0101234567.jpg

pamapi/src/main/resources/static/consultant/consultant_AG0109051204.jpg

pamapi/src/main/resources/static/consultant/consultant_AGAM11249699.jpg

pamapi/src/main/resources/static/consultant/consultant_B282677963.jpg

pamapi/src/main/resources/static/consultant/consultant_D265260662.jpg

pamapi/src/main/resources/static/consultant/consultant_J149388015.jpg

pamapi/src/main/resources/static/consultant/consultant_R221444250.jpg

pamapi/src/main/resources/static/consultant/consultant_X147309614.jpg

pamapi/src/main/resources/static/consultant/consultant_Z152717443.jpg

pamapi/src/main/resources/templates/mail/appointmentExpiringNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.expiring.content(${consultantName}, ${notifyUrl})}">很抱歉!您預約xxx顧問正忙碌中 , è«‹æ‚¨å–消預約並改選其他顧問</p>
</body>
</html>
pamapi/src/main/resources/templates/mail/appointmentPendingNotifyEmail.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
  <title>預約單未處理通知</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="#{email.write.appointment.pending.content(${pendingAppointmentSum})}">您有x則預約單未進行聯繫,請盡速處理</p>
</body>
</html>
pamapi/src/main/resources/templates/mail/writeSatisfactionNotice.html
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,10 @@
<!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.satisfaction.content(${consultantName},${appointmentUrl})}">顧問請您填寫保誠媒合平台的滿意度評比</p>
  </body>
</html>