From c38e486d7d7812488c00484c7442478f031b4d0e Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Mon, 10 Apr 2023 17:30:17 +0800 Subject: [PATCH] sync form work --- css/checkbox.scss | 2 +- css/dropdown.scss | 37 ++-- css/functions/checkbox.scss | 18 +- css/grid.scss | 31 ++-- css/popup.scss | 132 +++++++++++++- css/variables/definition.scss | 44 ++++- lib/app/communications/contact.js | 164 +++++++++++++++++ lib/app/communications/customer.js | 283 ++++++++++++++++++++++++++--- lib/app/communications/style.scss | 35 +++- lib/ui.js | 2 + lib/ui/grid.d.ts | 5 +- lib/ui/grid.js | 56 ++++-- lib/ui/popup.html | 12 +- lib/ui/popup.js | 188 +++++++++++++++---- vite.build.js | 1 + 15 files changed, 860 insertions(+), 150 deletions(-) create mode 100644 lib/app/communications/contact.js diff --git a/css/checkbox.scss b/css/checkbox.scss index fbd3a7e..98cb823 100644 --- a/css/checkbox.scss +++ b/css/checkbox.scss @@ -46,7 +46,7 @@ overflow: hidden; text-overflow: ellipsis; cursor: pointer; - color: $foreColor; + color: var(--color); } } } \ No newline at end of file diff --git a/css/dropdown.scss b/css/dropdown.scss index 1f3baaa..7978098 100644 --- a/css/dropdown.scss +++ b/css/dropdown.scss @@ -5,7 +5,6 @@ $headerHeight: 26px; $caretWidth: 26px; $dropItemHeight: 30px; -$borderRadius: 2px; $scrollBarSize: 4px; $searchBarHeight: 36px; @@ -21,16 +20,16 @@ $listMaxHeight: 210px; position: relative; >.drop-header { - border: 1px solid $borderColor; - border-radius: $borderRadius; - background-color: $bgColor; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background-color: var(--bg-color); display: flex; height: $headerHeight; transition: border-color .2s; &:focus, &:hover { - border-color: $focusColor; + border-color: var(--focus-color); // box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2); } @@ -52,7 +51,7 @@ $listMaxHeight: 210px; } &::-webkit-scrollbar-thumb { - border-radius: $borderRadius; + border-radius: var(--border-radius); } >span { @@ -64,7 +63,7 @@ $listMaxHeight: 210px; line-height: 20px; background-color: white; font-size: $tinySize; - border-radius: $borderRadius; + border-radius: var(--border-radius); cursor: pointer; position: relative; @@ -100,7 +99,7 @@ $listMaxHeight: 210px; padding: 0 4px; font-weight: 400; font-size: $smallSize; - color: $foreColor; + color: var(--color); } }*/ @@ -146,11 +145,11 @@ $listMaxHeight: 210px; } &.disabled { - border-color: $disabledBgColor; - color: $disabledForeColor; + border-color: var(--disabled-bg-color); + color: var(--disabled-color); &:focus { - border-color: $disabledBgColor; + border-color: var(--disabled-bg-color); // box-shadow: none; } @@ -167,13 +166,13 @@ $listMaxHeight: 210px; opacity: 0; transform: scaleY(0); transform-origin: top; - background-color: $bgColor; + background-color: var(--bg-color); top: calc($headerHeight + 2px); z-index: 2; transition: transform 120ms ease, opacity 120ms ease, visibility 120ms ease; width: calc(100% + 2px); box-sizing: border-box; - /*border: 1px solid $borderColor; + /*border: 1px solid var(--border-color); border-top-width: 0;*/ box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05); left: -1px; @@ -206,15 +205,15 @@ $listMaxHeight: 210px; width: 100%; height: $searchInputHeight; outline: none; - border: 1px solid $borderColor; - border-radius: $borderRadius; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); padding: 0 6px 0 22px; - color: $foreColor; + color: var(--color); transition: border-color .2s; &:hover, &:focus { - border-color: $focusColor; + border-color: var(--focus-color); } // &:focus { @@ -245,7 +244,7 @@ $listMaxHeight: 210px; @include scrollbar(); &.filtered>li:first-child { - background-color: $hoverColor; + background-color: var(--hover-color); } >li { @@ -261,7 +260,7 @@ $listMaxHeight: 210px; &:hover, &.selected { - background-color: $hoverColor; + background-color: var(--hover-color); } >.checkbox-wrapper { diff --git a/css/functions/checkbox.scss b/css/functions/checkbox.scss index fb6bb31..fb54c9b 100644 --- a/css/functions/checkbox.scss +++ b/css/functions/checkbox.scss @@ -1,7 +1,3 @@ -$boxBorderColor: #999898; -$boxCheckedColor: #1890ff; -$boxDisabledColor: #d9d9d9; - @mixin check-box() { .check-box-inner { position: relative; @@ -10,7 +6,7 @@ $boxDisabledColor: #d9d9d9; width: 14px; height: 14px; background-color: #fff; - border: 1px solid $boxBorderColor; + border: 1px solid var(--box-color); user-select: none; border-radius: 2px; transition: all .2s; @@ -33,8 +29,8 @@ $boxDisabledColor: #d9d9d9; display: none; &:checked+.check-box-inner { - border-color: $boxCheckedColor; - background-color: $boxCheckedColor; + border-color: var(--link-color); + background-color: var(--link-color); >svg { transform: scale(1); @@ -44,17 +40,17 @@ $boxDisabledColor: #d9d9d9; &:disabled { &+.check-box-inner { - border-color: $boxDisabledColor; + border-color: var(--disabled-box-color); cursor: default; } &:checked+.check-box-inner { - border-color: $boxDisabledColor; - background-color: $boxDisabledColor; + border-color: var(--disabled-box-color); + background-color: var(--disabled-box-color); } &~span { - color: $boxDisabledColor; + color: var(--disabled-box-color); cursor: default; } } diff --git a/css/grid.scss b/css/grid.scss index af4dcde..ba43b21 100644 --- a/css/grid.scss +++ b/css/grid.scss @@ -1,13 +1,3 @@ -@keyframes loading-spinner { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(359deg); - } -} - .grid { position: relative; box-sizing: border-box; @@ -17,8 +7,6 @@ & { --hover-bg-color: lightyellow; - --link-fore-color: #1890ff; - --link-disabled-color: #d9d9d9; --header-border-color: #adaba9; --header-bg-color: #fafafa; --header-fore-color: #000; @@ -32,8 +20,6 @@ --row-active-bg-color: #fafafa; --row-selected-bg-color: #e6f2fb; --text-disabled-color: gray; - --loading-bg-color: hsla(0, 0%, 100%, .4); - --loading-fore-color: rgba(0, 0, 0, .2); --font-size: .8125rem; --line-height: 36px; @@ -260,11 +246,16 @@ } } - .checkbox-wrapper .check-box-inner { + .checkbox-wrapper { + display: flex; + justify-content: center; - &, - >svg { - transition: none; + .check-box-inner { + + &, + >svg { + transition: none; + } } } @@ -293,7 +284,7 @@ } } - .icon { + .col-icon { display: flex; cursor: pointer; height: var(--line-height); @@ -304,7 +295,7 @@ >svg { width: 16px; height: 16px; - fill: var(--link-fore-color); + fill: var(--primary-color); transition: opacity .12s ease; } diff --git a/css/popup.scss b/css/popup.scss index 764f83b..86e031c 100644 --- a/css/popup.scss +++ b/css/popup.scss @@ -2,7 +2,7 @@ @import './variables/definition.scss'; $headerLineHeight: 24px; -$buttonHeight: 36px; +$buttonHeight: 28px; .popup-mask { position: fixed; @@ -15,17 +15,23 @@ $buttonHeight: 36px; z-index: 200; transition: opacity .12s ease; - &.active .popup-container { + & { + --corner-radius: 6px; + --loading-size: 20px; + --loading-border-radius: 10px; + } + + &.popup-active .popup-container { transform: scale(1.1); } .popup-container { min-width: 400px; max-width: 800px; - background-color: $bgColor; - border-radius: 2px; + background-color: var(--bg-color); + border-radius: var(--corner-radius); box-shadow: 0 2px 8px rgba(0 0 0 /11%); - transition: all .12s ease; + transition: opacity .12s ease, transform .12s ease; position: absolute; display: flex; flex-direction: column; @@ -33,11 +39,13 @@ $buttonHeight: 36px; .popup-header { flex: 0 0 auto; padding: 10px 12px 6px; + border-radius: var(--corner-radius) var(--corner-radius) 0 0; line-height: $headerLineHeight; user-select: none; background-color: var(--title-bg-color); color: var(--title-color); display: flex; + align-items: center; >div { flex: 1 1 auto; @@ -48,6 +56,7 @@ $buttonHeight: 36px; flex: 0 0 auto; width: $headerLineHeight; height: $headerLineHeight; + fill: var(--title-color); padding: 4px; cursor: pointer; box-sizing: border-box; @@ -63,6 +72,109 @@ $buttonHeight: 36px; margin: 6px 10px; flex: 1 1 auto; line-height: $headerLineHeight; + position: relative; + min-height: 100px; + + >.popup-loading { + position: absolute; + @include inset(0, 0, -46px, 0); + visibility: hidden; + opacity: 0; + transition: visibility 0s linear .12s, opacity .12s ease; + background-color: var(--loading-bg-color); + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + + >div { + background-color: var(--loading-fore-color); + border-radius: var(--loading-border-radius); + + >svg { + width: var(--loading-size); + height: var(--loading-size); + padding: 20px; + animation: loading-spinner 1.2s infinite linear; + } + } + } + + >.message-wrapper { + display: flex; + margin: 10px; + + >svg { + width: 40px; + height: 40px; + + &+span { + padding-left: 16px; + } + } + } + + .setting-wrapper { + --line-height: 28px; + + >.setting-item { + display: flex; + align-items: center; + line-height: var(--line-height); + margin: 4px 0; + + >.setting-label { + flex: 0 0 auto; + width: 120px; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 10px; + + &.setting-required::after { + content: '*'; + color: var(--red-color); + font-weight: bold; + } + + &+* { + flex: 1 1 auto; + margin-right: 10px; + box-sizing: border-box; + height: var(--line-height); + line-height: var(--line-height); + } + + &+input[type="text"], + &+input[type="email"], + &+input[type="tel"], + &+textarea { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + text-indent: var(--text-indent); + transition: border-color .12s ease; + + &:hover { + border-color: var(--focus-color); + } + + &:focus, + &:focus-visible { + outline: none; + } + } + + &+.drop-wrapper>.drop-header>.drop-text { + font-size: 12px; + } + + &+.checkbox-wrapper { + padding: 0; + } + } + } + } } .popup-footer { @@ -73,14 +185,18 @@ $buttonHeight: 36px; padding: 4px 10px 16px 2px; .popup-button { - margin-left: 8px; + margin-left: 12px; border: none; height: $buttonHeight; line-height: $buttonHeight; - padding: 0 8px; - min-width: 60px; + color: var(--title-color); + border-radius: var(--corner-radius); + padding: 0 16px; + box-sizing: border-box; + min-width: 70px; text-align: center; cursor: pointer; + user-select: none; background-color: var(--title-bg-color); transition: opacity .12s ease; diff --git a/css/variables/definition.scss b/css/variables/definition.scss index 896c9c1..e4ef44f 100644 --- a/css/variables/definition.scss +++ b/css/variables/definition.scss @@ -1,13 +1,37 @@ -// color -$borderColor: #d9d9d9; -$focusColor: #b9b9b9; -$disabledBgColor: #e9e9e9; -$disabledForeColor: #aaa; -$hoverColor: #eee; -$bgColor: #fff; -$foreColor: #201f1e; - // dimension $mediumSize: .875rem; // 14px $smallSize: .8125rem; // 13px -$tinySize: .75rem; // 12px \ No newline at end of file +$tinySize: .75rem; // 12px + +// animation +@keyframes loading-spinner { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(359deg); + } +} + +:root { + --color: #201f1e; + --red-color: red; + --bg-color: #fff; + --title-color: #fff; + --title-bg-color: rgb(68, 114, 196); + --border-color: #d9d9d9; + --focus-color: #b9b9b9; + --disabled-bg-color: #e9e9e9; + --disabled-color: #aaa; + --box-color: #999898; + --disabled-box-color: #d9d9d9; + --hover-color: #eee; + --link-color: #1890ff; + --primary-color: rgb(123,28,33); + --loading-bg-color: hsla(0, 0%, 100%, .4); + --loading-fore-color: rgba(0, 0, 0, .2); + + --border-radius: 2px; + --text-indent: 4px; +} \ No newline at end of file diff --git a/lib/app/communications/contact.js b/lib/app/communications/contact.js new file mode 100644 index 0000000..9b1bf39 --- /dev/null +++ b/lib/app/communications/contact.js @@ -0,0 +1,164 @@ +import { createElement } from "../../functions"; +import { createCheckbox } from "../../ui/checkbox"; +import Dropdown from "../../ui/dropdown"; +import { createPopup, showAlert } from "../../ui/popup"; +import { isEmail, nullOrEmpty, r } from "../../utility"; + +class Contact { + #option; + #refs; + + constructor(option = {}) { + this.#option = option; + } + + async show(parent = document.body) { + const c = this.#option.contact; + const contactName = createElement('input', input => { + input.type = 'text'; + input.tabIndex = 1; + input.maxLength = 200; + input.autocomplete = 'off'; + }); + const preferences = new Dropdown({ tabindex: 2 }); + preferences.source = [ + { value: '0', text: r('text', 'Text') }, + { value: '1', text: r('email', 'Email') }, + { value: '2', text: r('phone', 'Phone') } + ]; + const contactEmail = createElement('input', input => { + input.type = 'email'; + input.tabIndex = 3; + input.maxLength = 100; + input.autocomplete = 'off'; + }); + const contactMobile = createElement('input', input => { + input.type = 'tel'; + input.tabIndex = 4; + input.maxLength = 50; + input.autocomplete = 'off'; + }); + const checkOpt = createCheckbox({ + customerAttributes: { + tabindex: 5 + } + }); + const contactNotes = createElement('textarea', txt => { + txt.tabIndex = 6; + txt.maxLength = 2000; + txt.style.height = '100px'; + }); + const popup = createPopup( + c == null ? r('addContact', 'Add Contact') : r('editContact', 'Edit Contact'), + createElement('div', wrapper => { + wrapper.className = 'setting-wrapper'; + wrapper.style.width = '500px'; + }, + createElement('div', 'setting-item', + createElement('span', 'setting-label setting-required', r('contactNameColon', 'Contact Name:')), + contactName + ), + createElement('div', 'setting-item', + createElement('span', 'setting-label', r('contactPreferencesColon', 'Contact Preferences:')), + preferences.create() + ), + createElement('div', 'setting-item', + createElement('span', 'setting-label', r('contactEmailColon', 'Email Address:')), + contactEmail + ), + createElement('div', 'setting-item', + createElement('span', 'setting-label', r('contactMobileColon', 'Mobile:')), + contactMobile + ), + createElement('div', 'setting-item', + createElement('span', 'setting-label', r('contactOptColon', 'Opt Out:')), + checkOpt + ), + createElement('div', 'setting-item', + createElement('span', 'setting-label', r('contactNotesColon', 'Notes:')), + contactNotes + ) + ), + { + text: c == null ? r('addContactRecord', 'Add Contact Record') : r('editContactRecord', 'Edit Contact Record'), + trigger: () => { + const item = this.prepare(); + if (item == null) { + return false; + } + item.SaveToCustomer = 1; + if (typeof this.#option.onSave === 'function') { + return this.#option.onSave.call(this, item, c == null); + } + } + }, + { + text: r('workOrderOnly', 'Work Order Only'), + trigger: () => { + const item = this.prepare(); + if (item == null) { + return false; + } + item.SaveToCustomer = 0; + if (typeof this.#option.onSave === 'function') { + return this.#option.onSave.call(this, item, c == null); + } + } + }, + { text: r('cancel', 'Cancel') } + ) + if (c != null) { + contactName.value = c.Name; + preferences.select(String(c.ContactPreference)); + contactEmail.value = c.Email; + contactMobile.value = c.MobilePhone; + checkOpt.querySelector('input').checked = c.OptOut; + contactNotes.value = c.Notes; + } + this.#refs = { + contactName, + preferences, + contactEmail, + contactMobile, + checkOpt, + contactNotes + }; + const result = await popup.show(parent); + setTimeout(() => contactName.focus()); + return result; + } + + prepare() { + const item = { + 'Id': this.#option.contact?.Id, + 'Name': this.#refs.contactName.value, + 'ContactPreference': this.#refs.preferences.selected.value, + 'Email': this.#refs.contactEmail.value, + 'MobilePhone': this.#refs.contactMobile.value, + 'OptOut': this.#refs.checkOpt.querySelector('input').checked, + 'Notes': this.#refs.contactNotes.value + }; + const title = this.#option.contact == null ? r('addContact', 'Add Contact') : r('editContact', 'Edit Contact'); + if (nullOrEmpty(item.Name)) { + showAlert(title, r('contactNameRequired', 'Contact Name cannot be empty.'), 'warn') + .then(() => this.#refs.contactName.focus()); + return null; + } + if (nullOrEmpty(item.Email) && nullOrEmpty(item.MobilePhone)) { + showAlert(title, r('contactEmailPhoneRequired', 'Email and Mobile Phone cannot both be empty.'), 'warn') + .then(() => nullOrEmpty(item.Email) ? + this.#refs.contactEmail.focus() : + this.#refs.contactMobile.focus()); + return null; + } + if (!nullOrEmpty(item.Email) && !isEmail(item.Email)) { + showAlert(title, r('contactEmailInvalid', 'The email address is invalid.'), 'warn') + .then(() => this.#refs.contactEmail.focus()); + return null; + } + + return item; + } +} + +export default Contact; \ No newline at end of file diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js index 336d4c7..a56356d 100644 --- a/lib/app/communications/customer.js +++ b/lib/app/communications/customer.js @@ -7,7 +7,30 @@ import { setTooltip } from "../../ui/tooltip"; import { createIcon } from "../../ui/icon"; import { createCheckbox } from "../../ui/checkbox"; import { createBox } from "./lib"; -import { createPopup } from "../../ui/popup"; +import { createPopup, showAlert, showConfirm } from "../../ui/popup"; +import Grid from "../../ui/grid"; +import Contact from "./contact"; + +class NoteCol extends Grid.GridColumn { + static create() { + const wrapper = createElement('div', div => { + div.style.width = '100px'; + }, + createElement('div', 'contact-name'), + createElement('div', 'contact-note') + ); + return wrapper; + } + + static setValue(element, _val, item) { + const name = element.querySelector('.contact-name'); + name.innerText = item.Name; + if (name.scrollWidth > name.offsetWidth) { + setTooltip(name, item.Name); + } + element.querySelector('.contact-note').innerText = item.Notes; + } +} class CustomerCommunication { #container; @@ -16,6 +39,8 @@ class CustomerCommunication { #followers; #enter; #message; + #data = {}; + #gridContact; constructor(opt) { this.#option = opt ?? {}; @@ -87,13 +112,14 @@ class CustomerCommunication { } const mp = String(c.MobilePhone).trim(); const email = String(c.Email).trim(); - if (c.ContactPreference === '0' && !isPhone(mp) || - c.ContactPreference === '1' && !isEmail(email)) { + const pref = String(c.ContactPreference); + if (pref === '0' && !isPhone(mp) || + pref === '1' && !isEmail(email)) { return null; } - const to = c.ContactPreference === '0' ? mp : email; + const to = pref === '0' ? mp : email; return createElement('div', 'contact-item', - createIcon('fa-light', c.ContactPreference === '0' ? 'comment-lines' : 'envelope'), + createIcon('fa-light', pref === '0' ? 'comment-lines' : 'envelope'), setTooltip(createElement('span', span => { span.dataset.to = to; span.dataset.name = c.Name; @@ -156,11 +182,16 @@ class CustomerCommunication { this.#message.scrollTop = this.#message.scrollHeight } + setData(key, data) { + this.#data[key] = data; + } + create() { + const option = this.#option; // functions const checkAutoUpdate = createCheckbox({ className: 'check-auto-update', - checked: this.#option.autoUpdates, + checked: option.autoUpdates, checkedNode: createIcon('fa-regular', 'redo-alt'), uncheckedNode: createIcon('fa-regular', 'ban'), onchange: function () { @@ -171,7 +202,7 @@ class CustomerCommunication { }); const checkLink = createCheckbox({ className: 'check-status-link', - checked: this.#option.statusLink, + checked: option.statusLink, checkedNode: createIcon('fa-regular', 'link'), uncheckedNode: createIcon('fa-regular', 'unlink'), onchange: function () { @@ -185,10 +216,10 @@ class CustomerCommunication { createElement('div', div => div.innerText = r('messages', 'Customer Communication')), createElement('div', div => { div.className = 'title-company'; - if (nullOrEmpty(this.#option.companyName)) { + if (nullOrEmpty(option.companyName)) { div.style.display = 'none'; } else { - div.innerText = this.#option.companyName; + div.innerText = option.companyName; } }) ), @@ -198,7 +229,7 @@ class CustomerCommunication { ] ); // contacts - const readonly = this.#option.readonly; + const readonly = option.readonly; const contacts = createElement('div'); container.append( createElement('div', 'contact-bar', @@ -218,30 +249,236 @@ class CustomerCommunication { button.appendChild(createIcon('fa-solid', 'user-edit')); setTooltip(button, r('editContacts', 'Edit Contacts')); button.addEventListener('click', () => { - // TODO: const pop = createPopup( createElement('div', div => { div.style.display = 'flex'; div.append( - createElement('span', span => { - span.style.flex = '1 1 auto'; - span.innerText = r('editContacts', 'Edit Contacts'); - }), + createElement('div', div => { + div.className = 'popup-move'; + div.style.flex = '1 1 auto'; + }, + createElement('div', div => div.innerText = r('editContacts', 'Edit Contacts')), + createElement('div', div => { + div.className = 'title-company'; + if (nullOrEmpty(option.companyName)) { + div.style.display = 'none'; + } else { + div.innerText = option.companyName; + } + }) + ), createElement('button', button => { button.style.flex = '0 0 auto'; + button.style.backgroundColor = 'rgb(1, 199, 172)'; + button.style.marginRight = '10px'; button.className = 'roundbtn button-add-contact'; - button.backgroundColor = 'rgb(1, 199, 172)'; - button.appendChild(createIcon('fa-regular', 'user')); + button.appendChild(createIcon('fa-solid', 'user-plus', { + width: '16px', + height: '16px' + })); + button.addEventListener('click', () => { + const add = new Contact({ + onSave: (item) => { + const exists = this.#gridContact.source.some(s => s.Name === item.Name && s.MobilePhone === item.MobilePhone); + if (exists) { + showAlert(r('addContact', 'Add Contact'), r('contactUniqueRequired', 'Contact name and contact mobile must be a unique combination.'), 'warn'); + return false; + } + if (typeof option.onSave === 'function') { + const result = option.onSave(item, true); + if (result !== false) { + this.#gridContact.reload(); + } + return result; + } + } + }); + add.show(container); + }); setTooltip(button, r('addContact', 'Add Contact')) }) ) }), - createElement('div', div => { - div.style.height = '300px'; - div.innerText = 'Contacts from Customer Record'; - })); - container.append(pop); - setTimeout(() => pop.style.opacity = 1, 0); + createElement('div', null, + createElement('div', div => { + div.style.fontWeight = 'bold'; + div.innerText = r('contactFromRecord', 'Contacts from Customer Record'); + }), + createElement('div', div => { + div.className = 'contacts-record'; + div.style.maxHeight = '400px'; + div.style.width = '660px'; + }), + createElement('div', div => { + div.style.fontWeight = 'bold'; + div.innerText = r('contactFromWorkOrder', 'Contacts not on Customer Record'); + }), + createElement('div', div => { + div.className = 'contacts-wo'; + div.style.maxHeight = '200px'; + div.style.width = '660px'; + }) + ) + ); + pop.show(container).then(() => { + const selectedCol = { + key: 'selected', + type: Grid.ColumnTypes.Checkbox, + width: 50, + enabled: item => !item.OptOut && !item.OptOut_BC + }; + const iconCol = { + key: 'type', + type: Grid.ColumnTypes.Icon, + width: 50, + filter: c => String(c.ContactPreference) === '0' ? 'comment-lines' : 'envelope', + className: 'icon-contact-type', + iconType: 'fa-light' + }; + const nameCol = { key: 'Name', type: NoteCol, width: 160 }; + const buttonCol = { + type: Grid.ColumnTypes.Icon, + width: 40, + align: 'center', + iconType: 'fa-light' + }; + const createEditCol = grid => { + return { + key: 'edit', + ...buttonCol, + text: 'edit', + tooltip: r('edit', 'Edit'), + events: { + onclick: function () { + const edit = new Contact({ + contact: this, + onSave: item => { + const exists = grid.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone); + if (exists) { + showAlert(r('editContact', 'Edit Contact'), r('contactUniqueRequired', 'Contact name and contact mobile must be a unique combination.'), 'warn'); + return false; + } + if (typeof option.onSave === 'function') { + const result = option.onSave(item); + if (result !== false) { + grid.refresh(); + } + return result; + } + } + }); + edit.show(container); + } + } + } + }; + // contacts from customer record + const grid = new Grid(); + grid.height = 0; + grid.allowHtml = true; + grid.headerVisible = false; + grid.columns = [ + selectedCol, + iconCol, + nameCol, + { key: 'Email', width: 180 }, + { key: 'MobilePhone', width: 130 }, + createEditCol(grid), + { + key: 'delete', + ...buttonCol, + text: 'times', + tooltip: r('delete', 'Delete'), + events: { + onclick: function () { + showConfirm(r('remoteContact', 'Remove Contact'), r('removeFromCustomer', 'You are removing {name} from customer record.\n\nDo you want to Continue?').replace('{name}', this.Name), [ + { key: 'continue', text: r('continue', 'Continue') }, + { key: 'cancel', text: r('cancel', 'Cancel') } + ]).then(result => { + if (result === 'continue') { + if (typeof option.onDelete === 'function') { + option.onDelete(result, this, true); + } + const index = grid.source.indexOf(this); + if (index >= 0) { + const source = grid.source; + source.splice(index, 1); + grid.extraRows = source.filter(c => !nullOrEmpty(c.Notes)).length; + grid.source = source; + } + } + }); + } + } + } + ]; + grid.init(pop.container.querySelector('.contacts-record')); + const customerRecords = 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; + }); + grid.extraRows = customerRecords.filter(c => !nullOrEmpty(c.Notes)).length; + grid.source = customerRecords; + this.#gridContact = grid; + + // contacts from work order only + const gridWo = new Grid(); + gridWo.height = 0; + gridWo.allowHtml = true; + gridWo.headerVisible = false; + gridWo.columns = [ + selectedCol, + iconCol, + nameCol, + { key: 'Email', width: 180 }, + { key: 'MobilePhone', width: 130 }, + createEditCol(gridWo), + { + key: 'delete', + ...buttonCol, + text: 'times', + tooltip: r('delete', 'Delete'), + events: { + onclick: () => { + showConfirm(r('remoteContact', 'Remove Contact'), r('removeFromWorkorder', 'You are removing {name} from work order.\n\nDo you want to Continue?').replace('{name}', this.Name), [ + { key: 'continue', text: r('continue', 'Continue') }, + { key: 'cancel', text: r('cancel', 'Cancel') } + ]).then(result => { + if (result === 'continue') { + if (typeof option.onDelete === 'function') { + option.onDelete(result, this); + } + const index = gridWo.source.indexOf(this); + if (index >= 0) { + const source = gridWo.source; + source.splice(index, 1); + gridWo.extraRows = source.filter(c => !nullOrEmpty(c.Notes)).length; + gridWo.source = source; + } + } + }); + } + } + } + ]; + gridWo.init(pop.container.querySelector('.contacts-wo')); + const workOrderOnly = 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; + }); + gridWo.extraRows = workOrderOnly.filter(c => !nullOrEmpty(c.Notes)).length; + gridWo.source = workOrderOnly; + }); }); }) ) diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss index d7adc14..22977e5 100644 --- a/lib/app/communications/style.scss +++ b/lib/app/communications/style.scss @@ -1,7 +1,4 @@ -:root { - --title-color: #fff; - --title-bg-color: rgb(68, 114, 196); -} +@import '../../../css/variables/definition.scss'; .comm { display: flex; @@ -318,4 +315,34 @@ } } } + + .popup-mask .grid { + height: 100%; + overflow: hidden; + + >.grid-body .grid-body-content>.grid-row>td { + vertical-align: top; + + .icon-contact-type { + cursor: unset; + + >svg { + fill: #333; + } + + &:hover>svg { + opacity: unset; + } + } + + .contact-name { + overflow: hidden; + text-overflow: ellipsis; + } + + .contact-note { + color: #999; + } + } + } } \ No newline at end of file diff --git a/lib/ui.js b/lib/ui.js index 4b8f138..7b13823 100644 --- a/lib/ui.js +++ b/lib/ui.js @@ -4,6 +4,7 @@ import { createCheckbox, resolveCheckbox } from "./ui/checkbox"; import { setTooltip, resolveTooltip } from "./ui/tooltip"; import Dropdown from "./ui/dropdown"; import Grid from "./ui/grid"; +import Popup from "./ui/popup"; import { createPopup } from "./ui/popup"; export { @@ -21,5 +22,6 @@ export { // grid Grid, // popup + Popup, createPopup } diff --git a/lib/ui/grid.d.ts b/lib/ui/grid.d.ts index bfa9669..e355ff7 100644 --- a/lib/ui/grid.d.ts +++ b/lib/ui/grid.d.ts @@ -33,7 +33,7 @@ interface GridColumnDefinition { caption?: string; width?: Number; align?: "left" | "center" | "right"; - enabled?: boolean | string; + enabled?: boolean | string | ((item: GridItem | any) => boolean); css?: { [key: string]: string }; styleFilter?: (item: GridItem | any) => { [key: string]: string }; textStyle?: { [key: string]: string }; @@ -51,6 +51,7 @@ interface GridColumnDefinition { dropOptions?: DropdownOptions; source?: Array | ((item: GridItem | any) => Array | Promise>); iconType?: string; + className?: string | ((item: GridItem | any) => string); text?: string; tooltip?: string; onallchecked?: (this: Grid, col: GridColumnDefinition, flag: boolean) => void; @@ -73,6 +74,7 @@ interface Grid { langs?: { all: string, ok: string, reset: string }; virtualCount?: Number; rowHeight?: Number; + extraRows?: Number; filterRowHeight?: Number; height?: Number; readonly?: boolean; @@ -80,6 +82,7 @@ interface Grid { fullrowClick?: boolean; allowHtml?: boolean; holderDisabled?: boolean; + headerVisible?: boolean; window?: Window sortIndex?: Number; sortDirection?: keyof GridColumnDirection; diff --git a/lib/ui/grid.js b/lib/ui/grid.js index c641a35..9a26e21 100644 --- a/lib/ui/grid.js +++ b/lib/ui/grid.js @@ -172,19 +172,28 @@ class GridCheckboxColumn extends GridColumn { } class GridIconColumn extends GridColumn { - static create() { return createElement('span', 'icon') } + static create() { return createElement('span', 'col-icon') } static setValue(element, val, item, col) { + let className = col.className; + if (typeof className === 'function') { + className = className.call(col, item); + } + if (className == null) { + element.className = 'col-icon'; + } else { + element.className = `col-icon ${className}`; + } let type = col.iconType; if (typeof type === 'function') { - type = type(item); + type = type.call(col, item); } type ??= 'fa-regular'; if (element.dataset.type !== type || element.dataset.icon !== val) { const icon = createIcon(type, val); // const layer = element.children[0]; element.replaceChildren(icon); - !nullOrEmpty(col.tooltip) && setTooltip(icon, col.tooltip); + !nullOrEmpty(col.tooltip) && setTooltip(element, col.tooltip); element.dataset.type = type; element.dataset.icon = val; } @@ -238,7 +247,8 @@ class Grid { reset: r('reset', 'Reset') }; virtualCount = 100; - rowHeight = 39; + rowHeight = 36; + extraRows = 0; filterRowHeight = 30; height; readonly; @@ -246,6 +256,7 @@ class Grid { fullrowClick = true; allowHtml = false; holderDisabled = false; + headerVisible = true; window = global; sortIndex = -1; sortDirection = 1; @@ -416,7 +427,7 @@ class Grid { } scrollToIndex(index) { - const top = this.#scrollToTop(index * this.rowHeight, true); + const top = this.#scrollToTop(index * (this.rowHeight + 1), true); this.#refs.body.scrollTop = top; } @@ -431,13 +442,15 @@ class Grid { // body.style.top = `${height}px`; // top = height; // } - const top = this.#refs.header.offsetHeight; + const top = this.headerVisible === false ? 0 : this.#refs.header.offsetHeight; let height = this.height; - if (isNaN(height) || height <= 0) { + if (height === 0) { + height = this.#containerHeight; + } else if (isNaN(height) || height < 0) { height = this.#el.offsetHeight - top; } - const count = truncate((height - 1) / this.rowHeight) * (RedumCount * 2) + 1; + const count = truncate((height - 1) / (this.rowHeight + 1)) * (RedumCount * 2) + 1; if (force || count !== this.#rowCount) { this.#rowCount = count; this.reload(); @@ -446,7 +459,11 @@ class Grid { } reload() { - this.#containerHeight = this.#currentSource.length * this.rowHeight; + let length = this.#currentSource.length; + if (this.extraRows > 0) { + length += this.extraRows; + } + this.#containerHeight = length * (this.rowHeight + 1); this.#refs.body.scrollTop = 0; this.#refs.body.scrollLeft = 0; this.#refs.bodyContent.style.top = '0px'; @@ -555,6 +572,9 @@ class Grid { #createHeader() { const thead = createElement('table', 'grid-header'); + if (this.headerVisible === false) { + thead.style.display = 'none'; + } const header = createElement('tr'); thead.appendChild(header); const sizer = this.#refs.sizer; @@ -838,7 +858,9 @@ class Grid { enabled = false; } else { enabled = col.enabled; - if (typeof enabled === 'string') { + if (typeof enabled === 'function') { + enabled = enabled.call(col, item); + } else if (typeof enabled === 'string') { enabled = item[enabled]; } } @@ -1023,7 +1045,7 @@ class Grid { } #scrollToTop(top, reload) { - const rowHeight = this.rowHeight; + const rowHeight = (this.rowHeight + 1); top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight); if (top < 0) { top = 0; @@ -1234,7 +1256,8 @@ class Grid { return; } const key = col.key; - const test = typeof col.enabled === 'string'; + const isFunction = typeof col.enabled === 'function'; + const isString = typeof col.enabled === 'string'; if (typeof col.onallchecked === 'function') { col.onallchecked.call(this, col, flag); } else { @@ -1243,7 +1266,7 @@ class Grid { if (item == null) { continue; } - const enabled = test ? item[col.enabled] : col.enabled; + const enabled = isFunction ? col.enabled(item) : isString ? item[col.enabled] : col.enabled; if (enabled !== false) { item[key] = flag; row.__changed = true; @@ -1413,7 +1436,12 @@ class Grid { if (item == null) { return; } - const enabled = typeof col.enabled === 'string' ? item[col.enabled] : col.enabled; + let enabled = col.enabled; + if (typeof enabled === 'function') { + enabled = enabled.call(col, item); + } else if (typeof enabled === 'string') { + enabled = item[enabled]; + } if (enabled !== false) { item[col.key] = value; row.__changed = true; diff --git a/lib/ui/popup.html b/lib/ui/popup.html index d223f69..8b7f63a 100644 --- a/lib/ui/popup.html +++ b/lib/ui/popup.html @@ -14,9 +14,15 @@ document.querySelector('#button-popup').addEventListener('click', () => { const popup = ui.createPopup('title', 'content', - { text: 'Ok', trigger: () => true }); - document.body.appendChild(popup); - setTimeout(() => popup.style.opacity = 1); + { + text: 'Loading', trigger: p => { + p.loading = true; + setTimeout(() => p.loading = false, 1000); + return false; + } + }, + { text: 'OK' }); + popup.show(); });