Skip to content

Commit

Permalink
Add the adjustments index page
Browse files Browse the repository at this point in the history
  • Loading branch information
elia committed Nov 27, 2023
1 parent 33e744b commit 51dae23
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<%= page do %>
<%= page_header do %>
<%= page_header_back(solidus_admin.order_path(@order)) %>
<%= page_header_title(t(".title", number: @order.number)) %>
<%= page_header_actions do %>
<%= render component("ui/button").new(
tag: :button,
scheme: :secondary,
text: t(".discard"),
form: form_id
) %>
<%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %>
<% end %>
<% end %>

<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>
<%= render component("ui/table").new(
id: "order-adjustments-list",
model_class: Spree::Adjustment,
rows: @adjustments,
search_key: :label_cont,
search_url: solidus_admin.order_adjustments_path(@order),
batch_actions: [
{
display_name: t(".batch_actions.lock"),
action: solidus_admin.lock_order_adjustments_path(@order),
method: :put,
icon: "lock-line"
},
{
display_name: t(".batch_actions.unlock"),
action: solidus_admin.unlock_order_adjustments_path(@order),
method: :put,
icon: "lock-unlock-line"
},
{
display_name: t(".batch_actions.delete"),
action: spree.admin_order_adjustment_path(@order, '[]'),
method: :delete,
icon: "delete-bin-7-line"
},
],
columns: [
{
header: :state,
col: { class: 'w-[calc(5rem+2rem+2.5rem+1px)]' },
data: ->(adjustment) {
if adjustment.finalized?
icon = 'lock-fill'
title = t(".state.locked")
else
icon = 'lock-unlock-line'
title = t(".state.unlocked")
end
icon_tag(icon, class: "w-5 h-5 align-middle") + tag.span(title)
}
},
{
header: :adjustable,
col: { class: 'w-56' },
data: ->(adjustment) {
tag.figure(safe_join([
render(component("ui/thumbnail").for(adjustment.adjustable, class: "basis-10")),
figcaption_for_adjustable(adjustment),
]), class: "flex items-center gap-2")
}
},
{
header: :source,
col: { class: "w-56" },
data: ->(adjustment) {
tag.figure(safe_join([
figcaption_for_source(adjustment),
]), class: "flex items-center gap-2")
}
},
{
header: :amount,
col: { class: 'w-24' },
data: ->(adjustment) { tag.span adjustment.display_amount.to_html, class: "grow text-right whitespace-nowrap" }
},
{
header: tag.span(t(".actions.title"), class: 'sr-only'),
col: { class: 'w-24' },
wrap: false,
data: ->(adjustment) do
render(component('ui/dropdown').new(direction: :right, class: 'relative w-fit m-auto').with_content(
(adjustment.source ? ''.html_safe : link_to(
t(".actions.edit"),
spree.edit_admin_order_adjustment_path(@order, adjustment),
class: 'body-link',
)) + if adjustment.finalized?
link_to(
t(".actions.unlock"),
solidus_admin.unlock_order_adjustments_path(@order, id: adjustment),
"data-turbo-method": :put,
"data-turbo-confirm": t('.confirm'),
class: 'body-link',
)
else
safe_join([
link_to(
t(".actions.lock"),
solidus_admin.lock_order_adjustments_path(@order, id: adjustment),
"data-turbo-method": :put,
"data-turbo-confirm": t('.confirm'),
class: 'body-link',
),
link_to(
t(".actions.delete"),
spree.admin_order_adjustment_path(@order, adjustment),
"data-turbo-method": :delete,
"data-turbo-confirm": t('.confirm'),
class: 'body-link !text-red-500',
)
])
end
))
end
},
]
) %>
<% end %>

<%= page_with_sidebar_aside do %>
<%= render component('ui/panel').new(title: t(".totals.adjustable")) do |panel| %>
<% panel.with_section do %>
<%= render component('ui/details_list').new(items: @adjustments.group(:adjustable_type).sum(:amount).map { |type, amount|
{ label: (type ? type.constantize.model_name.human : tag.em(t('.none'))), value: Spree::Money.new(amount).to_html }
}.sort_by { _1[:label] }) %>
<% end %>
<% end %>

