Skip to content

Instantly share code, notes, and snippets.

@iftheshoefritz
Created October 13, 2021 19:40
Show Gist options
  • Save iftheshoefritz/844e96aa340a95082f69add25775f3df to your computer and use it in GitHub Desktop.
Save iftheshoefritz/844e96aa340a95082f69add25775f3df to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
class ProductsController < ApplicationController
include ProductWithQuoteFormParams
before_action :redirect_admins
before_action :set_organization
before_action :set_product,
only: %i[edit update destroy make_public make_private] # TODO why the last two actions?
authorize_resource
helper_method :organization
# GET /products/new
def new
@product_with_quote = ProductWithQuoteForm.new(organization: @organization)
end
# GET /products/1/edit
def edit; end
# POST /products
def create
product_quote_form = ProductWithQuoteForm.new(organization: @organization,
attributes: product_with_quote_params)
product_quote_form.save
@product = product_quote_form.object # TODO probably don't need @product, do we need product at all?
@product_with_quote = product_quote_form
if @product.save # TODO why doesn't save happen on product already?
redirect_to shop_url(id: @organization),
notice: 'Product was successfully created.'
else
@product_errors_messages = @product.errors.full_messages # TODO get the errors from the form object
render :new
end
end
private
# def save_product TODO INLINE
# product_quote_form = ProductWithQuoteForm.new(organization: @organization,
# attributes: product_with_quote_params)
# @product, @product_with_quote = product_quote_form.save_product
# end
def product_params
params.require(:product)
.permit(:name, :description, :depth_cm, :height_cm, :length_cm,
:picking_address, :picking_city, :price, :product_payment,
:cover, :public, :lot_url, images: [])
end
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = scoped_products.find_by!(slug: (params[:product_id] || params[:id]))
end
# TODO moved down below the first place it gets used
def scoped_products
@organization.products
end
def set_organization
@organization = current_user.organization
end
def current_ability
@current_ability ||= VendorAbility.new(current_user)
end
def redirect_admins
redirect_to admin_products_path if current_user.admin?
end
end
# frozen_string_literal: true
class QuotesController < ApplicationController
include ProductWithQuoteFormParams
authorize_resource class: false
before_action :set_organization
def create
@product, @new_quote, @product_with_quote = ProductWithQuoteForm.new(organization: @organization,
attributes: product_with_quote_params)
.save
product_creation_redirection
end
private
def product_creation_redirection
respond_to do |format|
if @product.persisted? && @quote.persisted?
format.html do
redirect_to shop_url(id: @product.organization),
notice: 'Product and Quote were successfully created.'
end
else
@product_errors_messages = @product.errors.full_messages
format.html { render 'products/new' }
end
end
end
def current_ability
@current_ability ||= VendorAbility.new(current_user)
end
def set_organization
@organization = current_user.organization
end
end
# frozen_string_literal: true
class ProductQuotationsController < ApplicationController
include ProductWithQuoteFormParams
before_action :set_organization
authorize_resource class: false
def create
@quote = ProductWithQuoteForm.new(organization: @organization,
attributes: product_with_quote_params)
.compute_price
render json: { price: @quote.price.round(2) }
end
private
def current_ability
@current_ability ||= VendorAbility.new(current_user)
end
def set_organization
@organization = current_user.organization
end
end
# frozen_string_literal: true
module ProductWithQuoteFormParams
extend ActiveSupport::Concern
def product_with_quote_params
sanitize_picking_address
params.require(:product_with_quote_form)
.permit(:white_glove, :insured, :delivery_address_short_name,
:delivery_address_postal_code, :delivery_address,
:name, :description, :depth_cm, :height_cm, :length_cm,
:picking_address_id, :picking_city, :picking_address, :price,
:product_payment, :sku, :cover, :public, :lot_url, images: [])
end
private
def sanitize_picking_address
return if picking_address_id.blank?
picking_address = current_user.organization.addresses.find picking_address_id
params[:product_with_quote_form][:picking_address] = picking_address.raw_address
params[:product_with_quote_form][:picking_city] = picking_address.city
end
def picking_address_id
params[:product_with_quote_form][:picking_address_id]
end
end
Here is the form object I’ve created
# frozen_string_literal: true
class ProductWithQuoteForm
include ActiveModel::Model
PRODUCT_ATTRIBUTES = %i[name description depth_cm height_cm length_cm
picking_address picking_city price product_payment
picking_address picking_address_city
cover public lot_url sku image].freeze
DELIVERY_ADDRESS_ATTRIBUTES = [:delivery_address_short_name,
:delivery_address_postal_code,
:delivery_address].freeze
OPTIONS_ATTRIBUTES = [:white_glove, :insured].freeze
attr_accessor :organization, :attributes, :picking_address_id, # TODO probably have attributes for free
*(PRODUCT_ATTRIBUTES + DELIVERY_ADDRESS_ATTRIBUTES + OPTIONS_ATTRIBUTES)
def initialize(organization:, attributes: {})
@attributes = attributes # TODO super(attributes) should cut out the attributes.each below
@organization = organization
attributes.each { |k, v| instance_variable_set("@#{k}", v) unless v.nil? }
@public = attributes[:public].presence || false
end
def save
ActiveRecord::Base.transaction do
save_product
new_quote.create if @product.valid?
[@product, new_quote, self]
end
end
def save_product
@product = @organization.products
.create(
attributes.slice(*PRODUCT_ATTRIBUTES)
)
[@product, self]
end
def compute_price
new_quote.pricing
end
private
def new_quote
Quote.new(quote_params)
end
def quote_params
{
organization: organization,
product: attributes.slice(*PRODUCT_ATTRIBUTES),
options: attributes.slice(*OPTIONS_ATTRIBUTES),
delivery_address: attributes.slice(*DELIVERY_ADDRESS_ATTRIBUTES)
}
end
end
# frozen_string_literal: true
class Quote
attr_reader :organization, :product, :delivery_address,
:options, :picking_address, :quote
def initialize(organization:, product:, delivery_address:, options:)
@organization = organization
@product = product
@delivery_address = delivery_address
@options = options
set_picking_address
end
def pricing
set_quote_details
compute_price
quote
end
def create
set_quote_details
quote.save
quote
end
private
def set_quote_details
set_quote_base
add_delivery_address_to_quote
add_item_to_quote
add_options_to_quote
end
def set_picking_address
@picking_address = organization.addresses
.find_by(raw_address: product[:picking_address])
end
def set_quote_base
@quote = Quotes::Base.new(organization, picking_address).call
end
def add_delivery_address_to_quote
Quotes::DeliveryAddress.new(quote, delivery_address).call
end
def add_item_to_quote
Quotes::Item.new(quote, product).call
end
def add_options_to_quote
Quotes::Options.new(quote, options).call
end
def compute_price
quote.assign_computed_fields
end
end
# frozen_string_literal: true
module Quotes
class Base
attr_accessor :organization, :picking_address
def initialize(organization, picking_address)
@organization = organization
@picking_address = picking_address
end
def call
Order.new(
organization_id: organization.id,
seller: organization.seller,
order_type: 'vendor',
pickup_address: picking_address.raw_address,
addresses: [picking_address],
quote: true
)
end
end
end
# frozen_string_literal: true
module Quotes
class DeliveryAddress
attr_reader :quote, :address
def initialize(quote, address)
@quote = quote
@address = address
end
def call
quote.delivery_address_short_name = address[:delivery_address_short_name]
quote.delivery_address_postal_code = address[:delivery_address_postal_code]
quote.delivery_address = address[:delivery_address]
build_address
quote
end
private
def build_address
quote.addresses.build(category: 'delivery',
addressable_type: 'Order',
country_code: address[:delivery_address_short_name],
postal_code: address[:delivery_address_postal_code],
raw_address: address[:delivery_address])
end
end
end
# frozen_string_literal: true
module Quotes
class Item
attr_accessor :quote, :product
attr_reader :sku, :description
# reader item?
def initialize(quote, product)
@quote = quote
@product = product
@sku = product[:sku]
@description = product[:description]
end
def call
quote.items.build(
depth: product[:depth_cm],
length: product[:length_cm],
height: product[:height_cm],
value: product[:price],
description: item_description
)
quote
end
private
def item_description
return unless sku.present? && description.present?
"Lot #{sku.split('_').last} #{description}"
end
end
end
# frozen_string_literal: true
module Quotes
class Options
attr_reader :quote, :options
def initialize(quote, options)
@quote = quote
@options = options
end
def call
options.each do |attribute, state|
quote.send("#{attribute}=", state)
end
quote
end
end
end
<div style="background-image: linear-gradient(to right top, #113c82, #0a4d92, #045fa2, #0570b1, #1282bf); border-radius: 0">
<div class="container-fluid py-5" data-controller="product-quotation">
<div class="row">
<div class="col-md-6 offset-md-2">
<h1><%= content_for :title %></h1>
<p><%= content_for :links %>
<%= simple_form_for(@product_with_quote, url: products_path(), wrapper: :simple, html: {'data-product-quotation-target': "form"}) do |f| %>
<% if @product_errors_messages.present? %>
<div class="alert alert-danger" id="error_explanation">
<h4><%= pluralize(@product_errors_messages.count, "error") %> prohibited this order from being saved:</h4>
<ul>
<% @product_errors_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<h3>Calculer votre prix</h3>
<div class="form-group">
<div class="form-row">
<div class="col-sm-4 col-md-4 col-lg-4 mb-4">
<%= f.input :depth_cm,
input_html: {
placeholder: Product.human_attribute_name(:depth_cm),
data: { 'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' }
} %>
</div>
<div class="col-sm-4 col-md-4 col-lg-4 mb-4">
<%= f.input :height_cm,
input_html: {
placeholder: Product.human_attribute_name(:height_cm),
data: { 'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' }
} %>
</div>
<div class="col-sm-4 col-md-4 col-lg-4 mb-4">
<%= f.input :length_cm,
input_html: {
placeholder: Product.human_attribute_name(:length_cm),
data: { 'product-quotation-target': 'input', action: 'change->product-quotation#displayPrice' }
} %>
</div>
</div>
<div class="form-row">
<div class="col-sm-12 col-md-6 col-lg-4 mb-4">
<%= f.input :price,
input_html: {
placeholder: Product.human_attribute_name(:price),
data: { 'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' }
} %>
</div>
</div>
<div class="form-row">
<div class="col-sm-4 col-md-6 col-lg-4 mb-4">
<%= f.input :picking_address_id,
input_html: {
data: { 'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' },
placeholder: Product.human_attribute_name(:picking_address)
}, collection: @organization.addresses.picking.map {|address| [address.raw_address, address.id]}
%>
</div>
<div class="col-sm-4 col-md-6 col-lg-4 mb-4" data-controller="places-autocomplete" data-places-autocomplete-role="delivery">
<%= f.text_field :delivery_address,
class: "form-control form-control-lg",
data: { 'places-autocomplete-target': 'input',
'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' } %>
<div data-controller='address-completion'
data-address-completion-listen-to='place_autocomplete.delivery'>
<%= f.hidden_field :delivery_address_postal_code,
data: { 'address-completion-target': 'postalCode' } %>
<%= f.hidden_field :delivery_address_short_name,
data: { 'address-completion-target': 'countryCode',
'product-quotation-target': 'input',
action: 'change->product-quotation#displayPrice' } %>
</div>
</div>
<div class="col-sm-4 col-md-6 col-lg-4 mb-4">
<div class="d-flex text-center justify-content-around align-items-center" style='height:100%;'>
<div>
<%= f.check_box :insured,
class: "switch",
data: { action: 'change->product-quotation#displayPrice' } %>
<label class="text-white" for="insured">
Assurance
</label>
</div>
<div>
<%= f.check_box :white_glove,
class: "switch",
data: { action: 'change->product-quotation#displayPrice' } %>
<label class="text-white" for="white_glove">
Gant Blanc
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<h3>Intégrer le produit à votre inventaire</h3>
<div class="form-group form-row">
<div class="col-12">
<%= f.input :name,
placeholder: Product.human_attribute_name(:name) %>
</div>
</div>
<div class="form-group form-row">
<div class="col-12">
<%= f.input :description,
placeholder: Product.human_attribute_name(:description) %>
</div>
</div>
<div class="form-group form-row">
<div class="col-12">
<%= f.input :lot_url,
placeholder: Product.human_attribute_name(:lot_url) %>
</div>
<div class="col-12">
<%= f.input :sku,
placeholder: Product.human_attribute_name(:sku) %>
</div>
</div>
<div class='d-flex'>
<div>
<div class="form-group form-row mb-3">
<div class="col">
<%= f.label :cover %><br>
<%= f.file_field :cover %>
</div>
</div>
<div class="form-group form-row">
<div class="col">
<%= f.label :images %><br>
<%= f.file_field :images, multiple: true %>
</div>
</div>
</div>
<div class="form-group form-row align-items-center">
<div class="col-sm-12 col-md-6 col-lg-4 mb-4 pt-3">
<span class="switch switch-md">
<input type="checkbox" class="switch">
<%= f.input :public,
placeholder: Product.human_attribute_name(:public),
input_html: { class: 'switch', input_options: { checked: @product_with_quote.public } }%>
</span>
</div>
</div>
</div>
</div>
<div class="form-actions">
<%= f.button :submit, class: 'btn btn-primary' %>
<%= f.button :submit, 'Enregistrer le devis',
class: 'btn btn-secondary',
formaction: quotes_path(),
disabled: true,
'data-product-quotation-target': "saveButton"
%>
</div>
<% end %>
</div>
<div>
<div class='d-flex'>
<div data-product-quotation-target="displayedPrice" id='displayedPrice'>--,--</div>
<div class='ml-1'>€</div>
</div>
<div>options</div>
</div>
</div>
</div>
</div>
import ApplicationController from './application_controller'
export default class extends ApplicationController {
static targets = ['input', 'displayedPrice', 'saveButton', 'form']
displayPrice (event) {
const requiredTargetsNotFilled = (targets) => targets.map(el => !!el.value).includes(false)
if (requiredTargetsNotFilled(this.inputTargets)) {
this.displayedPriceTarget.innerHTML = '--,--'
this.saveButtonTarget.disabled = true
} else {
var url = `/product_quotations`
fetch(url, {
method: 'POST',
body: new FormData(this.formTarget)
})
.then(response => {
if (response.ok) { response.json().then(data => this.setPrice(data['price'])) }
})
}
}
setPrice (price) {
this.displayedPriceTarget.innerHTML = price
this.saveButtonTarget.disabled = false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment