5

I have a collection of objects. What I would like to do is iterate over the entire collection, but show each object on a page/view by itself, and allow the user to interact with each object individually. Ideally, I would prefer not to use a multi-part form if I can avoid it, for reasons I spell out at the end of my question.

I am trying to implement screens like the images below.

Screen 1 Screen 2

Basically the user of the app, will go to a location to do a reconciliation of inventory (of each product) in that location. That's what the screens are showing. For each product, they have to update the inventory.

The summary & requirements are as follows:

  • A location has_many inventory_items.
  • A user begins a reconciliation whenever they want to do an inventory check.
  • A reconciliation habtm inventory_items && belongs_to :location.
  • An inventory_item habtm reconciliations && belongs_to :location.
  • I can't predict in advance how many inventory_items there are.
  • There could be dozens or hundreds of inventory_items.
  • I can break up the inventory_items into different groups if the number becomes unwieldy....similar to a pagination approach.

So my models look like this:

Reconciliation

# == Schema Information
#
# Table name: reconciliations
#
#  id          :integer          not null, primary key
#  location_id :integer
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#

class Reconciliation < ApplicationRecord
  belongs_to :location
  has_and_belongs_to_many :inventory_items
end

Location

# == Schema Information
#
# Table name: locations
#
#  id         :integer          not null, primary key
#  name       :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Location < ApplicationRecord
  has_and_belongs_to_many :products
  has_many :inventory_items, inverse_of: :location
  accepts_nested_attributes_for :inventory_items
  has_many :reconciliations
end

Inventory Item

# == Schema Information
#
# Table name: inventory_items
#
#  id                    :integer          not null, primary key
#  product_id            :integer
#  location_id           :integer
#  quantity_left         :integer
#  quantity_delivered    :integer
#  quantity_on_hand      :integer
#  date_of_last_delivery :datetime
#  created_at            :datetime         not null
#  updated_at            :datetime         not null
#

class InventoryItem < ApplicationRecord
  belongs_to :product
  belongs_to :location, inverse_of: :inventory_items
  has_and_belongs_to_many :reconciliations
end

Here is my inventory_items_reconciliations Join Table:

  create_table "inventory_items_reconciliations", id: false, force: :cascade do |t|
    t.bigint "inventory_item_id", null: false
    t.bigint "reconciliation_id", null: false
    t.index ["inventory_item_id", "reconciliation_id"], name: "index_inventory_item_id_reconciliation_id_join"
    t.index ["reconciliation_id", "inventory_item_id"], name: "index_reconciliation_id_inventory_item_id_join"
  end

My routes.rb:

  resources :locations, shallow: true do
    resources :inventory_items
    resources :reconciliations
  end

My ReconciliationsController#New:

  def new
    @location = Location.find(params[:location_id])
    @reconciliation = @location.reconciliations.new
    @inventory_items = @location.inventory_items
    @num_of_inventory_items = @inventory_items.coun
  end

My app/views/reconciliations/new.html.erb:

<% @inventory_items.each do |inventory_item| %>
  <%= render 'form', reconciliation: @reconciliation, inventory_item: inventory_item %>
<% end %>

My app/views/reconciliations/_form.html.erb:

<%= simple_form_for @reconciliation, url: :location_reconciliations do |f| %>
  <%= f.error_notification %>

  <strong>Name</strong>: <%= inventory_item.product.name %> <br />
  <strong>Quantity Left:</strong> <%= inventory_item.quantity_left %> <br />
  <strong>Quantity Delivered:</strong> <%= inventory_item.quantity_delivered %> <br />

  <div class="form-actions">
    <%= f.button :submit, "Update", class: "btn btn-primary" %>
  </div>
<% end %>

What this does is just displays all of the location.inventory_items on that reconciliation page, when all I want is for 1 to be displayed.