<%= render component('ui/panel').new(title: t(".totals.source")) do |panel| %>
<% panel.with_section do %>
<%= render component('ui/details_list').new(items: @adjustments.group(:source_type).sum(:amount).map { |type, amount|
{ label: (type ? type.constantize.model_name.human : tag.em(t('.none'))), value: Spree::Money.new(amount).to_html }
}.sort_by { _1[:label] }) %>
<% end %>
<% end %>
<% end %>
<% end %>

<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

class SolidusAdmin::Orders::Show::Adjustments::Index::Component < SolidusAdmin::BaseComponent
include SolidusAdmin::Layout::PageHelpers
NBSP = "&nbsp;".html_safe

def initialize(order:, adjustments:)
@order = order
@adjustments = adjustments
end

def form_id
@form_id ||= "#{stimulus_id}--form-#{@order.id}"
end

def icon_thumbnail(name)
render component("ui/thumbnail").new(src: svg_data_uri(icon_tag(name)))
end

def svg_data_uri(data)
"data:image/svg+xml;base64,#{Base64.strict_encode64(data)}"
end

def figcaption_for_source(adjustment)
return thumbnail_caption(adjustment.label, nil) unless adjustment.source_type

# ["Spree::PromotionAction", nil, "Spree::UnitCancel", "Spree::TaxRate"]
record = adjustment.source
record_class = adjustment.source_type&.constantize
model_name = record_class.model_name.human

case record || record_class
when Spree::TaxRate
detail = link_to("#{model_name}: #{record.zone&.name || t('spree.all_zones')}", spree.edit_admin_tax_rate_path(adjustment.source_id), class: "body-link")
when Spree::PromotionAction
detail = link_to("#{model_name}: #{record.promotion.name}", spree.edit_admin_promotion_path(adjustment.source_id), class: "body-link")
else
detail = "#{model_name}: #{record.display_amount}" if record.respond_to?(:display_amount)
end

thumbnail_caption(adjustment.label, detail)
end

def figcaption_for_adjustable(adjustment)
# ["Spree::LineItem", "Spree::Order", "Spree::Shipment"]
record = adjustment.adjustable
record_class = adjustment.adjustable_type&.constantize

case record || record_class
when Spree::LineItem
variant = record.variant
options_text = variant.options_text.presence

description = options_text || variant.sku
detail = link_to(variant.product.name, solidus_admin.product_path(record.variant.product), class: "body-link")
when Spree::Order
description = "#{Spree::Order.model_name.human} ##{record.number}"
detail = record.display_total
when Spree::Shipment
description = "#{t('spree.shipment')} ##{record.number}"
detail = link_to(record.shipping_method.name, spree.edit_admin_shipping_method_path(record.shipping_method), class: "body-link")
when nil
# noop
else
name_method = [:display_name, :name, :number].find { record.respond_to? _1 } if record
price_method = [:display_amount, :display_total, :display_cost].find { record.respond_to? _1 } if record

description = record_class.model_name.human
description = "#{description} - #{record.public_send(name_method)}" if name_method

# attempt creating a link
url_options = [:admin, record, :edit, { only_path: true }]
url = begin; spree.url_for(url_options); rescue NoMethodError => e; logger.error(e.to_s); nil end

description = link_to(description, url, class: "body-link") if url
detail = record.public_send(price_method) if price_method
end

thumbnail_caption(description, detail)
end

def thumbnail_caption(first_line, second_line)
tag.figcaption(safe_join([
tag.div(first_line || NBSP, class: 'text-black body-small whitespace-nowrap text-ellipsis overflow-hidden'),
tag.div(second_line || NBSP, class: 'text-gray-500 body-small whitespace-nowrap text-ellipsis overflow-hidden')
]), class: "flex flex-col gap-0 max-w-[15rem]")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
en:
title: "Order %{number} / Adjustments"
save: "Save"
discard: "Discard"
none: ""
actions:
title: "Actions"
delete: "Delete"
lock: "Lock"
unlock: "Unlock"
edit: "Edit"
state:
locked: "Locked"
unlocked: "Unlocked"
confirm: "Are you sure?"

