From 643503269e528281f0054450f86a48a8bb8b58f0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 2 Mar 2017 06:50:47 -0300 Subject: [PATCH] add Article and Category models --- kfet/static/kfet/js/kfet.api.js | 103 +++++++++ kfet/templates/kfet/kpsul.html | 397 ++++++++++++++++++++++++++++++++ 2 files changed, 500 insertions(+) diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 7836fcb9..21a67c9c 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -449,6 +449,79 @@ class Statement extends ModelObject { } +/** + * ArticleCategory model. Cannot be accessed through API. + * @extends Models.ModelObject + * @memberof Models + */ +class ArticleCategory extends ModelObject { + + /** + * Properties associated to a category + * @default ['id', 'name'] + * @see {@link Models.ModelObject.props|ModelObject.props} + */ + static get props() { + return ['id', 'name']; + } + + /** + * Default values for ArticleCategory model instances. + * @default { 'id': 0, 'name': '' } + * @see {@link Models.ModelObject.default_data|ModelObject.default_data} + */ + static get default_data() { + return {'id': 0, 'name': ''}; + } + + /** + * @default {@link Formatters.ArticleCategoryFormatter} + */ + formatter() { + return ArticleCategoryFormatter; + } +} + +/** + * Article model. Cannot be accessed through API. + * @extends Models.ModelObject + * @memberof Models + */ +class Article extends ModelObject { + /** + * Properties associated to an article + * @default ['id', 'name'] + * @see {@link Models.ModelObject.props|ModelObject.props} + */ + static get props() { + return ['id', 'name', 'price', 'stock', 'category']; + } + + /** + * Default values for Article model instances. + * @default { 'id': 0, 'name': '', 'price': 0, 'stock': 0, + * 'category': new ArticleCategory() } + * @see {@link Models.ModelObject.default_data|ModelObject.default_data} + */ + static get default_data() { + return { + 'id': 0, 'name': '', 'price': 0, 'stock': 0, + 'category': new ArticleCategory(), + }; + } + + /** + * @default {@link Formatters.ArticleCategoryFormatter} + */ + formatter() { + return ArticleFormatter; + } + + // Take care of 'price' type + // API currently returns a string object (serialization of Decimal type within Django) + get price() { return this._price; } + set price(v) { this._price = floatCheck(v); } +} /* ---------- ---------- */ @@ -683,3 +756,33 @@ class StatementFormatter extends Formatter { } } + + +/** + * @memberof Formatters + * @extends Formatters.Formatter + */ + +class ArticleCategoryFormatter extends Formatter { + + /** + * Properties renderable to html. + * @default {@link Models.Statement.props} + */ + static get props() { + return ArticleCategory.props; + } + + static get _data_stock() { + return { + 'default': '', 'low': 'low', + 'ok': 'ok', 'neg': 'neg', + }; + } + + static attr_data_stock(a) { + if (a.stock >= 5) { return this._data_stock.ok; } + else if (a.stock >= -5) { return this._data_stock.low; } + else /* a.stock < -5 */ { return this._data_stock.neg; } + } +} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 7fc56605..6454afa9 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -189,6 +189,403 @@ function booleanCheck(v) { return v == true; } +function functionCheck(v) { + if (typeof v === 'function') + return v; + return function(){}; +} + +function restrict(obj, attr_list) { + var restricted = {} ; + for (let attr of attr_list) { + restricted[attr] = obj[attr] ; + } + return restricted ; +} + +class ArticleCategory { + + constructor(id, name) { + this.id = id; + this.name = name; + } + + get id() { return this._id; } + + set id(v) { this._id = intCheck(v); } + + html(template) { + var $template = $(template) + $template.attr('id', 'data-category-'+this.id); + $template.find('td').text(this.name); + return $template ; + } + + static compare(a, b) { + return a.name.localeCompare(b.name) ; + } +} + +class Article { + + constructor () { + $.extend(this, this.constructor.default_data); + } + + static get default_data() { + return { + 'id': 0, 'name': '', 'price': 0, 'stock': 0, + 'category': new ArticleCategory(), + }; + } + + get id() { return this._id; } + get price() { return this._price; } + get stock() { return this._stock; } + + set id(v) { this._id = intCheck(v); } + set price(v) { this._price = floatCheck(v); } + set stock(v) { this._stock = intCheck(v); } + + get str_price_ukf() { + return amountToUKF(this.price, false); + } + + static get _data_stock_ok() { return ''; } + static get _data_stock_low() { return 'low'; } + static get _data_stock_neg() { return 'neg'; } + + get data_stock() { + var stock = this.stock ; + if (stock >= 5) { return this.constructor._data_stock_ok; } + else if (stock >= -5) { return this.constructor._data_stock_low; } + else /* stock < -5 */ { return this.constructor._data_stock_neg; } + } + + from(data) { + $.extend(this, this.constructor.default_data, data); + } + + html(template) { + var $template = $(template); + $template + .find('.name').text(this.name).end() + .find('.price').text(this.str_price_ukf).end() + .find('.stock').text(this.stock).end(); + $template.attr('id', 'data-article-'+this.id); + $template.attr('data-stock', this.data_stock); + return $template ; + } + + static compare(a, b) { + return a.name.localeCompare(b.name) ; + } + + reset() { + this.from({}); + } +} + +class ArticleList { + constructor() { + this.articles = []; + this.categories = []; + } + + get_or_create(id, name) { + var category = this.categories.find(c => c.id === id); + if (!category) { + category = new ArticleCategory(id, name); + this.categories.push(category); + } + return category; + } + + from(data, callback) { + callback = functionCheck(callback); + + for (let article_data of data['articles']) { + var category = this.get_or_create(article_data['category_id'], article_data['category__name']) ; + article_data = restrict(article_data, ['name', 'price', 'stock', 'id']) ; + article_data['category'] = category ; + + var article = new Article() ; + article.from(article_data) ; + this.articles.push(article) ; + } + + callback() ; + } + + fromAPI(on_success, on_error) { + on_error = functionCheck(on_error); + var that = this ; + $.ajax({ + dataType: "json", + url : "{% url 'kfet.kpsul.articles_data' %}", + method : "GET", + }) + .done((data) => this.from(data, on_success)) + .fail(on_error); + } + + display($container, $article_template, $category_template) { + this.categories.sort(ArticleCategory.compare) ; + for (let cat of this.categories) { + var cat_articles = this.articles.filter(a => a.category.id === cat.id) ; + cat_articles.sort(Article.compare); + + $container.append(cat.html($category_template)) ; + for (let art of cat_articles) { + $container.append(art.html($article_template)) ; + } + } + } + + clear() { + this.articles = []; + this.categories = [] ; + } + + find_by_id(name) { + return this.articles.find(function(a) { + return a.name === name ; + }); + } + + get_from_elt($elt) { + var id = $elt.attr('id').split('-')[2]; + + return this.articles.find(function(article) { + return (article.id == id) ; + }); + } +} + +class ArticleManager { + + constructor(env) { + this._env = env; // Global K-Psul Manager + + this._$container = $('#articles_data tbody'); + this._$input = $('#article_autocomplete'); + this._$nb = $('#article_number'); + this._category_template = ''; + this._article_template = ''; + + this.selected = new Article() ; + this.list = new ArticleList() ; + this.autocomplete = new ArticleAutocomplete(this); + + this._init_events(); + } + + get nb() { + return this._$nb.val() ; + } + + display_list() { + this.list.display(this._$container, this._article_template, this._category_template) ; + } + + validate(article) { + this.selected.from(article) ; + this._$input.val(article.name); + this._$nb.val('1'); + this._$nb.focus().select(); + } + + unset() { + this.selected.reset(); + } + + is_empty() { + return this.selected.id == 0 ; + } + + reset_data() { + this._$container.find('tr').remove(); + this.list.clear(); + this.list.fromAPI(this.display_list.bind(this)) ; + } + + update_data(data) { + for (let article_dict of data) { + var article = this.list.articles.find(function(art) { + return (art.id == article_dict['id']); + }); + + // For now, article additions are disregarded + if (article) { + article.stock = article_dict['stock']; + this._$container.find('#data-article-'+article_dict['id']+' .stock') + .text(article_dict['stock']); + } + } + } + + reset() { + this.unset() ; + this._$nb.val(''); + this._$input.val(''); + this.autocomplete.showAll() ; + } + + _init_events() { + var that = this; + + // 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12 + var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/; + var arrowKeys = /^(37|38|39|40)$/; + + //Global input event (to merge ?) + this._$input.on('keydown', function(e) { + if (e.keyCode == 13 && that._$input.val() == '') { + that._env.performOperations(); + } + }); + + this._$container.on('click', '.article', function() { + var article = that.list.get_from_elt($(this)) ; + that.validate(article); + }); + + this._$nb.on('keydown', function(e) { + if (e.keyCode == 13 && ArticleManager.check_nb(that.nb) && !that.is_empty()) { + that._env.addPurchase(that.selected, that.nb); + that.reset(); + that.focus(); + } + if (normalKeys.test(e.keyCode) || arrowKeys.test(e.KeyCode) || e.ctrlKey) { + if (e.ctrlKey && e.charCode == 65) + that._$nb.val(''); + return true ; + } + if (ArticleManager.check_nb(that.nb+e.key)) + return true; + return false; + + }); + } + + focus() { + this._$input.focus(); + return this; + } + + static check_nb(nb) { + return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24 ; + } +} + +class ArticleAutocomplete { + + constructor(article_manager) { + this.manager = article_manager; + this.matching = []; + this.active_categories = []; + this._$container = article_manager._$container ; + this._$input = $('#article_autocomplete'); + + this.showAll() ; + this._init_events(); + } + + _init_events() { + var that = this ; + + // 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12 + var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/; + + this._$input + .on('keydown', function(e) { + var text = that._$input.val() ; + if (normalKeys.test(e.keyCode) || e.ctrlKey) { + // For the backspace key, we suppose the cursor is at the very end + if(e.keyCode == 8) { + that.update(text.substring(0, text.length-1), true); + } + return true ; + } + that.update(text+e.key, false); + + return false ; + + }); + + } + + update(prefix, backspace) { + + var article_list = this.manager.list ; + var lower = prefix.toLowerCase() ; + var that = this ; + this.matching = article_list.articles.filter(function(article) { + return article.name.toLowerCase() + .indexOf(lower) === 0 ; + }); + + this.active_categories = article_list.categories.filter(function(category) { + return that.matching.find(function(article) { + return article.category === category ; + }); + }); + + if (this.matching.length == 1) { + if (!backspace) { + this.manager.validate(this.matching[0]) ; + this.showAll() ; + } else { + this.manager.unset(); + this.updateDisplay(); + } + } else if (this.matching.length > 1) { + this.manager.unset(); + this.updateDisplay() ; + if (!backspace) + this.updatePrefix(); + } + } + + updateDisplay() { + var article_list = this.manager.list ; + for (let article of article_list.articles) { + if (this.matching.indexOf(article) > -1) { + this._$container.find('#data-article-'+article.id).show(); + } else { + this._$container.find('#data-article-'+article.id).hide(); + } + } + + for (let category of article_list.categories) { + if (this.active_categories.indexOf(category) > -1) { + this._$container.find('#data-category-'+category.id).show(); + } else { + this._$container.find('#data-category-'+category.id).hide(); + } + } + } + + updatePrefix() { + var lower = this.matching.map(function (article) { + return article.name.toLowerCase() ; + }); + + lower.sort() ; + var first = lower[0], last = lower[lower.length-1], + length = first.length, i = 0; + while (i < length && first.charAt(i) === last.charAt(i)) i++; + + this._$input.val(first.substring(0,i)) ; + } + + showAll() { + this.matching = this.manager.list.articles; + this.active_categories = this.manager.list.categories; + this.updateDisplay(); + } +} + $(document).ready(function() { 'use strict';