app change

This commit is contained in:
2023-04-11 17:34:13 +08:00
parent c38e486d7d
commit cc3d11f617
14 changed files with 385 additions and 98 deletions

View File

@ -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
}

View File

@ -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;
});
});
})
)

View File

@ -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;
}
}
}
}

View File

@ -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
}

2
lib/ui/grid.d.ts vendored
View File

@ -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;

View File

@ -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: '<font style="color: red; margin-left: 8px">red</font>', c3: 12345, c4: 'another note', enabled: false },
{ c1: 'abc', c2: true, c2a: 'off', c2b: '<font style="color: red; margin-left: 8px">red</font>', c2c: 'multiple lines\nline2\nline3\n\nline5', c3: 12345, c4: 'another note' },
{ c1: 'abc2bbbbaaaaa', c2: false, c2a: 'pending', c2b: '<b style="margin-left: 8px">bold</b>', 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 @@
</script>
<style type="text/css">
#grid-sample {
/* height: 400px; */
height: 500px;
}
#grid-sample>.grid {

View File

@ -54,12 +54,15 @@ class GridColumn {
}
class GridInputColumn extends GridColumn {
static createEdit(trigger) {
static get editing() { return true };
static createEdit(trigger, _col, _parent, vals) {
const input = createElement('input');
input.setAttribute('type', 'text');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => vals.__editing = true);
return input;
}
@ -76,6 +79,29 @@ class GridInputColumn extends GridColumn {
static setEnabled(element, enabled) { element.disabled = enabled === false }
}
class GridTextColumn extends GridInputColumn {
static createEdit(trigger, _col, _parent, vals) {
const input = createElement('textarea');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => vals.__editing = true);
return input;
}
static setValue(element, val, _item, _col, grid) {
if (element.tagName !== 'TEXTAREA') {
super.setValue(element, val);
} else {
element.value = val;
if (val != null) {
const lines = String(val).split('\n').length;
element.style.height = `${lines * grid.lineHeight + 12}px`;
}
}
}
}
const SymbolDropdown = Symbol.for('ui-dropdown');
class GridDropdownColumn extends GridColumn {
@ -217,7 +243,8 @@ const ColumnTypes = {
1: GridInputColumn,
2: GridDropdownColumn,
3: GridCheckboxColumn,
4: GridIconColumn
4: GridIconColumn,
5: GridTextColumn
};
class Grid {
@ -248,6 +275,7 @@ class Grid {
};
virtualCount = 100;
rowHeight = 36;
lineHeight = 24;
extraRows = 0;
filterRowHeight = 30;
height;
@ -274,6 +302,7 @@ class Grid {
Dropdown: 2,
Checkbox: 3,
Icon: 4,
Text: 5,
isCheckbox(type) { return type === 3 }
};
@ -715,10 +744,15 @@ class Grid {
if (!this.holderDisabled) {
const holder = createElement('div', 'grid-hover-holder');
holder.addEventListener('mousedown', e => {
const keyid = e.currentTarget.keyid;
const holder = e.currentTarget;
const keyid = holder.keyid;
if (keyid == null) {
return;
}
delete holder.keyid;
if (holder.classList.contains('active')) {
holder.classList.remove('active');
}
return this.#onRowClicked(e, (keyid >>> MaxColumnBit) - this.#startIndex, keyid & MaxColumnMask);
});
holder.addEventListener('dblclick', e => this.#onRowDblClicked(e));
@ -846,8 +880,12 @@ class Grid {
const type = isCheckbox ? GridCheckboxColumn : this.#colTypes[col.key] ?? GridColumn;
let element;
if (!isCheckbox && selectChanged && typeof type.createEdit === 'function') {
if (vals.__editing && type.editing) {
val = type.getValue({ target: cell.children[0] });
this.#onRowChanged(null, startIndex + i, col, val, true);
}
element = selected ?
type.createEdit(e => this.#onRowChanged(e, startIndex + i, col, type.getValue(e)), col, this.#refs.bodyContent) :
type.createEdit(e => this.#onRowChanged(e, startIndex + i, col, type.getValue(e)), col, this.#refs.bodyContent, vals) :
type.create(col);
cell.replaceChildren(element);
} else {
@ -864,7 +902,7 @@ class Grid {
enabled = item[enabled];
}
}
type.setValue(element, val, item, col);
type.setValue(element, val, item, col, this);
if (typeof type.setEnabled === 'function') {
type.setEnabled(element, enabled);
}
@ -898,6 +936,9 @@ class Grid {
}
}
});
if (vals.__editing) {
delete vals.__editing;
}
});
}
@ -1322,7 +1363,7 @@ class Grid {
if (overflow) {
holder.keyid = keyid;
holder.innerText = element.innerText;
const top = this.#refs.bodyContent.offsetTop + target.offsetTop + 1;
const top = this.#refs.bodyContent.offsetTop + target.offsetTop;
let left = target.offsetLeft;
let width = holder.offsetWidth;
if (width > this.#bodyClientWidth) {
@ -1412,7 +1453,7 @@ class Grid {
}
#onRowDblClicked(e) {
if (e.target.tagName === 'INPUT') {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'LAYER' && e.target.className === 'check-box-inner' || e.target.tagName === 'LABEL' && (e.target.className === 'drop-text' || e.target.className === 'drop-caret')) {
return;
}
const index = this.selectedIndex;
@ -1427,7 +1468,7 @@ class Grid {
}
}
#onRowChanged(_e, index, col, value) {
#onRowChanged(_e, index, col, value, blur) {
if (this.#currentSource == null) {
return;
}
@ -1445,8 +1486,14 @@ class Grid {
if (enabled !== false) {
item[col.key] = value;
row.__changed = true;
if (typeof col.onchanged === 'function') {
col.onchanged.call(this, item, value);
if (blur) {
if (typeof col.oneditend === 'function') {
col.oneditend.call(this, item, value);
}
} else {
if (typeof col.onchanged === 'function') {
col.onchanged.call(this, item, value);
}
}
}
}

View File

@ -60,10 +60,16 @@ class Popup {
const button = createElement('div', 'popup-button');
button.innerText = b.text;
button.addEventListener('click', () => {
if (typeof b.trigger === 'function' && b.trigger(this) === false) {
return;
if (typeof b.trigger === 'function') {
const result = b.trigger(this);
if (typeof result?.then === 'function') {
result.then(r => r !== false && close()).catch(() => { });
} else if (result !== false) {
close();
}
} else {
close();
}
close();
});
return button;
}))
@ -140,20 +146,25 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
});
}
export function showConfirm(title, message, buttons, iconType = 'question', parent = document.body) {
export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) {
return new Promise(resolve => {
const popup = new Popup({
title,
content: createElement('div', 'message-wrapper',
createIcon('fa-solid', iconTypes[iconType] ?? 'question-circle'),
createElement('span', span => span.innerText = message)
createElement('span', null, content)
),
buttons: buttons?.map(b => {
return { text: b.text, trigger: p => resolve(b.key, p) }
return {
text: b.text, trigger: p => resolve({
key: b.key,
popup: p
})
};
}) ??
[
{ text: r('yes', 'Yes'), trigger: p => resolve('yes', p) },
{ text: r('no', 'No'), trigger: p => resolve('no', p) }
{ text: r('yes', 'Yes'), trigger: p => resolve({ key: 'yes', popup: p }) },
{ text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) }
]
});
popup.show(parent);