From 21fc26c2a22452c7b01547ab521625b511791a77 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 14 Aug 2017 18:25:59 +0530 Subject: [PATCH] Update cart ui from cur_frm, Add number pad --- erpnext/public/css/pos.css | 42 +++ erpnext/public/less/pos.less | 44 +++ .../page/point_of_sale/point_of_sale.js | 334 ++++++++++++------ 3 files changed, 305 insertions(+), 115 deletions(-) diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css index 399613d4970..de1e097bb88 100644 --- a/erpnext/public/css/pos.css +++ b/erpnext/public/css/pos.css @@ -23,6 +23,9 @@ width: 40%; margin-left: 15px; } +.cart-wrapper { + margin-bottom: 10px; +} .cart-wrapper .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -30,6 +33,10 @@ height: 200px; overflow: auto; } +.cart-items input { + height: 22px; + font-size: 12px; +} .fields { display: flex; } @@ -63,3 +70,38 @@ left: 50%; transform: translate(-50%, -50%); } +@keyframes yellow-fade { + 0% { + background-color: #fffce7; + } + 100% { + background-color: transparent; + } +} +.highlight { + animation: yellow-fade 1s ease-in 1; +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +.number-pad { + border-collapse: collapse; + cursor: pointer; + display: table; + margin: auto; +} +.num-row { + display: table-row; +} +.num-col { + display: table-cell; + border: 1px solid #d1d8dd; +} +.num-col > div { + width: 50px; + height: 50px; + text-align: center; + line-height: 50px; +} diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less index 9358f0a24c3..de165144f1c 100644 --- a/erpnext/public/less/pos.less +++ b/erpnext/public/less/pos.less @@ -35,6 +35,7 @@ } .cart-wrapper { + margin-bottom: 10px; .list-item__content:not(:first-child) { justify-content: flex-end; } @@ -43,6 +44,11 @@ .cart-items { height: 200px; overflow: auto; + + input { + height: 22px; + font-size: @text-medium; + } } .fields { @@ -84,4 +90,42 @@ left: 50%; transform: translate(-50%, -50%); } +} + +@keyframes yellow-fade { + 0% {background-color: @light-yellow;} + 100% {background-color: transparent;} +} + +.highlight { + animation: yellow-fade 1s ease-in 1; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +// number pad + +.number-pad { + border-collapse: collapse; + cursor: pointer; + display: table; + margin: auto; +} +.num-row { + display: table-row; +} +.num-col { + display: table-cell; + border: 1px solid @border-color; + + & > div { + width: 50px; + height: 50px; + text-align: center; + line-height: 50px; + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index c28a5bd8c6f..19eb70ee81a 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -1,3 +1,5 @@ +/* global Clusterize */ + frappe.pages['point-of-sale'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, @@ -5,11 +7,11 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { single_column: true }); - wrapper.pos = new erpnext.PointOfSale(wrapper); - cur_pos = wrapper.pos; + wrapper.pos = new PointOfSale(wrapper); + window.cur_pos = wrapper.pos; } -erpnext.PointOfSale = class PointOfSale { +class PointOfSale { constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -20,24 +22,25 @@ erpnext.PointOfSale = class PointOfSale { ]; frappe.require(assets, () => { - this.prepare().then(() => { - this.make(); - this.bind_events(); - }); + this.make(); }); } - prepare() { - this.set_online_status(); - this.prepare_menu(); - this.make_sales_invoice_frm() - return this.get_pos_profile(); - } - make() { - this.make_dom(); - this.make_cart(); - this.make_items(); + return frappe.run_serially([ + () => { + this.prepare_dom(); + this.prepare_menu(); + this.set_online_status(); + }, + () => this.make_sales_invoice_frm(), + () => this.setup_pos_profile(), + () => { + this.make_cart(); + this.make_items(); + this.bind_events(); + } + ]); } set_online_status() { @@ -54,7 +57,7 @@ erpnext.PointOfSale = class PointOfSale { }); } - make_dom() { + prepare_dom() { this.wrapper.append(`
@@ -68,29 +71,64 @@ erpnext.PointOfSale = class PointOfSale { } 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, + this.cart = new POSCart({ + wrapper: this.wrapper.find('.cart-container'), events: { - item_click: (item_code) => this.add_item_to_cart(item_code) + customer_change: (customer) => this.cur_frm.set_value('customer', customer), + increase_qty: (item_code) => { + this.add_item_to_cart(item_code); + }, + decrease_qty: (item_code) => { + this.add_item_to_cart(item_code, -1); + } } }); } - add_item_to_cart(item_code) { - const item = this.items.get(item_code); - this.cart.add_item(item); + make_items() { + this.items = new POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + item_click: (item_code) => { + if(!this.cur_frm.doc.customer) { + frappe.throw(__('Please select a customer')); + } + this.add_item_to_cart(item_code); + } + } + }); + } + + add_item_to_cart(item_code, qty = 1) { + + if(this.cart.exists(item_code)) { + // increase qty by 1 + this.cur_frm.doc.items.forEach((item) => { + if (item.item_code === item_code) { + frappe.model.set_value(item.doctype, item.name, 'qty', item.qty + qty); + // update cart + this.cart.add_item(item); + } + }); + return; + } + + // add to cur_frm + const item = this.cur_frm.add_child('items', { item_code: item_code }); + this.cur_frm.script_manager + .trigger('item_code', item.doctype, item.name) + .then(() => { + // update cart + this.cart.add_item(item); + }); } bind_events() { } - get_pos_profile() { + setup_pos_profile() { return frappe.call({ method: 'erpnext.stock.get_item_details.get_pos_profile', args: { @@ -104,13 +142,14 @@ erpnext.PointOfSale = class PointOfSale { make_sales_invoice_frm() { const dt = 'Sales Invoice'; return new Promise(resolve => { - frappe.model.with_doctype(dt, function() { + frappe.model.with_doctype(dt, () => { const page = $('
'); const frm = new _f.Frm(dt, page, false); const name = frappe.model.make_new_doc_and_get_name(dt, true); frm.refresh(name); frm.doc.items = []; - resolve(frm); + this.cur_frm = frm; + resolve(); }); }); } @@ -141,16 +180,18 @@ erpnext.PointOfSale = class PointOfSale { } } -erpnext.POSCart = class POSCart { - constructor(wrapper) { +class POSCart { + constructor({wrapper, events}) { this.wrapper = wrapper; - this.items = {}; + this.events = events; this.make(); + this.bind_events(); } make() { this.make_dom(); this.make_customer_field(); + this.make_numpad(); } make_dom() { @@ -172,7 +213,10 @@ erpnext.POSCart = class POSCart {
+
+
`); + this.$cart_items = this.wrapper.find('.cart-items'); } make_customer_field() { @@ -181,8 +225,9 @@ erpnext.POSCart = class POSCart { fieldtype: 'Link', label: 'Customer', options: 'Customer', + reqd: 1, onchange: (e) => { - cur_frm.set_value('customer', this.customer_field.value); + this.events.customer_change.apply(null, [this.customer_field.get_value()]); } }, parent: this.wrapper.find('.customer-field'), @@ -190,96 +235,113 @@ erpnext.POSCart = class POSCart { }); } - add_item(item) { - const { item_code } = item; - const _item = this.items[item_code]; - - if (_item) { - // exists, increase quantity - _item.quantity += 1; - this.update_quantity(_item); - } else { - // add it to this.items - item['qty'] = 1; - this.child = cur_frm.add_child('items', item) - cur_frm.script_manager.trigger("item_code", this.child.doctype, this.child.name); - - const _item = { - doc: item, - quantity: 1, - discount: 2, - rate: 2 + make_numpad() { + this.numpad = new NumberPad({ + wrapper: this.wrapper.find('.number-pad-container'), + onclick: (btn_value) => { + // on click + console.log(btn_value); } - Object.assign(this.items, { - [item_code]: _item - }); - this.add_item_to_cart(_item); - } + }); } - add_item_to_cart(item) { + add_item(item) { this.wrapper.find('.cart-items .empty-state').hide(); - const $item = $(this.get_item_html(item)) - $item.appendTo(this.wrapper.find('.cart-items')); - // $item.addClass('added'); - // this.wrapper.find('.cart-items').append(this.get_item_html(item)) - } - update_quantity(item) { - this.wrapper.find(`.list-item[data-item-name="${item.doc.item_code}"] .quantity`) - .text(item.quantity); - - $.each(cur_frm.doc["items"] || [], function(i, d) { - if (d.item_code == item.doc.item_code) { - frappe.model.set_value(d.doctype, d.name, "qty", d.qty + 1); - } - }); - } - - remove_item(item_code) { - delete this.items[item_code]; - - // this.refresh(); - } - - refresh() { - const item_codes = Object.keys(this.items); - const html = item_codes - .map(item_code => this.get_item_html(item_code)) - .join(""); - this.wrapper.find('.cart-items').html(html); - } - - get_item_html(_item) { - - let item; - if (typeof _item === "object") { - item = _item; - } - else if (typeof _item === "string") { - item = this.items[_item]; + if (this.exists(item.item_code)) { + // update quantity + this.update_item(item); + } else { + // add to cart + const $item = $(this.get_item_html(item)); + $item.appendTo(this.$cart_items); } + this.highlight_item(item.item_code); + this.scroll_to_item(item.item_code); + } + update_item(item) { + const $item = this.$cart_items.find(`[data-item-code="${item.item_code}"]`); + if(item.qty > 0) { + $item.find('.quantity input').val(item.qty); + $item.find('.discount').text(item.discount_percentage); + $item.find('.rate').text(item.rate); + } else { + $item.remove(); + } + } + + exists(item_code) { + let $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + return $item.length > 0; + } + + highlight_item(item_code) { + const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + $item.addClass('highlight'); + setTimeout(() => $item.removeClass('highlight'), 1000); + } + + scroll_to_item(item_code) { + const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + this.$cart_items.animate({ scrollTop }); + } + + get_item_html(item) { return ` -
+
- ${item.doc.item_name} + ${item.item_name}
- ${item.quantity} + ${get_quantity_html(item.qty)}
- ${item.discount} + ${item.discount_percentage}%
${item.rate}
`; + + function get_quantity_html(value) { + return ` +
+ + + + + + + + + +
+ `; + } + } + + bind_events() { + const events = this.events; + this.$cart_items.on('click', + '[data-action="increment"], [data-action="decrement"]', function() { + const $btn = $(this); + const $item = $btn.closest('.list-item[data-item-code]'); + const item_code = $item.attr('data-item-code'); + const action = $btn.attr('data-action'); + + if(action === 'increment') { + events.increase_qty(item_code); + } else if(action === 'decrement') { + events.decrease_qty(item_code); + } + }); } } -erpnext.POSItems = class POSItems { +class POSItems { constructor({wrapper, pos_profile, events}) { this.wrapper = wrapper; this.pos_profile = pos_profile; @@ -439,16 +501,10 @@ erpnext.POSItems = class POSItems {
- ${!item_image ? - ` + ${!item_image ? ` ${frappe.get_abbr(item_title)} - ` : - '' - } - ${item_image ? - `${item_title}` : - '' - } + ` : '' } + ${item_image ? `${item_title}` : '' }
${item_price} @@ -509,12 +565,12 @@ erpnext.POSItems = class POSItems { "`tabItem`.`end_of_life`", "`tabItem`.`total_projected_qty`" ], + filters: [['disabled', '=', '0']], order_by: "`tabItem`.`modified` desc", page_length: page_length, start: start } - }) - .then(r => { + }).then(r => { const data = r.message; const items = frappe.utils.dict(data.keys, data.values); @@ -528,4 +584,52 @@ erpnext.POSItems = class POSItems { }); }); } +} + +class NumberPad { + constructor({wrapper, onclick}) { + this.wrapper = wrapper; + this.onclick = onclick; + this.make_dom(); + this.bind_events(); + } + + make_dom() { + const button_array = [ + [1, 2, 3, 'Qty'], + [4, 5, 6, 'Disc'], + [7, 8, 9, 'Price'], + ['Del', 0, '.', 'Pay'] + ]; + + this.wrapper.html(` +
+ ${button_array.map(get_row).join("")} +
+ `); + + function get_row(row) { + return '
' + row.map(get_col).join("") + '
'; + } + + function get_col(col) { + return `
${col}
`; + } + } + + bind_events() { + // bind click event + const me = this; + this.wrapper.on('click', '.num-col', function() { + const $btn = $(this); + me.highlight_button($btn); + me.onclick.apply(null, [$btn.attr('data-value')]); + }); + } + + highlight_button($btn) { + // const $btn = this.wrapper.find(`[data-value="${value}"]`); + $btn.addClass('highlight'); + setTimeout(() => $btn.removeClass('highlight'), 1000); + } } \ No newline at end of file