totals:
adjustable: "Totals (by Adjustable)"
source: "Totals (by Source)"
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{ label: t('.taxes'), value: number_to_currency(@order.additional_tax_total) },
{ label: t('.shipping'), value: number_to_currency(@order.shipment_total) },
{ label: link_to(t('.add_promo_code'), '#', class: "body-link"), value: number_to_currency(@order.promo_total) },
{ label: link_to(t('.adjustments'), '#', class: "body-link"), value: number_to_currency(@order.adjustment_total) },
{ label: link_to(t('.adjustments'), solidus_admin.order_adjustments_path(@order), class: "body-link"), value: number_to_currency(@order.adjustment_total) },
{ label: t('.total'), value: number_to_currency(@order.total), class: 'font-semibold' }
]
) %>
Expand Down
55 changes: 55 additions & 0 deletions admin/app/controllers/solidus_admin/adjustments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

class SolidusAdmin::AdjustmentsController < SolidusAdmin::BaseController
before_action :load_order

def index
@adjustments = @order
.all_adjustments
.eligible
.order("adjustable_type ASC, created_at ASC")
.ransack(params[:q])
.result

set_page_and_extract_portion_from(@adjustments)

respond_to do |format|
format.html do
render component('orders/show/adjustments/index').new(
order: @order,
adjustments: @adjustments,
)
end
end
end

def lock
@adjustments = @order.all_adjustments.not_finalized.where(id: params[:id])
@adjustments.each(&:finalize!)
flash[:success] = t('.success')

redirect_to order_adjustments_path(@order)
end

def unlock
@adjustments = @order.all_adjustments.finalized.where(id: params[:id])
@adjustments.each(&:unfinalize!)
flash[:success] = t('.success')

redirect_to order_adjustments_path(@order)
end

def destroy
@adjustment = @order.all_adjustments.find(params[:id])
@adjustment.destroy
flash[:success] = t('.success')

redirect_to order_adjustments_path(@order)
end

private

def load_order
@order = Spree::Order.find_by!(number: params[:order_id])
end
end
8 changes: 8 additions & 0 deletions admin/config/locales/adjustments.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
en:
solidus_admin:
adjustments:
title: "Adjustments"
lock:
success: "Locked successfully"
unlock:
success: "Unlocked successfully"
7 changes: 7 additions & 0 deletions admin/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
end

resources :orders, except: [:destroy] do
resources :adjustments, only: [:index] do
collection do
delete :destroy
put :lock
put :unlock
end
end
resources :line_items, only: [:destroy, :create, :update]
resource :customer
resource :ship_address, only: [:show, :edit, :update], controller: "addresses", type: "ship"
Expand Down
34 changes: 34 additions & 0 deletions admin/spec/features/orders/adjustments_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require 'spec_helper'

describe "Order", :js, type: :feature do
before { sign_in create(:admin_user, email: '[email protected]') }

it "allows detaching a customer from an order" do
order = create(:order, number: "R123456789", user: create(:user))
Spree::Adjustment.insert_all([
{
order_id: order.id,
adjustable_id: order.id,
adjustable_type: "Spree::Order",
amount: 10,
label: "Test Adjustment",
eligible: true,
finalized: false,
created_at: Time.current,
updated_at: Time.current,
included: false,
source_type: "Spree::Order",
source_id: order.id,
promotion_code_id: nil,
},
])
visit "/admin/orders/R123456789"

click_on "Adjustments"
expect(page).to have_content("Test Adjustment")

expect(page).to be_axe_clean
end
end
2 changes: 2 additions & 0 deletions core/app/models/spree/adjustment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class Adjustment < Spree::Base

singleton_class.deprecate :return_authorization, deprecator: Spree.deprecator

allowed_ransackable_attributes << 'label'

extend DisplayMoney
money_methods :amount

Expand Down

0 comments on commit 51dae23

Please sign in to comment.