diff --git a/app/controllers/stockit_controller.rb b/app/controllers/stockit_controller.rb index 475ac3a3..85c027ea 100644 --- a/app/controllers/stockit_controller.rb +++ b/app/controllers/stockit_controller.rb @@ -4,30 +4,62 @@ class StockitController < ApplicationController @stock_articles = StockArticle.undeleted.includes(:supplier, :article_category). order('suppliers.name, article_categories.name, articles.name') end + + def index_on_stock_article_create # See publish/subscribe design pattern in /doc. + @stock_article = StockArticle.find(params[:id]) + + render :layout => false + end + def index_on_stock_article_update # See publish/subscribe design pattern in /doc. + @stock_article = StockArticle.find(params[:id]) + + render :layout => false + end + + # three possibilites to fill a new_stock_article form + # (1) start from blank or use params def new - @stock_article = StockArticle.new + @stock_article = StockArticle.new(params[:stock_article]) + + render :layout => false + end + + # (2) StockArticle as template + def copy + @stock_article = StockArticle.find(params[:stock_article_id]).dup + + render :layout => false + end + + # (3) non-stock Article as template + def derive + @stock_article = Article.find(params[:old_article_id]).becomes(StockArticle).dup + + render :layout => false end def create @stock_article = StockArticle.new(params[:stock_article]) - if @stock_article.save - redirect_to stock_articles_path, :notice => I18n.t('stockit.stock_create.notice') + if @stock_article.valid? and @stock_article.save + render :layout => false else - render :action => 'new' + render :action => 'new', :layout => false end end def edit @stock_article = StockArticle.find(params[:id]) + + render :layout => false end def update @stock_article = StockArticle.find(params[:id]) if @stock_article.update_attributes(params[:stock_article]) - redirect_to stock_articles_path, :notice => I18n.t('stockit.stock_update.notice') + render :layout => false else - render :action => 'edit' + render :action => 'edit', :layout => false end end @@ -36,9 +68,15 @@ class StockitController < ApplicationController @stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC') end + def show_on_stock_article_update # See publish/subscribe design pattern in /doc. + @stock_article = StockArticle.find(params[:id]) + + render :layout => false + end + def destroy - @article = StockArticle.find(params[:id]) - @article.mark_as_deleted + @stock_article = StockArticle.find(params[:id]) + @stock_article.mark_as_deleted render :layout => false rescue => error render :partial => "destroy_fail", :layout => false, diff --git a/app/helpers/stockit_helper.rb b/app/helpers/stockit_helper.rb index f6a0a1fc..aee6708e 100644 --- a/app/helpers/stockit_helper.rb +++ b/app/helpers/stockit_helper.rb @@ -14,4 +14,13 @@ module StockitHelper link_to t('.stock_taking'), stock_taking_path(stock_change.stock_taking) end end + + def stock_article_price_hint(stock_article) + t('simple_form.hints.stock_article.edit_stock_article.price', + :stock_article_copy_link => link_to(t('.copy_stock_article'), + stock_article_copy_path(stock_article), + :remote => true + ) + ) + end end diff --git a/app/views/stockit/_form.html.haml b/app/views/stockit/_form.html.haml index 32e58623..1e56b2c2 100644 --- a/app/views/stockit/_form.html.haml +++ b/app/views/stockit/_form.html.haml @@ -1,18 +1,21 @@ -= simple_form_for stock_article, :validate => true do |f| - = f.association :supplier - = f.input :name - = f.input :unit - = f.input :note - - - if stock_article.new_record? - = f.input :price - = f.input :tax, :wrapper => :append do - = f.input_field :tax - %span.add-on % - = f.input :deposit - - else - = f.input :price, :input_html => {:disabled => 'disabled'}, :hint => t('.price_hint') - = f.association :article_category - .form-actions - = f.submit class: 'btn' - = link_to t('ui.or_cancel'), stock_articles_path += simple_form_for stock_article, remote: true, :validate => true do |f| + .modal-header + = link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'} + %h3= title + .modal-body + = f.association :supplier + = f.input :name + = f.input :unit + = f.input :note + - if stock_article.new_record? + = f.input :price + = f.input :tax, :wrapper => :append do + = f.input_field :tax + %span.add-on % + = f.input :deposit + - else + = f.input :price, :input_html => {:disabled => 'disabled'}, :hint => stock_article_price_hint(stock_article).html_safe + = f.association :article_category + .modal-footer + = link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'} + = f.submit :class => 'btn btn-primary', 'data-disable-with' => t('ui.please_wait') diff --git a/app/views/stockit/_stock_article.html.haml b/app/views/stockit/_stock_article.html.haml new file mode 100644 index 00000000..c2eb3566 --- /dev/null +++ b/app/views/stockit/_stock_article.html.haml @@ -0,0 +1,14 @@ +%tr{:class => stock_article_classes(stock_article), :id => "stockArticle-#{stock_article.id}"} + %td= link_to stock_article.name, stock_article + %td= stock_article.quantity + %td= stock_article.quantity - stock_article.quantity_available + %th= stock_article.quantity_available + %td= stock_article.unit + %td= stock_article.price + %td= number_to_percentage stock_article.tax + %td= link_to stock_article.supplier.name, stock_article.supplier + %td= stock_article.article_category.name + %td + = link_to t('ui.edit'), edit_stock_article_path(stock_article), remote: true, class: 'btn btn-mini' + = link_to t('ui.delete'), stock_article, :method => :delete, :confirm => t('.confirm_delete', :name => stock_article.name), + class: 'btn btn-mini btn-danger', :remote => true diff --git a/app/views/stockit/_stock_article_details.html.haml b/app/views/stockit/_stock_article_details.html.haml new file mode 100644 index 00000000..617799e7 --- /dev/null +++ b/app/views/stockit/_stock_article_details.html.haml @@ -0,0 +1,26 @@ +#stockArticleDetails + %dl.dl-horizontal + %dt= heading_helper(StockArticle, :supplier) + %dd= link_to stock_article.supplier.name, stock_article.supplier + %dt= heading_helper(StockArticle, :name) + %dd= stock_article.name + %dt= heading_helper(StockArticle, :unit) + %dd= stock_article.unit + %dt= heading_helper(StockArticle, :price) + %dd= number_to_currency stock_article.price + %dt= heading_helper(StockArticle, :tax) + %dd= number_to_percentage stock_article.tax + %dt= heading_helper(StockArticle, :deposit) + %dd= number_to_currency stock_article.deposit + %dt= heading_helper(StockArticle, :fc_price) + %dd= number_to_currency stock_article.fc_price + %dt= heading_helper(StockArticle, :article_category) + %dd= stock_article.article_category.name + %dt= heading_helper(StockArticle, :note) + %dd= stock_article.note + %dt= heading_helper(StockArticle, :quantity) + %dd= stock_article.quantity + %dt= heading_helper(StockArticle, :quantity_available) + %dd= stock_article.quantity_available + .form-actions + = link_to t('ui.edit'), edit_stock_article_path(stock_article), remote: true, class: 'btn' diff --git a/app/views/stockit/copy.js.erb b/app/views/stockit/copy.js.erb new file mode 100644 index 00000000..e3bc3321 --- /dev/null +++ b/app/views/stockit/copy.js.erb @@ -0,0 +1,9 @@ +$('#modalContainer').html('<%= j(render( + :partial => "form", + :locals => { + :title => t('.title'), + :stock_article => @stock_article + } +)) %>'); + +$('#modalContainer').modal(); diff --git a/app/views/stockit/create.js.erb b/app/views/stockit/create.js.erb new file mode 100644 index 00000000..aa75b2dc --- /dev/null +++ b/app/views/stockit/create.js.erb @@ -0,0 +1,15 @@ +$('div.container-fluid').prepend('<%= j(render( + :partial => 'shared/alert_success', + :locals => { + :alert_message => t('.notice', :name => @stock_article.name) + } +)) %>'); + +// Publish database changes. +// See publish/subscribe design pattern in /doc. +$(document).trigger({ + type: 'StockArticle#create', + stock_article_id: <%= @stock_article.id %> +}); + +$('#modalContainer').modal('hide'); diff --git a/app/views/stockit/destroy.js.erb b/app/views/stockit/destroy.js.erb new file mode 100644 index 00000000..7bba9dbf --- /dev/null +++ b/app/views/stockit/destroy.js.erb @@ -0,0 +1,13 @@ +$('div.container-fluid').prepend('<%= j(render( + :partial => 'shared/alert_success', + :locals => { + :alert_message => t('.notice', :name => @stock_article.name) + } +)) %>'); + +// Publish database changes. +// See publish/subscribe design pattern in /doc. +$(document).trigger({ + type: 'StockArticle#destroy', + stock_article_id: <%= @stock_article.id %> +}); diff --git a/app/views/stockit/destroy.js.haml b/app/views/stockit/destroy.js.haml deleted file mode 100644 index 8bbde9ed..00000000 --- a/app/views/stockit/destroy.js.haml +++ /dev/null @@ -1,8 +0,0 @@ --# please polish the following line if you know how, same in partial _destroy_fail -var successDiv = $('
#{escape_javascript(t('ui.marks.close').html_safe)}
'); - -successDiv.append(document.createTextNode('#{escape_javascript(t('.notice', name: @article.name))}')); -$('div.container-fluid').prepend(successDiv); - -$('#stockArticle-#{@article.id}').remove(); --# WARNING: Do not use a simple .fadeOut() above, because it conflicts with the show/hide function of unavailable articles. diff --git a/app/views/stockit/edit.html.haml b/app/views/stockit/edit.html.haml deleted file mode 100644 index bdf498c6..00000000 --- a/app/views/stockit/edit.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- title t('.title') - -= render :partial => 'form', :locals => {:stock_article => @stock_article} diff --git a/app/views/stockit/edit.js.erb b/app/views/stockit/edit.js.erb new file mode 100644 index 00000000..e3bc3321 --- /dev/null +++ b/app/views/stockit/edit.js.erb @@ -0,0 +1,9 @@ +$('#modalContainer').html('<%= j(render( + :partial => "form", + :locals => { + :title => t('.title'), + :stock_article => @stock_article + } +)) %>'); + +$('#modalContainer').modal(); diff --git a/app/views/stockit/index.html.haml b/app/views/stockit/index.html.haml index c2899d9e..0fbd2722 100644 --- a/app/views/stockit/index.html.haml +++ b/app/views/stockit/index.html.haml @@ -3,6 +3,30 @@ :javascript $(function() { $('tr.unavailable').hide(); + + // Subscribe to database changes. + // See publish/subscribe design pattern in /doc. + $(document).on('StockArticle#create', function(e) { + $.ajax({ + url: '#{index_on_stock_article_create_stock_articles_path}', + type: 'get', + data: {id: e.stock_article_id}, + contentType: 'application/json; charset=UTF-8' + }); + }); + + $(document).on('StockArticle#destroy', function(e) { + $('#stockArticle-' + e.stock_article_id).remove(); + }); + + $(document).on('StockArticle#update', function(e) { + $.ajax({ + url: '#{index_on_stock_article_update_stock_articles_path}', + type: 'get', + data: {id: e.stock_article_id}, + contentType: 'application/json; charset=UTF-8' + }); + }); }) .well.well-small @@ -17,7 +41,7 @@ .btn-group = link_to_if @current_user.role_orders?, t('.order_online'), new_order_path(supplier_id: 0), class: 'btn', class: 'btn btn-primary' - = link_to t('.new_stock_article'), new_stock_article_path, class: 'btn' + = link_to t('.new_stock_article'), new_stock_article_path, remote: true, class: 'btn' = link_to t('.new_stock_taking'), new_stock_taking_path, class: 'btn' = link_to t('.show_stock_takings'), stock_takings_path, class: 'btn' @@ -42,22 +66,9 @@ %th= t '.article.supplier' %th= t '.article.category' %th - %tbody - - for article in @stock_articles - %tr{:class => stock_article_classes(article), :id => "stockArticle-#{article.id}"} - %td= link_to article.name, article - %td= article.quantity - %td= article.quantity - article.quantity_available - %th= article.quantity_available - %td= article.unit - %td= article.price - %td= number_to_percentage article.tax - %td= link_to article.supplier.name, article.supplier - %td= article.article_category.name - %td - = link_to t('ui.edit'), edit_stock_article_path(article), class: 'btn btn-mini' - = link_to t('ui.delete'), article, :method => :delete, :confirm => t('.confirm_delete'), - class: 'btn btn-mini btn-danger', :remote => true + %tbody#articles-tbody + - for stock_article in @stock_articles + = render :partial => 'stock_article', :locals => {:stock_article => stock_article} %p = t '.stock_worth' = number_to_currency StockArticle.stock_value diff --git a/app/views/stockit/index_on_stock_article_create.js.erb b/app/views/stockit/index_on_stock_article_create.js.erb new file mode 100644 index 00000000..eb48001b --- /dev/null +++ b/app/views/stockit/index_on_stock_article_create.js.erb @@ -0,0 +1,12 @@ +// Handle more advanced DOM update after AJAX database manipulation. +// See publish/subscribe design pattern in /doc. +(function() { + var stock_article_row = $('<%= j(render( + :partial => 'stock_article', + :locals => { + :stock_article => @stock_article + } + )) %>'); + + $('#articles-tbody').prepend(stock_article_row); +})(); diff --git a/app/views/stockit/index_on_stock_article_update.js.erb b/app/views/stockit/index_on_stock_article_update.js.erb new file mode 100644 index 00000000..6e43db8e --- /dev/null +++ b/app/views/stockit/index_on_stock_article_update.js.erb @@ -0,0 +1,12 @@ +// Handle more advanced DOM update after AJAX database manipulation. +// See publish/subscribe design pattern in /doc. +(function() { + var stock_article_row = $('<%= j(render( + :partial => 'stock_article', + :locals => { + :stock_article => @stock_article + } + )) %>'); + + $('#stockArticle-<%= @stock_article.id %>').replaceWith(stock_article_row); +})(); diff --git a/app/views/stockit/new.html.haml b/app/views/stockit/new.html.haml deleted file mode 100644 index 05609a84..00000000 --- a/app/views/stockit/new.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -- title t('.title') - -- content_for :head do - :javascript - $(function() { - $('#article_search').autocomplete({ - source: '#{articles_search_stock_articles_path}', - select: function(e, ui) { - alert(ui.item.value); - //location.href = '#{nil}' + ui.item.value; - } - }); - }) - - -/ - TODO: Fix this - %p - = t '.search_text' - = text_field_tag 'article_search' -#stock_article_form - = render :partial => 'form', :locals => {:stock_article => @stock_article} diff --git a/app/views/stockit/new.js.erb b/app/views/stockit/new.js.erb new file mode 100644 index 00000000..e3bc3321 --- /dev/null +++ b/app/views/stockit/new.js.erb @@ -0,0 +1,9 @@ +$('#modalContainer').html('<%= j(render( + :partial => "form", + :locals => { + :title => t('.title'), + :stock_article => @stock_article + } +)) %>'); + +$('#modalContainer').modal(); diff --git a/app/views/stockit/show.html.haml b/app/views/stockit/show.html.haml index 5f416bf9..077b6e3a 100644 --- a/app/views/stockit/show.html.haml +++ b/app/views/stockit/show.html.haml @@ -1,32 +1,22 @@ - title @stock_article.name +- content_for :javascript do + :javascript + $(function() { + // Subscribe to database changes. + // See publish/subscribe design pattern in /doc. + $(document).on('StockArticle#update', function(e) { + $.ajax({ + url: '#{show_on_stock_article_update_stock_articles_path}', + type: 'get', + data: {id: e.stock_article_id}, + contentType: 'application/json; charset=UTF-8' + }); + }); + }); .row-fluid .span6 - %dl.dl-horizontal - %dt= StockArticle.human_attribute_name 'supplier' - %dd= link_to @stock_article.supplier.name, @stock_article.supplier - %dt= StockArticle.human_attribute_name 'name' - %dd= @stock_article.name - %dt= StockArticle.human_attribute_name 'unit' - %dd= @stock_article.unit - %dt= StockArticle.human_attribute_name 'price' - %dd= number_to_currency @stock_article.price - %dt= StockArticle.human_attribute_name 'tax' - %dd= number_to_percentage @stock_article.tax - %dt= StockArticle.human_attribute_name 'deposit' - %dd= number_to_currency @stock_article.deposit - %dt= StockArticle.human_attribute_name 'fc_price' - %dd= number_to_currency @stock_article.fc_price - %dt= StockArticle.human_attribute_name 'article_category' - %dd= @stock_article.article_category.name - %dt= StockArticle.human_attribute_name 'note' - %dd= @stock_article.note - %dt= StockArticle.human_attribute_name 'quantity' - %dd= @stock_article.quantity - %dt= StockArticle.human_attribute_name 'quantity_available' - %dd= @stock_article.quantity_available - .form-actions - = link_to t('ui.edit'), edit_stock_article_path(@stock_article), class: 'btn' + = render :partial => 'stock_article_details', :locals => {:stock_article => @stock_article} .span6 %h2= t('.stock_changes') diff --git a/app/views/stockit/show_on_stock_article_update.js.erb b/app/views/stockit/show_on_stock_article_update.js.erb new file mode 100644 index 00000000..ce057f89 --- /dev/null +++ b/app/views/stockit/show_on_stock_article_update.js.erb @@ -0,0 +1,13 @@ +// Handle more advanced DOM update after AJAX database manipulation. +// See publish/subscribe design pattern in /doc. +(function() { + var stock_article_details = $('<%= j(render( + :partial => 'stock_article_details', + :locals => { + :stock_article => @stock_article + } + )) %>'); + + $('#stockArticleDetails').replaceWith(stock_article_details); + $('h1').first().text('<%= j(@stock_article.name) %>'); +})(); diff --git a/app/views/stockit/update.js.erb b/app/views/stockit/update.js.erb new file mode 100644 index 00000000..830d846c --- /dev/null +++ b/app/views/stockit/update.js.erb @@ -0,0 +1,15 @@ +$('div.container-fluid').prepend('<%= j(render( + :partial => 'shared/alert_success', + :locals => { + :alert_message => t('.notice', :name => @stock_article.name) + } +)) %>'); + +// Publish database changes. +// See publish/subscribe design pattern in /doc. +$(document).trigger({ + type: 'StockArticle#update', + stock_article_id: <%= @stock_article.id %> +}); + +$('#modalContainer').modal('hide'); diff --git a/config/locales/en.yml b/config/locales/en.yml index a48acd92..7011146f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1348,7 +1348,7 @@ en: units_to_order: If you change the total amount of delivered units, you also have to change individual group amounts by clicking on the article name. They will not be automatically recalculated and so ordergroups may be accounted for articles that were not delivered! update_current_price: Also update the price of the current order stock_article: - copy_stock_article: + copy: name: Please modify edit_stock_article: price: @@ -1421,8 +1421,10 @@ en: stockit: check: not_empty: ! '%{name} could not be deleted, the inventory is not zero.' + create: + notice: New stock article »%{name}« was created. destroy: - notice: Article %{name} was deleted. + notice: Article »%{name}« was deleted. edit: title: Edit stock articles form: @@ -1461,10 +1463,10 @@ en: reason: Reason stock_changes: Stock quantity changes stock_taking: Inventory - stock_create: - notice: Stock article was created. - stock_update: - notice: Stock article was saved. + stock_article: + confirm_delete: Are you sure you want to delete the stock article »%{name}«? + update: + notice: Stock article »%{name}« was saved. suppliers: create: notice: Supplier was created diff --git a/config/routes.rb b/config/routes.rb index 655a1038..62678230 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,9 +86,15 @@ Foodsoft::Application.routes.draw do end resources :stock_articles, :to => 'stockit' do + get :copy collection do get :articles_search get :fill_new_stock_article_form + + get :index_on_stock_article_create + get :index_on_stock_article_update + + get :show_on_stock_article_update end end diff --git a/doc/design_patterns/publish_subscribe.md b/doc/design_patterns/publish_subscribe.md new file mode 100644 index 00000000..d261d45a --- /dev/null +++ b/doc/design_patterns/publish_subscribe.md @@ -0,0 +1,33 @@ +# Publish/subscribe pattern +## Handling DOM updates after AJAX database manipulation + +As an example, let us consider the manipulation (create, update...) of `StockArticles`. This can be done in different views, e.g., `stock_articles/index`, `stock_articles/show` and `deliveries/_form` through modals using AJAX requests. As an advantage of the AJAX technique, the user does not need to reload the entire page. However, (after the update of the `StockArticle` in the database) it is generally required to update the DOM in the current view such that the page properly reacts to the asynchronous actions. + +The process can be divided in two steps: **1.** AJAX database manipulation and **2.** DOM updates for the particular view. The crucial point is the coupling of the two steps since the controller for the first step offers the same functionality to all views and does not need to know anything about the current view. + +1. AJAX database manipulation + 1. Example: current view `deliveries/_form` offers a link for the AJAX action `StockArticle#new`. This opens a modal filled with `stock_articles/_form`. + 2. AJAX form post addresses the `StockArticle#create` action which handles the database manipulation. + 3. The database manipulation is finished by the rendering of, e.g., `stock_articles/create.js.erb`. The key task there is to **publish** the database changes by calling `trigger`, i.e., + $(document).trigger({ + type: 'StockArticle#create', + stock_article_id: <%= @stock_article.id %> + }); +2. DOM updates for the particular view + 1. Each view has the opportunity to **subscribe** to particular events of the previous step. A very simple example is the update of the `stock_articles/index` view after `StockArticle#destroy`: + $(document).on('StockArticle#destroy', function(e) { + $('#stockArticle-' + e.stock_article_id).remove(); + }); + + However, in most of the situations you will like to use the full power of the MVC framework in order to read new data from the database and render some partial. Let us consider this slightly more advanced case in the following. + + The view `stock_articles/index` could listen (amongst others) to `StockArticle#create` like this: + $(document).on('StockArticle#create', function(e) { + $.ajax({ + url: '#{index_on_stock_article_create_stock_articles_path}', + type: 'get', + data: {id: e.stock_article_id}, + contentType: 'application/json; charset=UTF-8' + }); + }); + 2. The action `StockArticles#index_on_stock_article_create` is a special helper action to handle DOM updates of the `stock_articles/index` view after the creation of a new `StockArticle` with the given `id`.