Use barcode to create new order
This commit is contained in:
parent
ec2b516727
commit
6403835872
16 changed files with 222 additions and 125 deletions
BIN
app/assets/images/logo.png
Normal file
BIN
app/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -2,64 +2,68 @@
|
|||
// All this logic will automatically be available in application.js.
|
||||
// You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
ready = function() {
|
||||
$('.btn-inc').on('click', function() {
|
||||
increment($(this), 1);
|
||||
});
|
||||
$('.btn-dec').on('click', function() {
|
||||
increment($(this), -1);
|
||||
});
|
||||
$('.form_row').each(function(index, row) {
|
||||
updateInput(row, false);
|
||||
$(row).on('input', function() {
|
||||
updateInput(row);
|
||||
});
|
||||
});
|
||||
recalculate();
|
||||
};
|
||||
products_ordered = $('#product_search').keyup(function () {
|
||||
var rex = new RegExp($(this).val(), 'i');
|
||||
$('[data-name]').hide();
|
||||
$('[data-name]').filter(function () {
|
||||
return rex.test($(this).data("name"));
|
||||
}).show();
|
||||
})
|
||||
|
||||
// Validate input, and then update
|
||||
updateInput = function(row, useRecalculate) {
|
||||
if (useRecalculate == null) {
|
||||
useRecalculate = true;
|
||||
$('#products_modal').on('hidden.bs.modal', function () {
|
||||
$('#product_search').val('');
|
||||
});
|
||||
|
||||
increment_product = function(product_id) {
|
||||
input = $("#current_order").find(".order_item_wrapper[data-product=" + product_id + "]").find("input[type=number]");
|
||||
$(input).val(parseInt($(input).val()) + 1).change();
|
||||
}
|
||||
cell = row.querySelector("input");
|
||||
if (!cell.validity.valid) {
|
||||
if (parseInt(cell.value) > parseInt(cell.max)) {
|
||||
cell.value = parseInt(cell.max);
|
||||
} else {
|
||||
cell.value = 0;
|
||||
|
||||
$("#products_modal button").click(function() {
|
||||
increment_product($(this).data("product"))
|
||||
})
|
||||
|
||||
$("#from_barcode_form").on("ajax:before", function(xhr, settings) {
|
||||
// Stuff you wanna do after sending form
|
||||
}).on("ajax:success", function(data, status, xhr) {
|
||||
if (status != null) {
|
||||
increment_product(status["id"])
|
||||
}
|
||||
}).on("ajax:error", function(xhr, status, error) {
|
||||
// Display an error or something, whatever
|
||||
}).on("ajax:complete", function(xhr, status) {
|
||||
$("#from_barcode_form")[0].reset();
|
||||
// Do more stuff
|
||||
})
|
||||
|
||||
$('tr.order_item_wrapper').hide();
|
||||
$('tr.order_item_wrapper').filter(function() {
|
||||
return parseInt($(this).find('[type=number]').val()) > 0;
|
||||
}).show();
|
||||
|
||||
$('tr.order_item_wrapper input[type=number]').change(function() {
|
||||
tr = $(this).closest('tr.order_item_wrapper')
|
||||
$(tr).toggle(parseInt($(this).val()) > 0);
|
||||
$(tr).find("td").last().html(((parseInt($(tr).data("price")) * parseInt($(this).val())) / 100.0).toFixed(2))
|
||||
recalculate();
|
||||
})
|
||||
|
||||
recalculate = function() {
|
||||
/* Total Price */
|
||||
array = $('tr.order_item_wrapper').map(function() {
|
||||
return parseInt($(this).data("price")) * parseInt($(this).find("input[type=number]").val());
|
||||
})
|
||||
sum = 0;
|
||||
array.each(function(i, el) { sum += el; });
|
||||
$("#current_order .total_price").html((sum / 100.0).toFixed(2));
|
||||
|
||||
/* Message when no product has been choosen */
|
||||
$("#current_order #empty").toggle(!($('tr.order_item_wrapper input[type=number]').filter(function() {
|
||||
return parseInt($(this).val()) > 0;
|
||||
}).length));
|
||||
}
|
||||
disIfNec(row)
|
||||
if (useRecalculate) {
|
||||
recalculate()
|
||||
}
|
||||
};
|
||||
|
||||
disIfNec = function(row) {
|
||||
counter = parseInt($(row).find('.row_counter').val())
|
||||
$(row).find('.btn-dec').prop('disabled', counter === 0)
|
||||
$(row).find('.btn-inc').prop('disabled', counter === parseInt($(row).find('.row_counter').attr('max')))
|
||||
};
|
||||
|
||||
recalculate = function() {
|
||||
sum = 0
|
||||
$('.row_counter').each(function(i, value) { sum += parseInt($(value).val()) * parseInt($(value).data('price')) })
|
||||
return $('#order_price').html((sum / 100.0).toFixed(2))
|
||||
};
|
||||
|
||||
increment = function(button, n) {
|
||||
row = $(button).closest('.form_row')
|
||||
|
||||
// Fix the counter
|
||||
counter = $(row).find('.row_counter')
|
||||
value = parseInt(counter.val())
|
||||
if (isNaN(value)) {
|
||||
value = 0
|
||||
}
|
||||
counter.val(value + n)
|
||||
|
||||
updateInput(row[0])
|
||||
recalculate();
|
||||
}
|
||||
|
||||
$(document).ready(ready);
|
||||
|
|
|
@ -2,60 +2,78 @@
|
|||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
|
||||
.big-form-button {
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.form-control.big-form-field {
|
||||
height: 65px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form_row_image {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
.barcode-wrapper {
|
||||
font-size: 300%;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form_row input::-webkit-outer-spin-button,
|
||||
.form_row input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
.form_row .btn-lg {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
.form_row .caption {
|
||||
h4 {
|
||||
position: relative;
|
||||
span {
|
||||
#products_modal {
|
||||
.modal-header {
|
||||
h4 {
|
||||
display: inline-block;
|
||||
}
|
||||
input {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
.col-md-2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
button.product {
|
||||
width: 100%;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
padding-right: 50px;
|
||||
}
|
||||
small {
|
||||
margin-left: -45px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#order_price {
|
||||
width: 50px;
|
||||
|
||||
#current_order {
|
||||
text-align: left;
|
||||
border: 1px solid;
|
||||
padding: 10px;
|
||||
div.center {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.order_item_wrapper {
|
||||
input {
|
||||
border: none;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
table {
|
||||
border-top: 1px dashed;
|
||||
border-collapse: separate;
|
||||
margin-bottom: 15px;
|
||||
td:first-child {
|
||||
width: 40px;
|
||||
input {
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
tr:last-child td {
|
||||
border-top: 1px dashed;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
tr.margin {
|
||||
height: 10px;
|
||||
}
|
||||
td.euro {
|
||||
text-align: right;
|
||||
&::before {
|
||||
content: "€";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,8 @@ class OrdersController < ApplicationController
|
|||
load_and_authorize_resource :order, through: :user, shallow: true
|
||||
|
||||
def new
|
||||
products = (@user.products.for_sale.select("products.*", "sum(order_items.count) as count").group(:product_id).order("count desc") | Product.for_sale)
|
||||
products.each do |p|
|
||||
@order.order_items.build(product: p)
|
||||
end
|
||||
@products = Product.all.for_sale.order(:name)
|
||||
@order.products << @products
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -14,6 +12,7 @@ class OrdersController < ApplicationController
|
|||
flash[:success] = "#{@order.to_sentence} ordered. Enjoy it!"
|
||||
redirect_to root_path
|
||||
else
|
||||
@products = Product.all.for_sale.order(:name)
|
||||
render 'new'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,9 +31,13 @@ class ProductsController < ApplicationController
|
|||
respond_with @product
|
||||
end
|
||||
|
||||
def from_barcode
|
||||
render json: Product.find_by_barcode(params.require(:barcode))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def product_params
|
||||
params.require(:product).permit(:name, :price, :avatar, :category, :stock, :calories, :deleted)
|
||||
params.require(:product).permit(:name, :price, :avatar, :category, :stock, :calories, :deleted, :barcode)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ class Ability
|
|||
def initialize(user)
|
||||
return unless user
|
||||
|
||||
can :from_barcode, Product
|
||||
|
||||
if user.admin?
|
||||
can :manage, :all
|
||||
elsif user.koelkast?
|
||||
|
|
|
@ -28,6 +28,7 @@ class Product < ActiveRecord::Base
|
|||
validates :price_cents, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
||||
validates :stock, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :calories, numericality: { only_integer: true, allow_nil: true, greater_than_or_equal_to: 0 }
|
||||
validates :barcode, presence: true, uniqueness: true
|
||||
|
||||
scope :for_sale, -> { where deleted: false }
|
||||
|
||||
|
|
|
@ -44,7 +44,19 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def debt
|
||||
42.15
|
||||
def balance
|
||||
@balance || begin
|
||||
headers = {
|
||||
"Authorization" => "Token token=#{Rails.application.secrets.tab_api_key}",
|
||||
"Content-type" => "application/json"
|
||||
}
|
||||
result = HTTParty.get(File.join(Rails.application.config.api_url, "users", "#{name}.json"), headers: headers)
|
||||
|
||||
if result.code == 200
|
||||
JSON.parse(result.body)["balance"]
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,6 +32,6 @@
|
|||
%li= link_to "Edit profile", edit_user_path(current_user)
|
||||
%li
|
||||
%p.navbar-text
|
||||
Balance: #{euro_from_cents(current_user.debt)}
|
||||
Balance: #{euro_from_cents(current_user.balance)}
|
||||
.visible-xs.navbar-form
|
||||
= render 'layouts/session_button'
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.col-md-3.form_total
|
||||
%strong Total price
|
||||
.input-group
|
||||
%span.input-group-addon €
|
||||
%span#order_price.input-group-addon
|
||||
%span.input-group-btn
|
||||
= f.submit "Order!", class: "btn btn-primary big-form-button", skip_wrapper: true
|
19
app/views/orders/_products_modal.html.haml
Normal file
19
app/views/orders/_products_modal.html.haml
Normal file
|
@ -0,0 +1,19 @@
|
|||
#products_modal.modal{ tabindex: -1 }
|
||||
.modal-dialog.modal-lg
|
||||
.modal-content
|
||||
.modal-header
|
||||
%button.close{ data: { dismiss: :modal } }
|
||||
%span ×
|
||||
%h4.modal-title Kies een product
|
||||
.col-xs-3.pull-right
|
||||
%input#product_search.form-control{ placeholder: "Search" }
|
||||
.modal-body
|
||||
.container-fluid
|
||||
- @products.each do |product|
|
||||
.col-md-2{ data: { name: product.name } }
|
||||
%button.btn.btn-default.product{ data: { product: product.id, dismiss: :modal } }
|
||||
%p= product.name
|
||||
= image_tag product.avatar(:dagschotel), class: "center"
|
||||
.modal-footer
|
||||
%button.btn.btn-default{ data: { dismiss: :modal } }
|
||||
Close
|
|
@ -1,9 +1,43 @@
|
|||
%h3
|
||||
Order for #{@user.name}
|
||||
.row
|
||||
= f_form_for [@user, @order] do |f|
|
||||
= f.error_messages
|
||||
.col-md-12
|
||||
= f.fields_for :order_items do |op_field|
|
||||
= render op_field.object, f: op_field, product: op_field.object.product
|
||||
= render 'orders/price', f: f
|
||||
.center
|
||||
.row
|
||||
.col-md-6.col-md-offset-1.barcode-wrapper
|
||||
%h1 Order for #{@user.name}
|
||||
= form_tag from_barcode_products_path, id: "from_barcode_form", remote: true do
|
||||
%input.center-block{ type: :number, name: :barcode, autofocus: true }
|
||||
= "- OR -"
|
||||
%button.btn.btn-default.center-block{ data: { toggle: :modal, target: "#products_modal" } }
|
||||
Select Product Without Barcode
|
||||
.col-md-4.col-md-offset-1
|
||||
-# Huidige schuld: #{euro_from_cents @user.balance}
|
||||
#current_order
|
||||
.div.center
|
||||
= image_tag "logo.png"
|
||||
= form_for [@user, @order] do |f|
|
||||
%table
|
||||
%tr.margin
|
||||
= f.fields_for :order_items do |ff|
|
||||
%tr.order_item_wrapper{ data: { product: ff.object.product.id, price: ff.object.product.price_cents } }
|
||||
%td
|
||||
= ff.number_field :count
|
||||
= ff.fields_for :product do |fff|
|
||||
/ Needed for haml
|
||||
%td
|
||||
x
|
||||
%td
|
||||
%span= ff.object.product.name
|
||||
%td.euro
|
||||
= euro_from_cents(ff.object.product.price_cents * ff.object.count)
|
||||
%tr#empty
|
||||
%td
|
||||
%td
|
||||
%em Empty Order.
|
||||
%tr.margin
|
||||
%tr
|
||||
%td
|
||||
%td
|
||||
%td.text-right
|
||||
Total:
|
||||
%td.total_price.euro
|
||||
= f.submit "Order!", class: "btn btn-primary form-control"
|
||||
= render 'products_modal'
|
||||
- p @order.errors
|
||||
|
|
|
@ -27,7 +27,11 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :products, only: [:new, :create, :index, :edit, :update]
|
||||
resources :products, only: [:new, :create, :index, :edit, :update] do
|
||||
collection do
|
||||
post 'barcode' => 'products#from_barcode', as: :from_barcode
|
||||
end
|
||||
end
|
||||
resources :stocks, only: [:new, :create]
|
||||
|
||||
get 'overview' => 'orders#overview', as: "orders"
|
||||
|
|
5
db/migrate/20150919091214_add_barcode_to_products.rb
Normal file
5
db/migrate/20150919091214_add_barcode_to_products.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AddBarcodeToProducts < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :products, :barcode, :string, null: false, default: ""
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20150918190548) do
|
||||
ActiveRecord::Schema.define(version: 20150919091214) do
|
||||
|
||||
create_table "delayed_jobs", force: :cascade do |t|
|
||||
t.integer "priority", default: 0, null: false
|
||||
|
@ -60,6 +60,7 @@ ActiveRecord::Schema.define(version: 20150918190548) do
|
|||
t.integer "stock", default: 0, null: false
|
||||
t.integer "calories"
|
||||
t.boolean "deleted", default: false
|
||||
t.string "barcode", default: "", null: false
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
|
|
|
@ -28,6 +28,7 @@ FactoryGirl.define do
|
|||
stock { 30 + rand(30) }
|
||||
calories { rand 20 }
|
||||
avatar { Identicon.data_url_for name }
|
||||
sequence :barcode
|
||||
|
||||
factory :invalid_product do
|
||||
name nil
|
||||
|
|
Loading…
Reference in a new issue