diff --git a/lib/app/communications/contact.js b/lib/app/communications/contact.js index d4f3221..6eee87b 100644 --- a/lib/app/communications/contact.js +++ b/lib/app/communications/contact.js @@ -1,4 +1,4 @@ -import { Dropdown, createElement, createCheckbox, createPopup, showAlert } from "../../ui"; +import { Grid, Dropdown, createElement, createCheckbox, createPopup, showAlert } from "../../ui"; import { isEmail, nullOrEmpty, r } from "../../utility"; class Contact { @@ -185,4 +185,64 @@ class Contact { } } -export default Contact; \ No newline at end of file +class CustomerRecordContact { + #option; + #grid; + + constructor(option = {}) { + this.#option = option; + } + + async show(title, parent = document.body) { + const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3; + + const gridContainer = createElement('div', 'selcontact-grid'); + const popup = createPopup( + title, + createElement('div', 'selcontact-wrapper', + gridContainer + ), + { + text: r('ok', 'OK'), + key: 'ok', + trigger: () => { + if (typeof this.#option.onOk === 'function') { + return this.#option.onOk.call(this, this.#grid.source.filter(f => f.selected)); + } + } + }, + { text: r('cancel', 'Cancel'), key: 'cancel' } + ); + const result = await popup.show(parent); + // grid + const grid = new Grid(gridContainer); + grid.columns = [ + { + key: 'selected', + type: Grid.ColumnTypes.Checkbox, + width: 40, + // enabled: item => !nullOrEmpty(item.ID) + }, + { key: 'Name', caption: r("P_CR_CONTACTNAME", "Contact Name"), width: 100 }, + { key: 'Email', caption: r("P_CR_CONTACTEMAIL", "Contact Email"), css: { 'width': 180, 'text-align': 'left' } }, + { key: 'MobilePhoneDisplayText', caption: r("P_CR_CONTACTMOBILE", "Contact Mobile"), width: 130 }, + { key: 'ContactPreferenceStr', caption: r("P_CR_CONTACTPREFERENCES", "Contact Preferences"), width: 100 }, + { key: 'OptOut', caption: r("P_CR_OPTOUT", "Opt Out"), type: Grid.ColumnTypes.Checkbox, width: 70, enabled: false, align: 'center' }, + { key: 'Notes', caption: r("P_CR_NOTES", "Notes"), width: 120 } + ]; + grid.init(); + grid.source = this.#option.contacts.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) }); + this.#grid = grid; + return result; + } + + set source(contacts) { + this.#option.contacts = contacts; + const grid = this.#grid; + if (grid != null) { + this.#grid.source = contacts; + } + } +} + +export { Contact, CustomerRecordContact }; \ No newline at end of file diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js index 0f5f586..10357c1 100644 --- a/lib/app/communications/customer.js +++ b/lib/app/communications/customer.js @@ -1,7 +1,7 @@ import { Grid, createElement, setTooltip, createIcon, createCheckbox, createRadiobox, createPopup, showAlert, showConfirm } from "../../ui"; import { r, nullOrEmpty, formatUrl, isEmail, isPhone } from "../../utility"; import { createBox } from "./lib"; -import Contact from "./contact"; +import { Contact, CustomerRecordContact } from "./contact"; import Follower from "./follower"; class NoteCol extends Grid.GridColumn { @@ -124,7 +124,9 @@ class CustomerCommunication { } get contacts() { - return [...this.#contacts.children].map(el => { + return [...this.#contacts.children].filter(el => { + return el.querySelector('span').dataset.notsend == "false"; + }).map(el => { const span = el.querySelector('span'); return { 'Key': span.dataset.to, 'Value': span.dataset.name }; }); @@ -132,10 +134,13 @@ class CustomerCommunication { set contacts(contacts) { this.#contacts.replaceChildren(); if (contacts?.length > 0) { - for (let c of contacts) { - if (c.OptOut || c.OptOut_BC || c.selected === false) { - continue; - } + var cs = contacts.sort(function (a, b) { + if (a.Name == b.Name) return 0; return a.Name > b.Name ? 1 : -1; + }); + for (let c of cs) { + //if (c.OptOut || c.OptOut_BC || c.selected === false) { + // continue; + //} const mp = String(c.MobilePhoneDisplayText).trim(); const email = String(c.Email).trim(); const pref = String(c.ContactPreference); @@ -146,27 +151,34 @@ class CustomerCommunication { const to = pref === '1' ? email : mp; let icon; let method; - switch (pref) { - case '0': - icon = 'comment-lines'; - method = r('textsToColon', 'Texts to:'); - break; - case '2': - icon = 'mobile'; - method = r('callsToColon', 'Calls to:'); - break; - default: - icon = 'envelope'; - method = r('emailsToColon', 'Emails to:'); - break; + if (c.OptOut || c.OptOut_BC || c.selected === false) { + icon = 'times'; + method = r('optedOut', 'Opted Out:'); + } + else { + switch (pref) { + case '0': + icon = 'comment-lines'; + method = r('textsToColon', 'Texts to:'); + break; + case '2': + icon = 'mobile'; + method = r('callsToColon', 'Calls to:'); + break; + default: + icon = 'envelope'; + method = r('emailsToColon', 'Emails to:'); + break; + } } const span = createElement('span', span => { span.dataset.to = to; span.dataset.name = c.Name; + span.dataset.notsend = c.OptOut || c.OptOut_BC || c.selected === false; span.innerText = c.Name; }); const item = createElement('div', 'contact-item', - createIcon('fa-light', icon), + createIcon('fa-light', icon, { 'fill': (c.OptOut || c.OptOut_BC || c.selected === false) ? 'red' : '' }), span ); this.#contacts.appendChild(item); @@ -479,6 +491,88 @@ class CustomerCommunication { } }) ), + createElement('button', button => { + button.style.flex = '0 0 auto'; + button.style.backgroundColor = 'rgb(1, 199, 172)'; + button.style.marginRight = '10px'; + button.className = 'roundbtn button-from-customer-record'; + if (recordReadonly) { + button.style.display = 'none'; + } + button.appendChild(createIcon('fa-solid', 'handshake', { + width: '16px', + height: '16px' + })); + button.addEventListener('click', () => { + const sel = new CustomerRecordContact({ + contacts: [], + onOk: list => { + if (typeof this.#option.onSelectCRContacts === 'function') { + list?.map(c => { + delete c.selected; + return c; + }); + const result = this.#option.onSelectCRContacts(list); + } + const r = this.#data.contacts; + this.#gridContact.source = r.filter(c => c.Id >= 0).map(c => { + if (c.OptOut || c.OptOut_BC) { + return c; + } + if (typeof c.selected === 'undefined') { + c.selected = true; + } + return c; + }); + this.#gridWo.source = r.filter(c => c.Id < 0).map(c => { + if (c.OptOut || c.OptOut_BC) { + return c; + } + if (typeof c.selected === 'undefined') { + c.selected = true; + } + return c; + }); + } + }); + var title = r('selectFromCustomerRecord', 'Select from Customer Record'); + sel.show(title, container); + + if (typeof this.#option.onOpenSelectCRContacts === 'function') { + const result = this.#option.onOpenSelectCRContacts(); + if (typeof result?.then === 'function') { + return result.then(r => { + r.map(c => { + for (let cc of this.#data.contacts) { + if (c.Id === cc.Id) { + c.selected = true; + break; + } + } + if (typeof c.selected === 'undefined') { + c.selected = false; + } + return c; + }); + this.#data.contacts.filter(c => c.Id >= 0).map(c => { + if (c.OptOut || c.OptOut_BC) { + return c; + } + if (typeof c.selected === 'undefined') { + c.selected = true; + } + return c; + }); + + sel.source = r; + return r; + }); + } + return false; + } + }); + setTooltip(button, r('selectFromCustomerRecord', 'Select from Customer Record')) + }), createElement('button', button => { button.style.flex = '0 0 auto'; button.style.backgroundColor = 'rgb(1, 199, 172)'; @@ -568,7 +662,7 @@ class CustomerCommunication { const selectedCol = This => { return { key: 'selected', - type: TooltipCheckboxColumn, + type: Grid.ColumnTypes.Checkbox, width: 50, enabled: item => !item.OptOut && !item.OptOut_BC, onchanged: function () { @@ -576,7 +670,7 @@ class CustomerCommunication { option.onChanged([...This.#gridContact.source, ...This.#gridWo.source]); } }, - tooltip: item => item.OptOut ? r('optedOut', 'Opted Out') : r('optedIn', 'Opted In') + tooltip: item => item.selected ? r('optedIn', 'Opted In') : r('optedOut', 'Opted Out') } }; const iconCol = { @@ -819,6 +913,33 @@ class CustomerCommunication { button.appendChild(createIcon('fa-solid', 'pen')); setTooltip(button, r('editFollower', 'Edit Followers')); button.addEventListener('click', () => { + if (typeof this.#option.onInitFollower === 'function') { + this.#option.onInitFollower(this.#data.followers).then(data => { + if (typeof data === 'string') { + showAlert(r('customerRecord', 'Customer Record'), data, 'warn'); + return; + } + const add = new Follower({ + followers: data, + onOk: list => { + if (typeof this.#option.onAddFollower === 'function') { + const result = this.#option.onAddFollower(list); + if (typeof result?.then === 'function') { + return result.then(r => { + //this.#gridFollower.source = r; + return r; + }); + } + return false; + } + } + }); + var title = this.#data.followers?.length > 0 ? r('editFollowers', 'Edit Followers') : r('addFollowers', 'Add Followers'); + add.show(title, container); + }); + } + return; + const pop = createPopup( createElement('div', div => { div.style.display = 'flex'; diff --git a/lib/app/communications/follower.js b/lib/app/communications/follower.js index f939928..03dc41f 100644 --- a/lib/app/communications/follower.js +++ b/lib/app/communications/follower.js @@ -9,12 +9,12 @@ class Follower { this.#option = option; } - async show(parent = document.body) { + async show(title, parent = document.body) { const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3; const gridContainer = createElement('div', 'follower-grid'); const popup = createPopup( - r('addFollowers', 'Add Followers'), + title, createElement('div', 'follower-wrapper', createElement('div', div => div.innerText = r('whoWantReceiveCustomerNotification', 'Who do you want to receive customer notifications?')), createElement('input', search => { @@ -24,9 +24,10 @@ class Follower { search.addEventListener('input', () => { const key = search.value; if (nullOrEmpty(key)) { - this.#grid.source = this.#option.followers; + this.#grid.source = this.#option.followers.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) }); } else { - this.#grid.source = this.#option.followers.filter(f => f.Text || f.Email || contains(f.DisplayName, key, true)); + this.#grid.source = this.#option.followers.filter(f => f.Text || f.Email || contains(f.DisplayName, key, true)) + .sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) }); } }); }), @@ -66,7 +67,7 @@ class Follower { } ]; grid.init(); - grid.source = this.#option.followers; + grid.source = this.#option.followers.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) }); this.#grid = grid; return result; } diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss index 46adaea..84583d6 100644 --- a/lib/app/communications/style.scss +++ b/lib/app/communications/style.scss @@ -368,6 +368,8 @@ .contact-note { color: #999; + overflow: hidden; + text-overflow: ellipsis; } } } @@ -386,6 +388,16 @@ height: 380px; } } + + .selcontact-wrapper { + display: flex; + flex-direction: column; + width: 780px; + + > .selcontact-grid { + height: 200px; + } + } } } diff --git a/lib/ui/css/popup.scss b/lib/ui/css/popup.scss index bc24cd7..a31e1ce 100644 --- a/lib/ui/css/popup.scss +++ b/lib/ui/css/popup.scss @@ -119,6 +119,10 @@ $buttonHeight: 28px; animation: loading-spinner 1.2s infinite linear; } } + + &.ui-popup-loading-content { + bottom: 0; + } } >.message-wrapper { diff --git a/lib/ui/popup.js b/lib/ui/popup.js index 5781a1b..712a1dd 100644 --- a/lib/ui/popup.js +++ b/lib/ui/popup.js @@ -104,12 +104,48 @@ class Popup { } } + close(animation = true) { + const mask = this.#mask; + if (animation) { + mask.classList.add('ui-popup-active'); + mask.style.opacity = 0; + setTimeout(() => mask.remove(), 120); + } else { + mask.remove(); + } + } + create() { const mask = createElement('div', 'ui-popup-mask'); - if (this.#option.mask === false) { + const option = this.#option; + if (option.mask === false) { mask.classList.add('ui-popup-transparent'); } + if (!isNaN(option.zIndex)) { + mask.style.zIndex = String(option.zIndex); + } const container = createElement('div', 'ui-popup-container'); + if (option.changeZIndex === true) { + container.addEventListener('mousedown', () => { + const masks = [...this.#mask.parentElement.children].filter(e => e.classList.contains('ui-popup-mask')); + let max = 200; + masks.forEach(m => { + let index; + if (m.dataset.zindex != null) { + index = parseInt(m.dataset.zindex); + m.style.zIndex = isNaN(index) ? '' : String(index); + delete m.dataset.zindex; + } else { + index = parseInt(m.style.zIndex); + } + if (index > max) { + max = index; + } + }); + mask.dataset.zindex = mask.style.zIndex; + mask.style.zIndex = max + 1; + }); + } let tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); if (tabIndex < 0) { tabIndex = 0; @@ -120,14 +156,14 @@ class Popup { mask.style.opacity = 0; setTimeout(() => mask.remove(), 120); }; - let content = this.#option.content; + let content = option.content; if (!(content instanceof HTMLElement)) { content = createElement('div', d => d.innerText = content); } container.append( createElement('div', header => { header.className = 'ui-popup-header'; - let title = this.#option.title; + let title = option.title; if (!(title instanceof HTMLElement)) { title = createElement('div', t => { t.className = 'ui-popup-header-title'; @@ -135,7 +171,7 @@ class Popup { }); } header.appendChild(title); - if (this.#option.movable !== false) { + if (option.movable !== false) { const move = title.querySelector('.ui-popup-move') ?? title; move.addEventListener('mousedown', e => { const x = e.clientX - container.offsetLeft; @@ -150,15 +186,15 @@ class Popup { const up = () => { mask.removeEventListener('mousemove', move, { passive: false }); mask.removeEventListener('mouseup', up); - if (moved === true && typeof this.#option.onMoveEnded === 'function') { - this.#option.onMoveEnded.call(this); + if (moved === true && typeof option.onMoveEnded === 'function') { + option.onMoveEnded.call(this); } moved = false; }; mask.addEventListener('mouseup', up); }); } - if (this.#option.collapsable === true) { + if (option.collapsable === true) { const collapse = createIcon('fa-regular', 'compress-alt'); collapse.tabIndex = tabIndex + 2; collapse.classList.add('icon-expand'); @@ -183,6 +219,9 @@ class Popup { container.classList.add('ui-popup-collapse'); changeIcon(collapse, 'fa-regular', 'expand-alt'); } + if (typeof option.onResizeEnded === 'function') { + option.onResizeEnded.call(this); + } }); header.appendChild(collapse); } @@ -200,10 +239,10 @@ class Popup { createElement('div', null, createIcon('fa-regular', 'spinner-third')) )) ); - if (Array.isArray(this.#option.buttons)) { + if (Array.isArray(option.buttons) && option.buttons.length > 0) { tabIndex = Math.max.apply(null, [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); container.appendChild( - createElement('div', 'ui-popup-footer', ...this.#option.buttons.map((b, i) => { + createElement('div', 'ui-popup-footer', ...option.buttons.map((b, i) => { const button = createElement('button', 'ui-popup-button'); if (b.tabIndex > 0) { button.tabIndex = b.tabIndex; @@ -243,9 +282,11 @@ class Popup { } }); } + } else { + container.querySelector('.ui-popup-body>.ui-popup-loading').classList.add('ui-popup-loading-content'); } // resizable - if (this.#option.resizable === true) { + if (option.resizable === true) { container.append( createElement('layer', layer => { layer.className = 'ui-popup-border ui-popup-border-right'; @@ -291,6 +332,17 @@ class Popup { return; } let mask = this.#mask ?? this.create(); + const exists = [...parent.children].filter(e => e.classList.contains('ui-popup-mask')); + let zindex = 0; + for (let ex of exists) { + let z = parseInt(ex.style.zIndex); + if (!isNaN(z) && z > zindex) { + zindex = z; + } + } + if (zindex > 0) { + mask.style.zIndex = String(zindex + 1); + } parent.appendChild(mask); if (this.#option.mask === false) { // calculator position