diff --git a/css/checkbox.scss b/css/checkbox.scss index 98cb823..c73f2d5 100644 --- a/css/checkbox.scss +++ b/css/checkbox.scss @@ -38,7 +38,7 @@ &+* { flex: 1 1 auto; font-weight: 400; - font-size: $mediumSize; + font-size: var(--font-size); padding-left: 8px; padding-right: 6px; align-self: center; diff --git a/css/dropdown.scss b/css/dropdown.scss index 7978098..ad9569d 100644 --- a/css/dropdown.scss +++ b/css/dropdown.scss @@ -98,7 +98,7 @@ $listMaxHeight: 210px; line-height: $headerHeight; padding: 0 4px; font-weight: 400; - font-size: $smallSize; + font-size: var(--font-smaller-size); color: var(--color); } }*/ @@ -106,7 +106,7 @@ $listMaxHeight: 210px; >.drop-text { flex: 1 1 auto; cursor: pointer; - font-size: $mediumSize; + font-size: var(--font-size); // line-height: $headerHeight; padding: 0 6px; overflow: hidden; @@ -120,7 +120,7 @@ $listMaxHeight: 210px; cursor: initial; &::placeholder { - font-size: $smallSize; + font-size: var(--font-smaller-size); font-style: italic; } } @@ -240,7 +240,7 @@ $listMaxHeight: 210px; list-style: none; max-height: $listMaxHeight; overflow-y: auto; - font-size: $mediumSize; + font-size: var(--font-size); @include scrollbar(); &.filtered>li:first-child { diff --git a/css/functions/checkbox.scss b/css/functions/checkbox.scss index fb54c9b..c8ddd1e 100644 --- a/css/functions/checkbox.scss +++ b/css/functions/checkbox.scss @@ -25,7 +25,17 @@ } } - >input[type="checkbox"] { + &.radiobox-wrapper { + .check-box-inner { + box-sizing: border-box; + border-radius: 8px; + width: 16px; + height: 16px; + } + } + + >input[type="checkbox"], + >input[type="radio"] { display: none; &:checked+.check-box-inner { diff --git a/css/grid.scss b/css/grid.scss index ba43b21..1e90170 100644 --- a/css/grid.scss +++ b/css/grid.scss @@ -21,8 +21,8 @@ --row-selected-bg-color: #e6f2fb; --text-disabled-color: gray; - --font-size: .8125rem; - --line-height: 36px; + --row-height: 36px; + --line-height: 24px; --header-line-height: 26px; --text-indent: 8px; @@ -39,7 +39,7 @@ --header-padding: 4px 12px 4px 8px; --spacing-s: 4px; - --spacing-cell: 0 4px 0 8px; + --spacing-cell: 6px 4px 6px 8px; } &:focus, @@ -49,9 +49,11 @@ &, input[type="text"], + textarea, .drop-wrapper>.drop-header>.drop-text, .drop-wrapper>.drop-box>.drop-list { font-size: var(--font-size); + font-family: var(--font-family); } >.grid-sizer { @@ -86,7 +88,7 @@ >div { line-height: var(--header-line-height); - min-height: var(--line-height); + min-height: var(--row-height); display: flex; align-items: center; padding: var(--header-padding); @@ -228,12 +230,11 @@ white-space: pre; } - >input[type="text"] { + >input[type="text"], + >textarea { border: none; box-sizing: border-box; - height: calc(var(--line-height) + 2px); width: 100%; - text-indent: var(--text-indent); padding: 0; &:focus, @@ -246,6 +247,20 @@ } } + >input[type="text"] { + height: var(--row-height); + text-indent: var(--text-indent); + } + + >textarea { + resize: none; + line-height: var(--line-height); + display: block; + padding: var(--spacing-cell); + white-space: nowrap; + @include scrollbar(); + } + .checkbox-wrapper { display: flex; justify-content: center; @@ -260,7 +275,7 @@ } .drop-wrapper { - height: calc(var(--line-height) + 2px); + height: var(--row-height); width: 100%; display: flex; flex-direction: column; @@ -275,11 +290,11 @@ } >.drop-box { - top: calc(var(--line-height) + 2px); + top: calc(var(--row-height) + 2px); &.slide-up { top: unset; - bottom: calc(var(--line-height) + 2px); + bottom: calc(var(--row-height) + 2px); } } } @@ -287,7 +302,6 @@ .col-icon { display: flex; cursor: pointer; - height: var(--line-height); justify-content: center; align-items: center; position: relative; diff --git a/css/tooltip.scss b/css/tooltip.scss index 6b793bb..a100dbf 100644 --- a/css/tooltip.scss +++ b/css/tooltip.scss @@ -43,7 +43,7 @@ } >.tooltip-content { - font-size: $smallSize; + font-size: var(--font-smaller-size); line-height: 1rem; white-space: normal; overflow: hidden; diff --git a/css/variables/definition.scss b/css/variables/definition.scss index e4ef44f..a4246ca 100644 --- a/css/variables/definition.scss +++ b/css/variables/definition.scss @@ -1,8 +1,3 @@ -// dimension -$mediumSize: .875rem; // 14px -$smallSize: .8125rem; // 13px -$tinySize: .75rem; // 12px - // animation @keyframes loading-spinner { 0% { @@ -28,10 +23,15 @@ $tinySize: .75rem; // 12px --disabled-box-color: #d9d9d9; --hover-color: #eee; --link-color: #1890ff; - --primary-color: rgb(123,28,33); + --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; + + --font-size: .8125rem; + --font-smaller-size: .75rem; + --font-larger-size: .875rem; + --font-family: "Franklin Gothic Book", "San Francisco", "Segoe UI", "Open Sans", "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei UI", sans-serif; } \ No newline at end of file diff --git a/lib/app.js b/lib/app.js index a0ac1cc..45877af 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,7 +1,11 @@ import CustomerCommunication from "./app/communications/customer"; import InternalComment from "./app/communications/internal"; +import Popup, { showAlert, showConfirm } from "./ui/popup"; export { CustomerCommunication, - InternalComment + InternalComment, + Popup, + showAlert, + showConfirm } \ No newline at end of file diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js index a56356d..1b592c3 100644 --- a/lib/app/communications/customer.js +++ b/lib/app/communications/customer.js @@ -5,7 +5,7 @@ import { nullOrEmpty } from "../../utility/strings"; import { formatUrl, isEmail, isPhone } from "../../utility"; import { setTooltip } from "../../ui/tooltip"; import { createIcon } from "../../ui/icon"; -import { createCheckbox } from "../../ui/checkbox"; +import { createCheckbox, createRadiobox } from "../../ui/checkbox"; import { createBox } from "./lib"; import { createPopup, showAlert, showConfirm } from "../../ui/popup"; import Grid from "../../ui/grid"; @@ -13,9 +13,7 @@ import Contact from "./contact"; class NoteCol extends Grid.GridColumn { static create() { - const wrapper = createElement('div', div => { - div.style.width = '100px'; - }, + const wrapper = createElement('div', 'contact-wrapper', createElement('div', 'contact-name'), createElement('div', 'contact-note') ); @@ -41,6 +39,8 @@ class CustomerCommunication { #message; #data = {}; #gridContact; + #gridWo; + #gridFollower; constructor(opt) { this.#option = opt ?? {}; @@ -107,7 +107,7 @@ class CustomerCommunication { } #createContactItem(c) { - if (c.OptOut || c.OptOut_BC) { + if (c.OptOut || c.OptOut_BC || c.selected === false) { return null; } const mp = String(c.MobilePhone).trim(); @@ -160,6 +160,17 @@ class CustomerCommunication { this.#enter.disabled = flag === true; } + /** + * @param {boolean} flag + */ + set recordReadonly(flag) { + this.#option.recordReadonly = flag; + if (this.#container == null) { + return; + } + this.#container.querySelector('.button-edit-contacts').style.display = flag === true ? 'none' : ''; + } + get followers() { return [...this.#followers.children].map(el => { const span = el.querySelector('span'); @@ -167,6 +178,7 @@ class CustomerCommunication { }); } set followers(followers) { + this.#data.followers = followers; this.#followers.replaceChildren(); if (followers?.length > 0) { this.#container.querySelector('.follower-bar').style.display = ''; @@ -243,7 +255,7 @@ class CustomerCommunication { createElement('button', button => { button.className = 'roundbtn button-edit-contacts'; button.style.backgroundColor = 'rgb(1, 199, 172)'; - if (readonly === true) { + if (readonly === true || option.recordReadonly) { button.style.display = 'none'; } button.appendChild(createIcon('fa-solid', 'user-edit')); @@ -286,10 +298,14 @@ class CustomerCommunication { } if (typeof option.onSave === 'function') { const result = option.onSave(item, true); - if (result !== false) { - this.#gridContact.reload(); + if (typeof result?.then === 'function') { + return result.then(r => { + this.#gridContact.source = r.filter(c => c.Id >= 0); + this.#gridWo.source = r.filter(c => c.Id < 0); + return r; + }); } - return result; + return false; } } }); @@ -321,11 +337,18 @@ class CustomerCommunication { ) ); pop.show(container).then(() => { - const selectedCol = { - key: 'selected', - type: Grid.ColumnTypes.Checkbox, - width: 50, - enabled: item => !item.OptOut && !item.OptOut_BC + const selectedCol = This => { + return { + key: 'selected', + type: Grid.ColumnTypes.Checkbox, + width: 50, + enabled: item => !item.OptOut && !item.OptOut_BC, + onchanged: function () { + if (typeof option.onChanged === 'function') { + option.onChanged(This.#gridContact.source.concat(This.#gridWo.source)); + } + } + } }; const iconCol = { key: 'type', @@ -342,7 +365,7 @@ class CustomerCommunication { align: 'center', iconType: 'fa-light' }; - const createEditCol = grid => { + const createEditCol = (This) => { return { key: 'edit', ...buttonCol, @@ -353,17 +376,23 @@ class CustomerCommunication { const edit = new Contact({ contact: this, onSave: item => { - const exists = grid.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone); + const exists = + This.#gridContact.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone) || + This.#gridWo.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(); + if (typeof result?.then === 'function') { + return result.then(r => { + This.#gridContact.source = r.filter(c => c.Id >= 0); + This.#gridWo.source = r.filter(c => c.Id < 0); + return r; + }); } - return result; + return false; } } }); @@ -378,7 +407,7 @@ class CustomerCommunication { grid.allowHtml = true; grid.headerVisible = false; grid.columns = [ - selectedCol, + selectedCol(this), iconCol, nameCol, { key: 'Email', width: 180 }, @@ -391,23 +420,45 @@ class CustomerCommunication { 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); + showConfirm( + r('remoteContact', 'Remove Contact'), + createElement('div', null, + createElement('div', div => div.innerText = r('removeFrom', 'Remove {name} from').replace('{name}', this.Name)), + createElement('div', div => { + div.style.display = 'flex'; + div.style.justifyContent = 'center'; + div.style.marginTop = '10px'; + }, + createRadiobox({ + name: 'remove-type', + label: r('customerRecord', 'Customer Record'), + checked: true, + className: 'radio-customer-record' + }), + createRadiobox({ + name: 'remove-type', + label: r('workOrder', 'Work Order') + }) + ) + ), + [ + { key: 'ok', text: r('ok', 'OK') }, + { key: 'cancel', text: r('cancel', 'Cancel') } + ]).then(result => { + if (result?.key === 'ok') { + const isRecord = result.popup.container.querySelector('.radio-customer-record>input').checked; + if (typeof option.onDelete === 'function') { + option.onDelete(result.key, this, isRecord); + } + 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; + } } - 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; - } - } - }); + }); } } } @@ -432,7 +483,7 @@ class CustomerCommunication { gridWo.allowHtml = true; gridWo.headerVisible = false; gridWo.columns = [ - selectedCol, + selectedCol(this), iconCol, nameCol, { key: 'Email', width: 180 }, @@ -449,9 +500,9 @@ class CustomerCommunication { { key: 'continue', text: r('continue', 'Continue') }, { key: 'cancel', text: r('cancel', 'Cancel') } ]).then(result => { - if (result === 'continue') { + if (result?.key === 'continue') { if (typeof option.onDelete === 'function') { - option.onDelete(result, this); + option.onDelete(result.key, this); } const index = gridWo.source.indexOf(this); if (index >= 0) { @@ -478,6 +529,7 @@ class CustomerCommunication { }); gridWo.extraRows = workOrderOnly.filter(c => !nullOrEmpty(c.Notes)).length; gridWo.source = workOrderOnly; + this.#gridWo = gridWo; }); }); }) @@ -512,7 +564,116 @@ class CustomerCommunication { button.appendChild(createIcon('fa-solid', 'pen')); setTooltip(button, r('editFollower', 'Edit Followers')); button.addEventListener('click', () => { - // TODO: + const pop = createPopup( + createElement('div', div => { + div.style.display = 'flex'; + div.style.alignItems = 'center'; + div.append( + createElement('div', div => { + div.className = 'popup-move'; + div.style.flex = '1 1 auto'; + div.innerText = r('editContacts', 'Edit Contacts') + '\n' + r('followers', 'Followers'); + }), + 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-follower'; + 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 (typeof result?.then === 'function') { + return result.then(r => { + this.#gridContact.source = r.filter(c => c.Id >= 0); + this.#gridWo.source = r.filter(c => c.Id < 0); + return r; + }); + } + return false; + } + } + }); + add.show(container); + */ + }); + setTooltip(button, r('addFollower', 'Add Follower')) + }) + ) + }), + createElement('div', null, + createElement('div', div => { + div.style.fontWeight = 'bold'; + div.innerText = r('contactFromRecord', 'Contacts from Customer Record'); + }), + createElement('div', div => { + div.className = 'followers-record'; + div.style.maxHeight = '400px'; + div.style.width = '660px'; + }) + ) + ); + pop.show(container).then(() => { + const grid = new Grid(); + grid.height = 0; + grid.allowHtml = true; + grid.headerVisible = false; + grid.columns = [ + { + key: 'type', + type: Grid.ColumnTypes.Icon, + width: 50, + filter: c => String(c.ContactPreference) === '0' ? 'comment-lines' : 'envelope', + className: 'icon-contact-type', + iconType: 'fa-light' + }, + { key: 'Name', width: 160 }, + { key: 'Email', width: 180 }, + { key: 'MobilePhone', width: 130 }, + { + key: 'delete', + type: Grid.ColumnTypes.Icon, + width: 40, + align: 'center', + iconType: 'fa-light', + text: 'times', + tooltip: r('delete', 'Delete'), + events: { + onclick: function () { + showConfirm( + r('deleteFollower', 'Delete Follower'), + r('promptDeleteFollower', 'Do you want to delete this follower?')).then(result => { + if (result?.key === 'yes') { + if (typeof option.onDeleteFollower === 'function') { + option.onDeleteFollower(result.key, this); + } + 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('.followers-record')); + grid.source = this.#data.followers; + this.#gridFollower = grid; + }); }); }) ) diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss index 22977e5..81c3783 100644 --- a/lib/app/communications/style.scss +++ b/lib/app/communications/style.scss @@ -13,7 +13,6 @@ --dark-fore-opacity-color: rgba(255, 255, 255, .6); --strong-color: #333; --light-color: #ccc; - --medium-font-size: .875rem; } .roundbtn { @@ -179,7 +178,7 @@ >span { flex: 1 1 auto; color: var(--strong-color); - font-size: var(--medium-font-size); + font-size: var(--font-larger-size); overflow: hidden; text-overflow: ellipsis; padding-right: 10px; @@ -318,11 +317,16 @@ .popup-mask .grid { height: 100%; + min-height: 120px; overflow: hidden; >.grid-body .grid-body-content>.grid-row>td { vertical-align: top; + .col-icon { + padding: 10px 4px 10px 8px; + } + .icon-contact-type { cursor: unset; @@ -335,13 +339,18 @@ } } - .contact-name { - overflow: hidden; - text-overflow: ellipsis; - } + .contact-wrapper { + width: 100px; + padding: var(--spacing-cell); - .contact-note { - color: #999; + .contact-name { + overflow: hidden; + text-overflow: ellipsis; + } + + .contact-note { + color: #999; + } } } } diff --git a/lib/ui/checkbox.js b/lib/ui/checkbox.js index 583f4d6..01f544e 100644 --- a/lib/ui/checkbox.js +++ b/lib/ui/checkbox.js @@ -1,9 +1,9 @@ import { createElement } from "../functions"; import { createIcon } from "./icon"; -function fillCheckbox(container, type, label) { +function fillCheckbox(container, type, label, charactor = 'check') { container.appendChild( - createElement('layer', 'check-box-inner', createIcon(type, 'check')) + createElement('layer', 'check-box-inner', createIcon(type, charactor)) ); if (label instanceof HTMLElement) { container.appendChild(label); @@ -14,6 +14,33 @@ function fillCheckbox(container, type, label) { } } +function createRadiobox(opts = {}) { + const container = createElement('label', 'checkbox-wrapper radiobox-wrapper', + createElement('input', input => { + input.setAttribute('type', 'radio'); + input.name = opts.name; + if (opts.checked === true) { + input.checked = true; + } + if (opts.enabled === false) { + input.disabled = true; + } + if (opts.customerAttributes != null) { + for (let entry of Object.entries(opts.customerAttributes)) { + input.setAttribute(entry[0], entry[1]); + } + } + if (typeof opts.onchange === 'function') { + input.addEventListener('change', opts.onchange); + } + })); + if (opts.className) { + container.classList.add(opts.className); + } + fillCheckbox(container, opts.type || 'fa-regular', opts.label, 'circle'); + return container; +} + function createCheckbox(opts = {}) { const container = createElement('label', 'checkbox-wrapper', createElement('input', input => { @@ -131,5 +158,6 @@ function resolveCheckbox(container = document.body, legacy) { export { createCheckbox, - resolveCheckbox + resolveCheckbox, + createRadiobox } \ No newline at end of file diff --git a/lib/ui/grid.d.ts b/lib/ui/grid.d.ts index e355ff7..65b2319 100644 --- a/lib/ui/grid.d.ts +++ b/lib/ui/grid.d.ts @@ -25,6 +25,7 @@ interface GridColumnType { 2: "Dropdown"; 3: "Checkbox"; 4: "Icon"; + 5: "Text"; } interface GridColumnDefinition { @@ -123,6 +124,7 @@ declare var Grid: { Dropdown: 2, Checkbox: 3, Icon: 4, + Text: 5, isCheckbox(type: Number): boolean; }; GridColumn: typeof GridColumn; diff --git a/lib/ui/grid.html b/lib/ui/grid.html index 2bdfa62..92856b5 100644 --- a/lib/ui/grid.html +++ b/lib/ui/grid.html @@ -223,6 +223,7 @@ type: statusCol, enabled: 'enabled' }, + { key: 'c2c', caption: '多行编辑', type: Grid.ColumnTypes.Text, enabled: 'enabled' }, { key: 'c3', caption: 'column 3', width: 90 }, { key: 'c4', caption: 'Note', type: Grid.ColumnTypes.Input }, { @@ -252,11 +253,11 @@ grid.cellDblClicked = (rId, cId) => console.log(`row (${rId}), column (${cId}) double clicked.`); grid.init(); grid.source = [ - { c1: 'abc', c2: true, c2a: 'off', c2b: 'red', c3: 12345, c4: 'another note', enabled: false }, + { c1: 'abc', c2: true, c2a: 'off', c2b: 'red', c2c: 'multiple lines\nline2\nline3\n\nline5', c3: 12345, c4: 'another note' }, { c1: 'abc2bbbbaaaaa', c2: false, c2a: 'pending', c2b: 'bold', c3: 1225, c4: 'Note note this is note' }, - { c1: 'type', c2: false, c2a: 'broken', c3: 121111 }, + { c1: 'type', c2: false, c2a: 'broken', c2c: 'multiple lines\nline2\nline3\n\nline5', c3: 121111 }, { c1: 'diff', c2: true, c2a: 'running', c3: 124445555555555555 }, - { c1: 'diff', c2: true, c2a: 'running', c3: 12499 }, + { c1: 'diff', c2: true, c2a: 'running', c3: 12499, enabled: false }, { c1: 'diff', c2: true, c2a: 'off', c3: 1244445 } ]; @@ -265,7 +266,7 @@