Add clusterize, move customer field to POSCart, get POS profile
This commit is contained in:
parent
6e7db034f2
commit
03e7ec29e7
@ -1,7 +1,7 @@
|
||||
.pos {
|
||||
padding: 15px;
|
||||
}
|
||||
.customer-container {
|
||||
.cart-container {
|
||||
padding: 0 15px;
|
||||
display: inline-block;
|
||||
width: 39%;
|
||||
|
329
erpnext/public/js/pos/clusterize.js
Normal file
329
erpnext/public/js/pos/clusterize.js
Normal file
@ -0,0 +1,329 @@
|
||||
/*! Clusterize.js - v0.17.6 - 2017-03-05
|
||||
* http://NeXTs.github.com/Clusterize.js/
|
||||
* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
|
||||
|
||||
;(function(name, definition) {
|
||||
if (typeof module != 'undefined') module.exports = definition();
|
||||
else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
|
||||
else this[name] = definition();
|
||||
}('Clusterize', function() {
|
||||
"use strict"
|
||||
|
||||
// detect ie9 and lower
|
||||
// https://gist.github.com/padolsey/527683#comment-786682
|
||||
var ie = (function(){
|
||||
for( var v = 3,
|
||||
el = document.createElement('b'),
|
||||
all = el.all || [];
|
||||
el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
|
||||
all[0];
|
||||
){}
|
||||
return v > 4 ? v : document.documentMode;
|
||||
}()),
|
||||
is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
|
||||
var Clusterize = function(data) {
|
||||
if( ! (this instanceof Clusterize))
|
||||
return new Clusterize(data);
|
||||
var self = this;
|
||||
|
||||
var defaults = {
|
||||
rows_in_block: 50,
|
||||
blocks_in_cluster: 4,
|
||||
tag: null,
|
||||
show_no_data_row: true,
|
||||
no_data_class: 'clusterize-no-data',
|
||||
no_data_text: 'No data',
|
||||
keep_parity: true,
|
||||
callbacks: {}
|
||||
}
|
||||
|
||||
// public parameters
|
||||
self.options = {};
|
||||
var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
|
||||
for(var i = 0, option; option = options[i]; i++) {
|
||||
self.options[option] = typeof data[option] != 'undefined' && data[option] != null
|
||||
? data[option]
|
||||
: defaults[option];
|
||||
}
|
||||
|
||||
var elems = ['scroll', 'content'];
|
||||
for(var i = 0, elem; elem = elems[i]; i++) {
|
||||
self[elem + '_elem'] = data[elem + 'Id']
|
||||
? document.getElementById(data[elem + 'Id'])
|
||||
: data[elem + 'Elem'];
|
||||
if( ! self[elem + '_elem'])
|
||||
throw new Error("Error! Could not find " + elem + " element");
|
||||
}
|
||||
|
||||
// tabindex forces the browser to keep focus on the scrolling list, fixes #11
|
||||
if( ! self.content_elem.hasAttribute('tabindex'))
|
||||
self.content_elem.setAttribute('tabindex', 0);
|
||||
|
||||
// private parameters
|
||||
var rows = isArray(data.rows)
|
||||
? data.rows
|
||||
: self.fetchMarkup(),
|
||||
cache = {},
|
||||
scroll_top = self.scroll_elem.scrollTop;
|
||||
|
||||
// append initial data
|
||||
self.insertToDOM(rows, cache);
|
||||
|
||||
// restore the scroll position
|
||||
self.scroll_elem.scrollTop = scroll_top;
|
||||
|
||||
// adding scroll handler
|
||||
var last_cluster = false,
|
||||
scroll_debounce = 0,
|
||||
pointer_events_set = false,
|
||||
scrollEv = function() {
|
||||
// fixes scrolling issue on Mac #3
|
||||
if (is_mac) {
|
||||
if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
|
||||
pointer_events_set = true;
|
||||
clearTimeout(scroll_debounce);
|
||||
scroll_debounce = setTimeout(function () {
|
||||
self.content_elem.style.pointerEvents = 'auto';
|
||||
pointer_events_set = false;
|
||||
}, 50);
|
||||
}
|
||||
if (last_cluster != (last_cluster = self.getClusterNum()))
|
||||
self.insertToDOM(rows, cache);
|
||||
if (self.options.callbacks.scrollingProgress)
|
||||
self.options.callbacks.scrollingProgress(self.getScrollProgress());
|
||||
},
|
||||
resize_debounce = 0,
|
||||
resizeEv = function() {
|
||||
clearTimeout(resize_debounce);
|
||||
resize_debounce = setTimeout(self.refresh, 100);
|
||||
}
|
||||
on('scroll', self.scroll_elem, scrollEv);
|
||||
on('resize', window, resizeEv);
|
||||
|
||||
// public methods
|
||||
self.destroy = function(clean) {
|
||||
off('scroll', self.scroll_elem, scrollEv);
|
||||
off('resize', window, resizeEv);
|
||||
self.html((clean ? self.generateEmptyRow() : rows).join(''));
|
||||
}
|
||||
self.refresh = function(force) {
|
||||
if(self.getRowsHeight(rows) || force) self.update(rows);
|
||||
}
|
||||
self.update = function(new_rows) {
|
||||
rows = isArray(new_rows)
|
||||
? new_rows
|
||||
: [];
|
||||
var scroll_top = self.scroll_elem.scrollTop;
|
||||
// fixes #39
|
||||
if(rows.length * self.options.item_height < scroll_top) {
|
||||
self.scroll_elem.scrollTop = 0;
|
||||
last_cluster = 0;
|
||||
}
|
||||
self.insertToDOM(rows, cache);
|
||||
self.scroll_elem.scrollTop = scroll_top;
|
||||
}
|
||||
self.clear = function() {
|
||||
self.update([]);
|
||||
}
|
||||
self.getRowsAmount = function() {
|
||||
return rows.length;
|
||||
}
|
||||
self.getScrollProgress = function() {
|
||||
return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
|
||||
}
|
||||
|
||||
var add = function(where, _new_rows) {
|
||||
var new_rows = isArray(_new_rows)
|
||||
? _new_rows
|
||||
: [];
|
||||
if( ! new_rows.length) return;
|
||||
rows = where == 'append'
|
||||
? rows.concat(new_rows)
|
||||
: new_rows.concat(rows);
|
||||
self.insertToDOM(rows, cache);
|
||||
}
|
||||
self.append = function(rows) {
|
||||
add('append', rows);
|
||||
}
|
||||
self.prepend = function(rows) {
|
||||
add('prepend', rows);
|
||||
}
|
||||
}
|
||||
|
||||
Clusterize.prototype = {
|
||||
constructor: Clusterize,
|
||||
// fetch existing markup
|
||||
fetchMarkup: function() {
|
||||
var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
|
||||
while (rows_nodes.length) {
|
||||
rows.push(rows_nodes.shift().outerHTML);
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
// get tag name, content tag name, tag height, calc cluster height
|
||||
exploreEnvironment: function(rows, cache) {
|
||||
var opts = this.options;
|
||||
opts.content_tag = this.content_elem.tagName.toLowerCase();
|
||||
if( ! rows.length) return;
|
||||
if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
|
||||
if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);
|
||||
if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
|
||||
this.getRowsHeight(rows);
|
||||
},
|
||||
getRowsHeight: function(rows) {
|
||||
var opts = this.options,
|
||||
prev_item_height = opts.item_height;
|
||||
opts.cluster_height = 0;
|
||||
if( ! rows.length) return;
|
||||
var nodes = this.content_elem.children;
|
||||
var node = nodes[Math.floor(nodes.length / 2)];
|
||||
opts.item_height = node.offsetHeight;
|
||||
// consider table's border-spacing
|
||||
if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
|
||||
opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
|
||||
// consider margins (and margins collapsing)
|
||||
if(opts.tag != 'tr') {
|
||||
var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
|
||||
var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
|
||||
opts.item_height += Math.max(marginTop, marginBottom);
|
||||
}
|
||||
opts.block_height = opts.item_height * opts.rows_in_block;
|
||||
opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
|
||||
opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
|
||||
return prev_item_height != opts.item_height;
|
||||
},
|
||||
// get current cluster number
|
||||
getClusterNum: function () {
|
||||
this.options.scroll_top = this.scroll_elem.scrollTop;
|
||||
return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0;
|
||||
},
|
||||
// generate empty row if no data provided
|
||||
generateEmptyRow: function() {
|
||||
var opts = this.options;
|
||||
if( ! opts.tag || ! opts.show_no_data_row) return [];
|
||||
var empty_row = document.createElement(opts.tag),
|
||||
no_data_content = document.createTextNode(opts.no_data_text), td;
|
||||
empty_row.className = opts.no_data_class;
|
||||
if(opts.tag == 'tr') {
|
||||
td = document.createElement('td');
|
||||
// fixes #53
|
||||
td.colSpan = 100;
|
||||
td.appendChild(no_data_content);
|
||||
}
|
||||
empty_row.appendChild(td || no_data_content);
|
||||
return [empty_row.outerHTML];
|
||||
},
|
||||
// generate cluster for current scroll position
|
||||
generate: function (rows, cluster_num) {
|
||||
var opts = this.options,
|
||||
rows_len = rows.length;
|
||||
if (rows_len < opts.rows_in_block) {
|
||||
return {
|
||||
top_offset: 0,
|
||||
bottom_offset: 0,
|
||||
rows_above: 0,
|
||||
rows: rows_len ? rows : this.generateEmptyRow()
|
||||
}
|
||||
}
|
||||
var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0),
|
||||
items_end = items_start + opts.rows_in_cluster,
|
||||
top_offset = Math.max(items_start * opts.item_height, 0),
|
||||
bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
|
||||
this_cluster_rows = [],
|
||||
rows_above = items_start;
|
||||
if(top_offset < 1) {
|
||||
rows_above++;
|
||||
}
|
||||
for (var i = items_start; i < items_end; i++) {
|
||||
rows[i] && this_cluster_rows.push(rows[i]);
|
||||
}
|
||||
return {
|
||||
top_offset: top_offset,
|
||||
bottom_offset: bottom_offset,
|
||||
rows_above: rows_above,
|
||||
rows: this_cluster_rows
|
||||
}
|
||||
},
|
||||
renderExtraTag: function(class_name, height) {
|
||||
var tag = document.createElement(this.options.tag),
|
||||
clusterize_prefix = 'clusterize-';
|
||||
tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
|
||||
height && (tag.style.height = height + 'px');
|
||||
return tag.outerHTML;
|
||||
},
|
||||
// if necessary verify data changed and insert to DOM
|
||||
insertToDOM: function(rows, cache) {
|
||||
// explore row's height
|
||||
if( ! this.options.cluster_height) {
|
||||
this.exploreEnvironment(rows, cache);
|
||||
}
|
||||
var data = this.generate(rows, this.getClusterNum()),
|
||||
this_cluster_rows = data.rows.join(''),
|
||||
this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
|
||||
top_offset_changed = this.checkChanges('top', data.top_offset, cache),
|
||||
only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
|
||||
callbacks = this.options.callbacks,
|
||||
layout = [];
|
||||
|
||||
if(this_cluster_content_changed || top_offset_changed) {
|
||||
if(data.top_offset) {
|
||||
this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
|
||||
layout.push(this.renderExtraTag('top-space', data.top_offset));
|
||||
}
|
||||
layout.push(this_cluster_rows);
|
||||
data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
|
||||
callbacks.clusterWillChange && callbacks.clusterWillChange();
|
||||
this.html(layout.join(''));
|
||||
this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
|
||||
callbacks.clusterChanged && callbacks.clusterChanged();
|
||||
} else if(only_bottom_offset_changed) {
|
||||
this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
|
||||
}
|
||||
},
|
||||
// unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
|
||||
html: function(data) {
|
||||
var content_elem = this.content_elem;
|
||||
if(ie && ie <= 9 && this.options.tag == 'tr') {
|
||||
var div = document.createElement('div'), last;
|
||||
div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
|
||||
while((last = content_elem.lastChild)) {
|
||||
content_elem.removeChild(last);
|
||||
}
|
||||
var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
|
||||
while (rows_nodes.length) {
|
||||
content_elem.appendChild(rows_nodes.shift());
|
||||
}
|
||||
} else {
|
||||
content_elem.innerHTML = data;
|
||||
}
|
||||
},
|
||||
getChildNodes: function(tag) {
|
||||
var child_nodes = tag.children, nodes = [];
|
||||
for (var i = 0, ii = child_nodes.length; i < ii; i++) {
|
||||
nodes.push(child_nodes[i]);
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
checkChanges: function(type, value, cache) {
|
||||
var changed = value != cache[type];
|
||||
cache[type] = value;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
// support functions
|
||||
function on(evt, element, fnc) {
|
||||
return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
|
||||
}
|
||||
function off(evt, element, fnc) {
|
||||
return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
|
||||
}
|
||||
function isArray(arr) {
|
||||
return Object.prototype.toString.call(arr) === '[object Array]';
|
||||
}
|
||||
function getStyle(prop, elem) {
|
||||
return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
|
||||
}
|
||||
|
||||
return Clusterize;
|
||||
}));
|
@ -5,7 +5,7 @@
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.customer-container {
|
||||
.cart-container {
|
||||
padding: 0 15px;
|
||||
// flex: 2;
|
||||
display: inline-block;
|
||||
|
@ -15,25 +15,26 @@ erpnext.PointOfSale = class PointOfSale {
|
||||
this.page = wrapper.page;
|
||||
|
||||
const assets = [
|
||||
'assets/frappe/js/lib/clusterize.js',
|
||||
'assets/erpnext/js/pos/clusterize.js',
|
||||
'assets/erpnext/css/pos.css'
|
||||
];
|
||||
|
||||
frappe.require(assets, () => {
|
||||
this.prepare();
|
||||
this.make();
|
||||
this.bind_events();
|
||||
this.prepare().then(() => {
|
||||
this.make();
|
||||
this.bind_events();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare() {
|
||||
this.set_online_status();
|
||||
this.prepare_menu();
|
||||
return this.get_pos_profile();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.make_dom();
|
||||
this.make_customer_field();
|
||||
this.make_cart();
|
||||
this.make_items();
|
||||
}
|
||||
@ -52,6 +53,66 @@ erpnext.PointOfSale = class PointOfSale {
|
||||
});
|
||||
}
|
||||
|
||||
make_dom() {
|
||||
this.wrapper.append(`
|
||||
<div class="pos">
|
||||
<section class="cart-container">
|
||||
|
||||
</section>
|
||||
<section class="item-container">
|
||||
|
||||
</section>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
make_cart() {
|
||||
this.cart = new erpnext.POSCart(this.wrapper.find('.cart-container'));
|
||||
}
|
||||
|
||||
make_items() {
|
||||
this.items = new erpnext.POSItems({
|
||||
wrapper: this.wrapper.find('.item-container'),
|
||||
pos_profile: this.pos_profile,
|
||||
events: {
|
||||
item_click: (item_code) => this.add_item_to_cart(item_code)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_item_to_cart(item_code) {
|
||||
const item = this.items.get(item_code);
|
||||
this.cart.add_item(item);
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
|
||||
}
|
||||
|
||||
get_pos_profile() {
|
||||
return frappe.call({
|
||||
method: 'erpnext.stock.get_item_details.get_pos_profile',
|
||||
args: {
|
||||
company: frappe.sys_defaults.company
|
||||
}
|
||||
}).then(r => {
|
||||
this.pos_profile = r.message;
|
||||
});
|
||||
}
|
||||
|
||||
make_sales_invoice_frm() {
|
||||
const dt = 'Sales Invoice';
|
||||
return new Promise(resolve => {
|
||||
frappe.model.with_doctype(dt, function() {
|
||||
const page = $('<div>');
|
||||
const frm = new _f.Frm(dt, page, false);
|
||||
const name = frappe.model.make_new_doc_and_get_name(dt, true);
|
||||
frm.refresh(name);
|
||||
resolve(frm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare_menu() {
|
||||
this.page.clear_menu();
|
||||
|
||||
@ -76,19 +137,38 @@ erpnext.PointOfSale = class PointOfSale {
|
||||
frappe.set_route('List', 'POS Profile');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.POSCart = class POSCart {
|
||||
constructor(wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
this.items = {};
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.make_dom();
|
||||
this.make_customer_field();
|
||||
}
|
||||
|
||||
make_dom() {
|
||||
this.wrapper.append(`
|
||||
<div class="pos">
|
||||
<section class="customer-container">
|
||||
<div class="customer-field">
|
||||
<div class="customer-field">
|
||||
</div>
|
||||
<div class="cart-wrapper">
|
||||
<div class="list-item-table">
|
||||
<div class="list-item list-item--head">
|
||||
<div class="list-item__content list-item__content--flex-2 text-muted">${__('Item Name')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Quantity')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Discount')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Rate')}</div>
|
||||
</div>
|
||||
<div class="cart-wrapper">
|
||||
<div class="cart-items">
|
||||
<div class="empty-state">
|
||||
<span>No Items added to cart</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="item-container">
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
@ -105,61 +185,6 @@ erpnext.PointOfSale = class PointOfSale {
|
||||
});
|
||||
}
|
||||
|
||||
make_cart() {
|
||||
this.cart = new erpnext.POSCart(this.wrapper.find('.cart-wrapper'));
|
||||
}
|
||||
|
||||
make_items() {
|
||||
this.items = new erpnext.POSItems(this.wrapper.find('.item-container'), {
|
||||
item_click: (item_code) => this.add_item_to_cart(item_code)
|
||||
});
|
||||
}
|
||||
|
||||
add_item_to_cart(item_code) {
|
||||
const item = this.items.get(item_code);
|
||||
this.cart.add_item(item);
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
|
||||
}
|
||||
|
||||
make_sales_invoice_frm() {
|
||||
const dt = 'Sales Invoice';
|
||||
frappe.model.with_doctype(dt, function() {
|
||||
const page = $('<div>');
|
||||
const frm = new _f.Frm(dt, page, false);
|
||||
const name = frappe.model.make_new_doc_and_get_name(dt, true);
|
||||
frm.refresh(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.POSCart = class POSCart {
|
||||
constructor(wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
this.items = {};
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.wrapper.append(`
|
||||
<div class="list-item-table">
|
||||
<div class="list-item list-item--head">
|
||||
<div class="list-item__content list-item__content--flex-2 text-muted">${__('Item Name')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Quantity')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Discount')}</div>
|
||||
<div class="list-item__content text-muted text-right">${__('Rate')}</div>
|
||||
</div>
|
||||
<div class="cart-items">
|
||||
<div class="empty-state">
|
||||
<span>No Items added to cart</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
add_item(item) {
|
||||
const { item_code } = item;
|
||||
const _item = this.items[item_code];
|
||||
@ -240,9 +265,11 @@ erpnext.POSCart = class POSCart {
|
||||
}
|
||||
|
||||
erpnext.POSItems = class POSItems {
|
||||
constructor(wrapper, events) {
|
||||
constructor({wrapper, pos_profile, events}) {
|
||||
this.wrapper = wrapper;
|
||||
this.pos_profile = pos_profile;
|
||||
this.items = {};
|
||||
|
||||
this.make_dom();
|
||||
this.make_fields();
|
||||
|
||||
@ -432,7 +459,7 @@ erpnext.POSItems = class POSItems {
|
||||
return template;
|
||||
}
|
||||
|
||||
get_items(start = 0, page_length = 2000) {
|
||||
get_items(start = 0, page_length = 20) {
|
||||
return new Promise(res => {
|
||||
frappe.call({
|
||||
method: "frappe.desk.reportview.get",
|
||||
|
Loading…
Reference in New Issue
Block a user