diff --git a/Gemfile b/Gemfile
index f4bec0e..381c485 100644
--- a/Gemfile
+++ b/Gemfile
@@ -21,6 +21,7 @@ group :assets do
end
gem 'jquery-rails'
+gem 'activeadmin'
# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 932c5fe..068108f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -14,6 +14,17 @@ GEM
rack-cache (~> 1.1)
rack-test (~> 0.6.1)
sprockets (~> 2.1.2)
+ activeadmin (0.4.0)
+ bourbon (>= 1.0.0)
+ devise (>= 1.1.2)
+ fastercsv
+ formtastic (>= 2.0.0)
+ inherited_resources (< 1.3.0)
+ jquery-rails (>= 1.0.0)
+ kaminari (>= 0.13.0)
+ meta_search (>= 0.9.2)
+ rails (>= 3.0.0)
+ sass (>= 3.1.0)
activemodel (3.2.1)
activesupport (= 3.2.1)
builder (~> 3.0.0)
@@ -29,6 +40,9 @@ GEM
i18n (~> 0.6)
multi_json (~> 1.0)
arel (3.0.2)
+ bcrypt-ruby (3.0.1)
+ bourbon (1.3.6)
+ sass (>= 3.1)
builder (3.0.0)
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
@@ -37,22 +51,47 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.2.0)
+ devise (2.0.0)
+ bcrypt-ruby (~> 3.0)
+ orm_adapter (~> 0.0.3)
+ railties (~> 3.1)
+ warden (~> 1.1)
erubis (2.7.0)
execjs (1.3.0)
multi_json (~> 1.0)
+ fastercsv (1.5.4)
+ formtastic (2.0.2)
+ rails (~> 3.0)
+ has_scope (0.5.1)
hike (1.2.1)
i18n (0.6.0)
+ inherited_resources (1.2.2)
+ has_scope (~> 0.5.0)
+ responders (~> 0.6.0)
journey (1.0.3)
jquery-rails (2.0.1)
railties (>= 3.2.0, < 5.0)
thor (~> 0.14)
json (1.6.5)
+ kaminari (0.13.0)
+ actionpack (>= 3.0.0)
+ activesupport (>= 3.0.0)
+ railties (>= 3.0.0)
mail (2.4.3)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
+ meta_search (1.1.2)
+ actionpack (~> 3.1)
+ activerecord (~> 3.1)
+ activesupport (~> 3.1)
+ meta_search
+ polyamorous (~> 0.5.0)
mime-types (1.17.2)
multi_json (1.1.0)
+ orm_adapter (0.0.6)
+ polyamorous (0.5.0)
+ activerecord (~> 3.0)
polyglot (0.3.3)
rack (1.4.1)
rack-cache (1.2)
@@ -79,6 +118,7 @@ GEM
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
+ responders (0.6.5)
sass (3.1.15)
sass-rails (3.2.4)
railties (~> 3.2.0)
@@ -98,11 +138,14 @@ GEM
uglifier (1.2.3)
execjs (>= 0.3.0)
multi_json (>= 1.0.2)
+ warden (1.1.0)
+ rack (>= 1.0)
PLATFORMS
ruby
DEPENDENCIES
+ activeadmin
coffee-rails (~> 3.2.1)
jquery-rails
rails (= 3.2.1)
diff --git a/app/admin/dashboards.rb b/app/admin/dashboards.rb
new file mode 100644
index 0000000..5c8a3fc
--- /dev/null
+++ b/app/admin/dashboards.rb
@@ -0,0 +1,44 @@
+ActiveAdmin::Dashboards.build do
+
+ # Define your dashboard sections here. Each block will be
+ # rendered on the dashboard in the context of the view. So just
+ # return the content which you would like to display.
+
+ # == Simple Dashboard Section
+ # Here is an example of a simple dashboard section
+ #
+ # section "Recent Posts" do
+ # ul do
+ # Post.recent(5).collect do |post|
+ # li link_to(post.title, admin_post_path(post))
+ # end
+ # end
+ # end
+
+ # == Render Partial Section
+ # The block is rendered within the context of the view, so you can
+ # easily render a partial rather than build content in ruby.
+ #
+ # section "Recent Posts" do
+ # div do
+ # render 'recent_posts' # => this will render /app/views/admin/dashboard/_recent_posts.html.erb
+ # end
+ # end
+
+ # == Section Ordering
+ # The dashboard sections are ordered by a given priority from top left to
+ # bottom right. The default priority is 10. By giving a section numerically lower
+ # priority it will be sorted higher. For example:
+ #
+ # section "Recent Posts", :priority => 10
+ # section "Recent User", :priority => 1
+ #
+ # Will render the "Recent Users" then the "Recent Posts" sections on the dashboard.
+
+ # == Conditionally Display
+ # Provide a method name or Proc object to conditionally render a section at run time.
+ #
+ # section "Membership Summary", :if => :memberships_enabled?
+ # section "Membership Summary", :if => Proc.new { current_admin_user.account.memberships.any? }
+
+end
diff --git a/app/assets/javascripts/.bills.js.swp b/app/assets/javascripts/.bills.js.swp
new file mode 100644
index 0000000..5e0bc1c
Binary files /dev/null and b/app/assets/javascripts/.bills.js.swp differ
diff --git a/app/assets/javascripts/.products.js.swp b/app/assets/javascripts/.products.js.swp
new file mode 100644
index 0000000..a89ac01
Binary files /dev/null and b/app/assets/javascripts/.products.js.swp differ
diff --git a/app/assets/javascripts/active_admin.js b/app/assets/javascripts/active_admin.js
new file mode 100644
index 0000000..d2b66c5
--- /dev/null
+++ b/app/assets/javascripts/active_admin.js
@@ -0,0 +1 @@
+//= require active_admin/base
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 9097d83..58ae75a 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -12,4 +12,6 @@
//
//= require jquery
//= require jquery_ujs
+//= require jquery-ui
+//= require underscore-min
//= require_tree .
diff --git a/app/assets/javascripts/bills.js b/app/assets/javascripts/bills.js
new file mode 100644
index 0000000..c07a122
--- /dev/null
+++ b/app/assets/javascripts/bills.js
@@ -0,0 +1,169 @@
+$(document).ready( function(){
+
+ billitems_list = [];
+
+ // Autocomplete
+ $('.code_autocomplete').on('focus', function(){
+ $(this).autocomplete(
+ {
+ minLength:1,
+ source:'/products/code_autocomplete',
+ focus: function(event, ui){
+ $('#code').val(ui.item.code);
+ $('#name').val(ui.item.name);
+ },
+ change:function(event, ui){
+ console.log(ui);
+ if(ui.item){
+ $('#item_id').val(ui.item.id);
+ $('#code').val(ui.item.code);
+ $('#name').val(ui.item.name);
+ if ($('#price').val()== ""){
+ $('#price').val(ui.item.price);
+ }
+ if ($('#quant').val()== ""){
+ $('#quant').val(1);
+ }
+ }
+ }
+ }
+ );
+ }
+);
+//billitem template
+var billitem_tmpl = _.template("><%= code %><%= name %><%= price %><%= quantity %>Remove
");
+
+// render bill items_list
+function render_bill_items_list(){
+ if (billitems_list.length){
+ _.each(billitems_list, function(bi){
+ $('#bill_area').append(billitem_tmpl(bi));
+ });}
+}
+// Add to bill
+function add_to_bill(){
+ var billitem = {
+ _guid:Math.guid(),
+ id:$('#item_id').val(),
+ code:$('#code').val(),
+ name:$('#name').val(),
+ price:$('#price').val(),
+ quantity:$('#quant').val()
+ };
+ $('#reset_billitem').trigger('click');
+ billitems_list.push(billitem);
+ $('#bill_area').append(billitem_tmpl(billitem));
+}
+
+function calculate_total(){
+ var total = _.reduce(billitems_list, function(tot, bi){ return tot+parseInt(bi.price)*parseInt(bi.quantity); }, 0);
+ $('#total strong').text(total);
+}
+
+$('#add_item_2_bill').live('click', function(){
+ if(parseInt($('#item_id').val())){
+ add_to_bill();
+ calculate_total();
+ }else{
+ alert('This product does not exist in the product database, please create it first.')
+ }
+});
+
+$('#reset_billitem').live('click', function(){
+ _.each($('#bill_item_form .item'), function(it){
+ $(it).val('');
+ });
+});
+
+$('#add_bill').live('click', function(evt){
+ evt.preventDefault();
+ if(billitems_list.length){
+ $.ajax({
+ url:"/bills",
+ data:{billitems:billitems_list},
+ dataType:'json',
+ type:'POST',
+ success:function(data, textStatus, jqXHR){
+ console.log(data);
+ if(data.success){
+ $('#reset_bill').trigger('click');
+ $('#reset_billitem').trigger('click');
+ }
+ }
+ });
+ }else{
+ alert('No items were found in the bill please add some items.');
+ }
+});
+
+// deleting items from the bill
+$('.del_item').live('click',function(evt){
+ evt.preventDefault();
+ var ref = $($(this)[0]);
+ ref = ref.parent('.billitem').filter(':first');
+ var guid = ref.attr("data-guid");
+ ref.remove();
+ billitems_list = _.reject(billitems_list, function(bi){return bi._guid == guid;});
+ calculate_total();
+});
+
+// Reset bill
+$('#reset_bill').live('click', function(){
+ $('.del_item').trigger('click');
+});
+
+// external code
+Math.guid = function(){
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ }).toUpperCase();
+};
+
+
+/*
+ // Updating subtotal on field change
+ function show_subtotal(){
+ var price = parseInt($('#billitem_price').val());
+ var numbers = parseInt($('#billitem_numbers').val());
+ if (price && numbers){
+ $('#subtotal').text("Subtotal : " + price*numbers)
+ }
+ }
+
+ $('#bill_input_form #billitem_price').on('change', show_subtotal);
+ $('#bill_input_form #billitem_numbers').on('change', show_subtotal);
+
+ // Respond on successful write to billitem & bill
+ $(function(){
+ $('#bill_input_form')
+ .bind('ajax:success', function(e, data, status, xhr){
+ if(data.success){
+ $('#bill_table').append(bill_record(data));
+ }
+ return false
+ })
+ .bind('ajax:error', function(e, xhr, status, error){
+ console.log("error json: "+ xhr.respomseText);
+ })
+
+ })
+
+ // debug
+ $('input[name="commit"]').on('click', function(){console.log('Triggered submit');});
+
+ // Delete a bill item from the bill
+ $('.delete_bill_item').on('click', function(){
+ id = $(this).parent('tr').attr('id');
+ this_record = $(this);
+ $.ajax({
+ url:"/bills/"+id+"/destroy_item.json",
+ succeess:function(data){
+ if(data.success){
+ this_record.remove();
+ }
+ }
+ });
+ });
+ */
+})
diff --git a/app/assets/javascripts/bootstrap-modal.js b/app/assets/javascripts/bootstrap-modal.js
new file mode 100644
index 0000000..bceb303
--- /dev/null
+++ b/app/assets/javascripts/bootstrap-modal.js
@@ -0,0 +1,210 @@
+/* =========================================================
+ * bootstrap-modal.js v2.0.1
+ * http://twitter.github.com/bootstrap/javascript.html#modals
+ * =========================================================
+ * Copyright 2012 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+
+!function( $ ){
+
+ "use strict"
+
+ /* MODAL CLASS DEFINITION
+ * ====================== */
+
+ var Modal = function ( content, options ) {
+ this.options = options
+ this.$element = $(content)
+ .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
+ }
+
+ Modal.prototype = {
+
+ constructor: Modal
+
+ , toggle: function () {
+ return this[!this.isShown ? 'show' : 'hide']()
+ }
+
+ , show: function () {
+ var that = this
+
+ if (this.isShown) return
+
+ $('body').addClass('modal-open')
+
+ this.isShown = true
+ this.$element.trigger('show')
+
+ escape.call(this)
+ backdrop.call(this, function () {
+ var transition = $.support.transition && that.$element.hasClass('fade')
+
+ !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position
+
+ that.$element
+ .show()
+
+ if (transition) {
+ that.$element[0].offsetWidth // force reflow
+ }
+
+ that.$element.addClass('in')
+
+ transition ?
+ that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
+ that.$element.trigger('shown')
+
+ })
+ }
+
+ , hide: function ( e ) {
+ e && e.preventDefault()
+
+ if (!this.isShown) return
+
+ var that = this
+ this.isShown = false
+
+ $('body').removeClass('modal-open')
+
+ escape.call(this)
+
+ this.$element
+ .trigger('hide')
+ .removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade') ?
+ hideWithTransition.call(this) :
+ hideModal.call(this)
+ }
+
+ }
+
+
+ /* MODAL PRIVATE METHODS
+ * ===================== */
+
+ function hideWithTransition() {
+ var that = this
+ , timeout = setTimeout(function () {
+ that.$element.off($.support.transition.end)
+ hideModal.call(that)
+ }, 500)
+
+ this.$element.one($.support.transition.end, function () {
+ clearTimeout(timeout)
+ hideModal.call(that)
+ })
+ }
+
+ function hideModal( that ) {
+ this.$element
+ .hide()
+ .trigger('hidden')
+
+ backdrop.call(this)
+ }
+
+ function backdrop( callback ) {
+ var that = this
+ , animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+ if (this.isShown && this.options.backdrop) {
+ var doAnimate = $.support.transition && animate
+
+ this.$backdrop = $('
')
+ .appendTo(document.body)
+
+ if (this.options.backdrop != 'static') {
+ this.$backdrop.click($.proxy(this.hide, this))
+ }
+
+ if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+ this.$backdrop.addClass('in')
+
+ doAnimate ?
+ this.$backdrop.one($.support.transition.end, callback) :
+ callback()
+
+ } else if (!this.isShown && this.$backdrop) {
+ this.$backdrop.removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade')?
+ this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) :
+ removeBackdrop.call(this)
+
+ } else if (callback) {
+ callback()
+ }
+ }
+
+ function removeBackdrop() {
+ this.$backdrop.remove()
+ this.$backdrop = null
+ }
+
+ function escape() {
+ var that = this
+ if (this.isShown && this.options.keyboard) {
+ $(document).on('keyup.dismiss.modal', function ( e ) {
+ e.which == 27 && that.hide()
+ })
+ } else if (!this.isShown) {
+ $(document).off('keyup.dismiss.modal')
+ }
+ }
+
+
+ /* MODAL PLUGIN DEFINITION
+ * ======================= */
+
+ $.fn.modal = function ( option ) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('modal')
+ , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('modal', (data = new Modal(this, options)))
+ if (typeof option == 'string') data[option]()
+ else if (options.show) data.show()
+ })
+ }
+
+ $.fn.modal.defaults = {
+ backdrop: true
+ , keyboard: true
+ , show: true
+ }
+
+ $.fn.modal.Constructor = Modal
+
+
+ /* MODAL DATA-API
+ * ============== */
+
+ $(function () {
+ $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
+ var $this = $(this), href
+ , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+ , option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data())
+
+ e.preventDefault()
+ $target.modal(option)
+ })
+ })
+
+}( window.jQuery );
\ No newline at end of file
diff --git a/app/assets/javascripts/bootstrap-typeahead.js b/app/assets/javascripts/bootstrap-typeahead.js
new file mode 100644
index 0000000..5031559
--- /dev/null
+++ b/app/assets/javascripts/bootstrap-typeahead.js
@@ -0,0 +1,271 @@
+/* =============================================================
+ * bootstrap-typeahead.js v2.0.1
+ * http://twitter.github.com/bootstrap/javascript.html#typeahead
+ * =============================================================
+ * Copyright 2012 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============================================================ */
+
+!function( $ ){
+
+ "use strict"
+
+ var Typeahead = function ( element, options ) {
+ this.$element = $(element)
+ this.options = $.extend({}, $.fn.typeahead.defaults, options)
+ this.matcher = this.options.matcher || this.matcher
+ this.sorter = this.options.sorter || this.sorter
+ this.highlighter = this.options.highlighter || this.highlighter
+ this.$menu = $(this.options.menu).appendTo('body')
+ this.source = this.options.source
+ this.shown = false
+ this.listen()
+ }
+
+ Typeahead.prototype = {
+
+ constructor: Typeahead
+
+ , select: function () {
+ var val = this.$menu.find('.active').attr('data-value')
+ this.$element.val(val)
+ return this.hide()
+ }
+
+ , show: function () {
+ var pos = $.extend({}, this.$element.offset(), {
+ height: this.$element[0].offsetHeight
+ })
+
+ this.$menu.css({
+ top: pos.top + pos.height
+ , left: pos.left
+ })
+
+ this.$menu.show()
+ this.shown = true
+ return this
+ }
+
+ , hide: function () {
+ this.$menu.hide()
+ this.shown = false
+ return this
+ }
+
+ , lookup: function (event) {
+ var that = this
+ , items
+ , q
+
+ this.query = this.$element.val()
+
+ if (!this.query) {
+ return this.shown ? this.hide() : this
+ }
+
+ items = $.grep(this.source, function (item) {
+ if (that.matcher(item)) return item
+ })
+
+ items = this.sorter(items)
+
+ if (!items.length) {
+ return this.shown ? this.hide() : this
+ }
+
+ return this.render(items.slice(0, this.options.items)).show()
+ }
+
+ , matcher: function (item) {
+ return ~item.toLowerCase().indexOf(this.query.toLowerCase())
+ }
+
+ , sorter: function (items) {
+ var beginswith = []
+ , caseSensitive = []
+ , caseInsensitive = []
+ , item
+
+ while (item = items.shift()) {
+ if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
+ else if (~item.indexOf(this.query)) caseSensitive.push(item)
+ else caseInsensitive.push(item)
+ }
+
+ return beginswith.concat(caseSensitive, caseInsensitive)
+ }
+
+ , highlighter: function (item) {
+ return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
+ return '' + match + ''
+ })
+ }
+
+ , render: function (items) {
+ var that = this
+
+ items = $(items).map(function (i, item) {
+ i = $(that.options.item).attr('data-value', item)
+ i.find('a').html(that.highlighter(item))
+ return i[0]
+ })
+
+ items.first().addClass('active')
+ this.$menu.html(items)
+ return this
+ }
+
+ , next: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , next = active.next()
+
+ if (!next.length) {
+ next = $(this.$menu.find('li')[0])
+ }
+
+ next.addClass('active')
+ }
+
+ , prev: function (event) {
+ var active = this.$menu.find('.active').removeClass('active')
+ , prev = active.prev()
+
+ if (!prev.length) {
+ prev = this.$menu.find('li').last()
+ }
+
+ prev.addClass('active')
+ }
+
+ , listen: function () {
+ this.$element
+ .on('blur', $.proxy(this.blur, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ if ($.browser.webkit || $.browser.msie) {
+ this.$element.on('keydown', $.proxy(this.keypress, this))
+ }
+
+ this.$menu
+ .on('click', $.proxy(this.click, this))
+ .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
+ }
+
+ , keyup: function (e) {
+ e.stopPropagation()
+ e.preventDefault()
+
+ switch(e.keyCode) {
+ case 40: // down arrow
+ case 38: // up arrow
+ break
+
+ case 9: // tab
+ case 13: // enter
+ if (!this.shown) return
+ this.select()
+ break
+
+ case 27: // escape
+ this.hide()
+ break
+
+ default:
+ this.lookup()
+ }
+
+ }
+
+ , keypress: function (e) {
+ e.stopPropagation()
+ if (!this.shown) return
+
+ switch(e.keyCode) {
+ case 9: // tab
+ case 13: // enter
+ case 27: // escape
+ e.preventDefault()
+ break
+
+ case 38: // up arrow
+ e.preventDefault()
+ this.prev()
+ break
+
+ case 40: // down arrow
+ e.preventDefault()
+ this.next()
+ break
+ }
+ }
+
+ , blur: function (e) {
+ var that = this
+ e.stopPropagation()
+ e.preventDefault()
+ setTimeout(function () { that.hide() }, 150)
+ }
+
+ , click: function (e) {
+ e.stopPropagation()
+ e.preventDefault()
+ this.select()
+ }
+
+ , mouseenter: function (e) {
+ this.$menu.find('.active').removeClass('active')
+ $(e.currentTarget).addClass('active')
+ }
+
+ }
+
+
+ /* TYPEAHEAD PLUGIN DEFINITION
+ * =========================== */
+
+ $.fn.typeahead = function ( option ) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('typeahead')
+ , options = typeof option == 'object' && option
+ if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
+ if (typeof option == 'string') data[option]()
+ })
+ }
+
+ $.fn.typeahead.defaults = {
+ source: []
+ , items: 8
+ , menu: ''
+ , item: ''
+ }
+
+ $.fn.typeahead.Constructor = Typeahead
+
+
+ /* TYPEAHEAD DATA-API
+ * ================== */
+
+ $(function () {
+ $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
+ var $this = $(this)
+ if ($this.data('typeahead')) return
+ e.preventDefault()
+ $this.typeahead($this.data())
+ })
+ })
+
+}( window.jQuery );
\ No newline at end of file
diff --git a/app/assets/javascripts/products.js b/app/assets/javascripts/products.js
new file mode 100644
index 0000000..1b6a541
--- /dev/null
+++ b/app/assets/javascripts/products.js
@@ -0,0 +1,122 @@
+$(document).ready(function(){
+ product_tmpl = _.template('\
+ \
+ <%=id%> \
+ <%=code%> \
+ <%=name%> \
+ <%=price%> \
+ <%=quantity%>\
+ \
+ \
+ ');
+ var p_form = $('#new_product_form');
+ $('#add_to_products').live('click',function(evt){
+ evt.preventDefault();
+ var product = {
+ code:$('#p_code', p_form).val(),
+ name:$('#p_name', p_form).val(),
+ price:parseInt($('#p_price', p_form).val()),
+ quantity:parseInt($('#p_quant', p_form).val())
+ };
+ $.ajax({
+ url:'/products',
+ data:JSON.stringify(product),
+ type:'POST',
+ contentType:'application/json',
+ dataType:'json',
+ success:function(data){
+ if(data.success){
+ $('#reset_new_product').trigger('click');
+ $('#products_list').prepend(product_tmpl(data.product));
+ }else{
+ alert(data.msg);
+ }
+ },
+ })
+ });
+ var modal_template = _.template(' \
+ \
+ \
+ \
+
\
+ \
+ ');
+ $('.list_item_edit').live('click', function(evt){
+ var product_id = $(this).attr('id');
+ $(this).parent('.product').addClass('editing');
+ $.ajax({
+ url:'/products/' + product_id,
+ type:'GET',
+ contentType:'application/json',
+ dataType:'json',
+ success:function(data){
+ if(data.success){
+ $('#modalEdit').html(modal_template(data.product));
+ $('#modalEdit').modal('show');
+ }
+ }
+ });
+ });
+
+ $('#update_product').live('click', function(evt){
+ evt.preventDefault();
+ var product ={
+ id:parseInt($('#edit_product_form #e_id').val()),
+ code:$('#edit_product_form #e_code').val(),
+ name:$('#edit_product_form #e_name').val(),
+ price:parseInt($('#edit_product_form #e_price').val()),
+ quantity:parseInt($('#edit_product_form #e_quant').val())
+ };
+ $.ajax({
+ url:'/products/' + product.id,
+ type:'PUT',
+ data:JSON.stringify(product),
+ contentType:'application/json',
+ dataType:'json',
+ success:function(data){
+ if(data.success){
+ $('#modalEdit').modal('hide');
+ $('.editing').remove();
+ $('#products_list').prepend(product_tmpl(data.product));
+ }
+ }
+ });
+ });
+
+ $('#delete_product').live('click', function(evt){
+ console.log('delete product');
+ evt.preventDefault();
+ var id=parseInt($('#edit_product_form #e_id').val());
+ $.ajax({
+ url:'/products/' + id,
+ type:'DELETE',
+ contentType:'application/json',
+ dataType:'json',
+ success:function(data){
+ if(data.success){
+ $('#modalEdit').modal('hide');
+ $('.editing').remove();
+ }
+ }
+ });
+ });
+
+});
diff --git a/app/assets/stylesheets/active_admin.css.scss b/app/assets/stylesheets/active_admin.css.scss
new file mode 100644
index 0000000..9b2dc9d
--- /dev/null
+++ b/app/assets/stylesheets/active_admin.css.scss
@@ -0,0 +1,6 @@
+// Active Admin CSS Styles
+@import "active_admin/mixins";
+@import "active_admin/base";
+
+// To customize the Active Admin interfaces, add your
+// styles here:
diff --git a/app/assets/stylesheets/bills.css.scss b/app/assets/stylesheets/bills.css.scss
new file mode 100644
index 0000000..9e195c8
--- /dev/null
+++ b/app/assets/stylesheets/bills.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the bills controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/products.css.scss b/app/assets/stylesheets/products.css.scss
new file mode 100644
index 0000000..ba440af
--- /dev/null
+++ b/app/assets/stylesheets/products.css.scss
@@ -0,0 +1,8 @@
+// Place all the styles related to the Products controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+.editing{
+ background-color:yellow;
+ opacity:.80;
+ filter:"alpha(80)";
+}
diff --git a/app/controllers/bills_controller.rb b/app/controllers/bills_controller.rb
new file mode 100644
index 0000000..40c6424
--- /dev/null
+++ b/app/controllers/bills_controller.rb
@@ -0,0 +1,20 @@
+class BillsController < ApplicationController
+ def new
+ @bill = Bill.new
+ @billitem = Billitem.new
+ end
+
+ def create
+ @bill= Bill.create
+ items = params[:billitems]
+ items.each do |item|
+ item = item[1] # array of objects turned to hash with the index as the key. The value is the actual object
+ @bill.new_item(item)
+ Product.sell(item)
+ end
+ if @bill.save!
+ render :json => {success:true}
+ end
+
+ end
+end
diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb
new file mode 100644
index 0000000..1b847a6
--- /dev/null
+++ b/app/controllers/products_controller.rb
@@ -0,0 +1,49 @@
+class ProductsController < ApplicationController
+ http_basic_authenticate_with :name => "theshop", :password => "theshop", :except => [:code_autocomplete, :products_list]
+
+ def code_autocomplete
+ @items = Product.where('code like ?', params[:term].upcase+"%")
+ items = []
+ @items.each {|i|
+ items << {label:i.code, id:i.id, code:i.code, name:i.name, price:i.price}
+ }
+ render :json => items
+ end
+
+ def index
+ @products = Product.all
+ end
+
+ def products_list
+ @products = Product.all
+ end
+
+ def update
+ @product = Product.find(params[:product][:id])
+ if @product.update_attributes(params[:product])
+ render :json => {success:true, product:@product}
+ else
+ render :json => {success:false}
+ end
+ end
+
+ def create
+ @product = Product.where({code:params[:product][:code].upcase}).first()
+ if @product.nil?
+ params[:product][:code] = params[:product][:code].upcase
+ product = Product.create(params[:product])
+ render :json => {success:true, product:product}
+ else
+ render :json => {success:false, msg:"Save failed : Product already exists, edit it from the product database."}
+ end
+ end
+
+ def destroy
+ Product.find(params[:id]).destroy
+ render :json => {success:true}
+ end
+
+ def show
+ render :json => {success:true, product:Product.find(params[:id])}
+ end
+end
diff --git a/app/helpers/bills_helper.rb b/app/helpers/bills_helper.rb
new file mode 100644
index 0000000..2c9967d
--- /dev/null
+++ b/app/helpers/bills_helper.rb
@@ -0,0 +1,2 @@
+module BillsHelper
+end
diff --git a/app/helpers/products_helper.rb b/app/helpers/products_helper.rb
new file mode 100644
index 0000000..ab5c42b
--- /dev/null
+++ b/app/helpers/products_helper.rb
@@ -0,0 +1,2 @@
+module ProductsHelper
+end
diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb
new file mode 100644
index 0000000..039464f
--- /dev/null
+++ b/app/models/admin_user.rb
@@ -0,0 +1,9 @@
+class AdminUser < ActiveRecord::Base
+ # Include default devise modules. Others available are:
+ # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
+ devise :database_authenticatable,
+ :recoverable, :rememberable, :trackable, :validatable
+
+ # Setup accessible (or protected) attributes for your model
+ attr_accessible :email, :password, :password_confirmation, :remember_me
+end
diff --git a/app/models/bill.rb b/app/models/bill.rb
index b2609a2..7f94f40 100644
--- a/app/models/bill.rb
+++ b/app/models/bill.rb
@@ -1,2 +1,7 @@
class Bill < ActiveRecord::Base
+ has_many :billitems
+
+ def new_item(item)
+ self.billitems.create({product_id:item[:id], price:item[:price], quant:item[:quantity]})
+ end
end
diff --git a/app/models/billitem.rb b/app/models/billitem.rb
index c2eabdf..300fc45 100644
--- a/app/models/billitem.rb
+++ b/app/models/billitem.rb
@@ -1,2 +1,4 @@
class Billitem < ActiveRecord::Base
+ belongs_to :bill
+ has_one :product
end
diff --git a/app/models/product.rb b/app/models/product.rb
index 077a819..56e9bc4 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -1,2 +1,8 @@
class Product < ActiveRecord::Base
+ has_many :billitems
+ def self.sell(item)
+ prod = self.find(item[:id])
+ prod.update_attributes({price:item[:price], quantity:(prod.quantity - item[:quantity].to_i)})
+ prod
+ end
end
diff --git a/app/views/bills/.new.html.erb.swp b/app/views/bills/.new.html.erb.swp
new file mode 100644
index 0000000..a048e58
Binary files /dev/null and b/app/views/bills/.new.html.erb.swp differ
diff --git a/app/views/bills/new.html.erb b/app/views/bills/new.html.erb
new file mode 100644
index 0000000..4fba431
--- /dev/null
+++ b/app/views/bills/new.html.erb
@@ -0,0 +1,40 @@
+
+
+ -
+ <%= link_to "Bill", new_bill_path %>
+
+ -
+ <%= link_to "Products list", products_list_products_path %>
+
+ -
+ <%= link_to "Products management", products_path %>
+
+
+
+
+
+
+
Bill area
+
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index a17417a..f3fdffe 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -7,8 +7,6 @@
<%= csrf_meta_tags %>
-
-<%= yield %>
-
+ <%= yield %>