Created
October 13, 2021 19:40
-
-
Save iftheshoefritz/844e96aa340a95082f69add25775f3df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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