So, what I would like to do is this:

  • Get the collection of inventory_items in the location that the user has chosen.
  • Begin iteration over that collection and show the user each object, in their own view, one at a time.
  • As the user progresses (i.e. once they press 'Next'), essentially mark that inventory_item as reconciled even if the user didn't update the quantity (i.e. say there have been no sold since it was delivered).
  • Once all inventory_items in this collection are iterated over, then save the reconciliation record to the database that accurately reflects the quantity information for each inventory_item within this reconiliation cycle.

I looked at the Wicked Gem, but it seems that I need to be able to statically declare the number of steps in advance. As you can see above, if each inventory_item in my collection is a step, there needs to be a dynamic number of steps.

I also came across similar constraints with other multi-step wizard approaches.

How do I achieve what I am trying to do?

marcamillion
  • 32,933
  • 55
  • 189
  • 380
  • @SimpleLime Good suggestion. The biggest issue with that is that I need the app to be fast, so if there are a ton of objects & images it is can take forever to load (especially on a mobile device). So I am trying to keep a low footprint and keep it peppy...hence my pagination approach. – marcamillion Mar 24 '18 at 19:44
  • this is an interesting question and I will come back to you later for a technical solution, but I do believe you are making your solution slightly complex. I see from your profile that you have an interest for business and maybe also accounting, as you want to structure this functionality with `reconciliation` accounts `inventory` and `items`. Sorry for my too broad answer. I also found myself in my latest project https://github.com/fabriziobertoglio1987/sprachspiel having to record purchases and sales between users. – Fabrizio Bertoglio Apr 05 '18 at 16:44
  • 1) My first approach was to record `purchases` and `sales` 2) My second approach got me into trouble by using and improving the https://github.com/mbulat/plutus gem 3) My third approach was deleting all purchases, sales, account, reconciliation accoutns etc... and just keeping `items` and `users` later I'll post a possible technical solution. sorry for talking so broadly – Fabrizio Bertoglio Apr 05 '18 at 16:46

3 Answers3

3

What is a reconciliation?

i) “A procedure for confirming that the balance in a chequebook matches the corresponding bank statement. This is normally done by preparing a bank reconciliation statement.

ii) A procedure for confirming the reliability of a company’s accounting records by regularly comparing [balances of transactions]. An account reconciliation may be prepared on a daily, monthly, or annual basis.”

user has_many items

warehouse has_many accounts

accounts has_many items

warehouse has_many items, throught: :accounts

item belongs_to warehouse

class Product
   has_many :items
   accepts_nested_attributes_for :items
end

class Item
   belongs_to :product
end

class Account
   has_many :products
   has_many :warehouses, through: :products
end

class Warehouse 
   has_many :products
   has_many :accounts, through: :products
end

Your routes.rb

resources :warehouses do 
    resources :products
    resources :accounts
end

resources :accounts do 
    resources :products
    resources :warehouses
end

resources :products do 
    resources :items
end

The account model will have a reconciled field.

This is your form. It is displaying a Product.

enter image description here

form_for @product do |f|
   <!-- your fields: account_id and number of items--> 
   f.fields_for :items do |item|
     <!-- your item -->
   end
   f.submit
end

ProductsController#update

def update
    warehouse = Warehouse.find(params[:warehouse_id])
    @product = warehouse.products.first
    # needs to be refactored
    if @product.update && @product.is_the_last_one
       redirect_to :your_choosen_root
    elsif @product.update 
       redirect_to edit_product_path(@product.next)
    else
       render :edit
    end
end
    

Once all inventory_items in this collection are iterated over, then save the reconciliation record to the database that accurately reflects the quantity information for each inventory_item within this reconiliation cycle.

I'll think about it tomorrow (it's 22:55) :)

Community
  • 1
  • 1
Fabrizio Bertoglio
  • 5,890
  • 4
  • 16
  • 57
1

Like Simple Lime said...I skimmed. That said, I agree with rethinking having standard pagination and save on click. I'd be pretty peeved if my thumb hit something and then had to start over.

Anyway, I would set the display of each content block to display="none" and wait wait for selectionOnChange or onFormSubmit or whatever the previous block is waiting for - if I understand your use case correctly.

Miranda Short
  • 94
  • 1
  • 11
  • But if I load everything and use JS in a carousel, how does that solve the problem if my finger hits something and I have to start over? Wouldn't that be the same issue? – marcamillion Mar 24 '18 at 19:42
  • It'd be the same issue, I was only saying that as a caveat. To avoid JS you could try the HTML onChange stuff, but it's probably going to be more preferable to set each page as a partial and render it all in as hared view that is saved upon each submit. To be honest, trying to load it all in one page is what _is_ going to keep it weighed down which is the opposite of what you want. – Miranda Short Mar 25 '18 at 00:49
  • Yeh that's what I figured. I am definitely trying to set each page as a partial and render it as a shared view, when needed....as opposed to everything at once. – marcamillion Mar 25 '18 at 04:04
  • Rendering as a shared view on one page is still a lot of stuff to load on one page (speaking from experience). If the individual views are heavy, it's just the same as loading it all once. – Miranda Short Mar 25 '18 at 05:45
  • Sorry...my bad. I should have been more clear. What I am trying to do is render each object in it's own dedicated view. – marcamillion Mar 25 '18 at 06:04
1

My approach. Start an empty reconciliation (new and create methods). This creates a reconciliation with all inventory items copied from the items in the location. After creating the reconciliation, you are redirected to the edit form, which only edits one item. The update method redirects again to the update, but for the next item... There can be many errors as I don´t know all your models and have not tested (has wrote here directly). But the idea should work. Just adapt to your needs.

ReconciliationsController

def new
  @location = Location.find(params[:location_id])
  # Start reconciliation (without items)
  @reconciliation = @location.reconciliations.new
end

def create
  @location = Location.find(params[:location_id])
  # Create an empty reconciliation (params do not include items)
  @reconciliation = @location.reconciliations.new(reconciliation_params)

  @inventory_items = @location.inventory_items

  # Add items from location (this may require some changes depending on how you defined your models
  # If it does not work, you can create all inventory items in a loop
  # for each @inventory_items
  @reconciliation.inventory_items = @inventory_items

  if @reconciliation.save
    #Start editing reconciliation, from first item
    redirect_to edit_reconciliation_path(@reconciliation, item: 0)
  else
    redirect_to 'new'
  end

end

def edit
  @reconciliation = Reconciliation.find(params[:id])
  @inventory_items = @reconciliation.inventory_items.order(:id)
  @inventory_item = @inventory_items[params[:item]]
  @item = params[:item].to_i + 1
  @item = -1 if @inventory_items[@item].nil? # End condition.
end

def update
  @reconciliation = Reconciliation.find(params[:id])
  @reconciliation.update_attributes(reconciliation_params)
  if params[:item] != -1
    redirect_to edit_reconciliation_path(@reconciliation, item: item)
  else
    redirect_to root_path
  end
end

views/reconciliations/new

<%= simple_form_for @reconciliation do |f| %>
  .... fields for the reconciliation.
  .... Don't include the form for inventory_items
  .... So that you create an empty reconciliation
  <%= hidden_field :location_id %>
  <%= f.submit "Start reconciliation %>
<% end %>

views/reconciliations/edit

<%= simple_form_for @reconciliation do |f| %>
  .... fields for the reconciliation.
  .... Include the form for only 1 inventory_item

  <%= f.simple_fields_for @inventory_item do |ff| %>

    <%= @inventory_item.product.name %> <br />
    </strong> <%= @inventory_item.quantity_left %> <br />
    </strong> <%= @inventory_item.quantity_delivered %> <br />
    ... Fields to change data
    <%= hidden_field :item %>

  <% end %>

  <%= f.submit (@item == -1 ? "Finish" : "Next") %>
<% end %>
Pablo
  • 3,004
  • 1
  • 12
  • 19