Skip to content

Commit f3afa60

Browse files
Refactor admin invoice show page and routes for better UX and Turbo compatibility
- Refactored : - Simplified action buttons layout and updated button classes for consistency with the rest of the UI. - Replaced the info block grid with a clean two-column table for invoice details. - Unified table styles for invoice items and payment orders, using consistent custom classes. - Improved markup structure for clarity and maintainability. - Updated : - Added and under the scope for admin. - Removed legacy member route. - Ensured RESTful and Turbo-friendly routing for invoice actions. - Cleaned up controller logic (see diff) to match new routes and UI flow. These changes improve the maintainability, consistency, and Turbo/Hotwire compatibility of the admin invoice management interface.
1 parent ce7950e commit f3afa60

File tree

8 files changed

+249
-111
lines changed

8 files changed

+249
-111
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module Admin
2+
class Invoices::MarkAsPaidsController < BaseController
3+
rescue_from Errors::InvoiceAlreadyPaid, with: :invoice_already_paid
4+
5+
before_action :set_invoice
6+
before_action :authorize_user
7+
before_action :authorize_for_update
8+
9+
def edit; end
10+
11+
def update
12+
raise(Errors::InvoiceAlreadyPaid, @invoice.id) if @invoice.paid?
13+
14+
@invoice.assign_attributes(invoice_params)
15+
@invoice.mark_as_paid_at(invoice_params[:paid_at])
16+
@invoice.save!
17+
18+
flash[:notice] = t('invoices.marked_as_paid')
19+
redirect_to admin_invoice_path(@invoice), status: :see_other
20+
end
21+
22+
private
23+
24+
def set_invoice
25+
@invoice = Invoice.find(params[:invoice_id])
26+
end
27+
28+
def invoice_already_paid
29+
flash[:alert] = t('invoices.already_paid')
30+
redirect_to admin_invoice_path(@invoice), status: :see_other
31+
end
32+
33+
def authorize_user
34+
authorize! :read, Invoice
35+
end
36+
37+
def authorize_for_update
38+
authorize! :update, @invoice
39+
end
40+
41+
def invoice_params
42+
params.require(:invoice).permit(:notes, :paid_at)
43+
end
44+
end
45+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Admin
2+
class Invoices::TogglePartialPaymentsController < BaseController
3+
before_action :set_invoice
4+
before_action :authorize_user
5+
before_action :authorize_for_update
6+
7+
def update
8+
if @invoice.toggle(:partial_payments).save
9+
action = @invoice.partial_payments? ? 'activated' : 'deactivated'
10+
redirect_to admin_invoice_path(@invoice), notice: t("invoices.partial_payments_#{action}"), status: :see_other
11+
else
12+
redirect_to admin_invoice_path(@invoice), alert: t(:something_went_wrong), status: :see_other
13+
end
14+
rescue StandardError => e
15+
Rails.logger.error "Error toggling partial payments: #{e.message}"
16+
redirect_to admin_invoice_path(@invoice), alert: t(:something_went_wrong), status: :see_other
17+
end
18+
19+
def set_invoice
20+
@invoice = Invoice.find(params[:invoice_id])
21+
end
22+
23+
def authorize_user
24+
authorize! :read, Invoice
25+
end
26+
27+
def authorize_for_update
28+
authorize! :update, @invoice
29+
end
30+
end
31+
end

app/controllers/admin/invoices_controller.rb

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
module Admin
55
class InvoicesController < BaseController
66
before_action :authorize_user
7-
before_action :create_invoice_if_needed, except: :toggle_partial_payments
8-
before_action :set_invoice, only: %i[show download update edit toggle_partial_payments]
9-
before_action :authorize_for_update, only: %i[edit update]
7+
before_action :create_invoice_if_needed
8+
before_action :set_invoice, only: %i[show download]
109

1110
# GET /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b
1211
def show
@@ -24,7 +23,6 @@ def index
2423
else
2524
@pagy, @invoices = pagy(invoices, items: params[:per_page] ||= 15)
2625
end
27-
2826
end
2927

3028
# GET /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b/download
@@ -35,80 +33,16 @@ def download
3533
send_data(raw_pdf, filename: @invoice.filename)
3634
end
3735

38-
# GET /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b/edit
39-
def edit
40-
if @invoice.paid?
41-
respond_to do |format|
42-
format.html do
43-
redirect_to admin_invoice_path(@invoice), notice: t('invoices.already_paid')
44-
end
45-
format.json { render json: @invoice.errors, status: :unprocessable_entity }
46-
end
47-
end
48-
end
49-
50-
# PUT /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b
51-
def update
52-
respond_to do |format|
53-
if update_predicate
54-
format.html do
55-
redirect_to admin_invoice_path(@invoice), notice: t('invoices.marked_as_paid')
56-
end
57-
format.json { render :show, status: :ok, location: @invoice }
58-
else
59-
format.html { redirect_to admin_invoice_path(@invoice), notice: t(:something_went_wrong) }
60-
format.json { render json: @invoice.errors, status: :unprocessable_entity }
61-
end
62-
end
63-
rescue Errors::InvoiceAlreadyPaid
64-
respond_to do |format|
65-
format.html { redirect_to admin_invoice_path(@invoice), notice: t('invoices.already_paid') }
66-
format.json { render json: @invoice.errors, status: :unprocessable_entity }
67-
end
68-
end
69-
70-
# POST /admin/invoices/aa450f1a-45e2-4f22-b2c3-f5f46b5f906b/toggle_partial_payments
71-
def toggle_partial_payments
72-
respond_to do |format|
73-
if @invoice.toggle(:partial_payments).save
74-
format.html do
75-
action = @invoice.partial_payments? ? 'activated' : 'deactivated'
76-
redirect_to admin_invoice_path(@invoice), notice: t("invoices.partial_payments_#{action}")
77-
end
78-
format.json { render :show, status: :ok, location: @invoice }
79-
else
80-
format.html { redirect_to admin_invoice_path(@invoice), notice: t(:something_went_wrong) }
81-
format.json { render json: @invoice.errors, status: :unprocessable_entity }
82-
end
83-
end
84-
end
85-
8636
private
8737

8838
def set_invoice
8939
@invoice = Invoice.includes(:invoice_items).find(params[:id])
9040
end
9141

92-
def update_params
93-
update_params = params.require(:invoice).permit(:notes)
94-
merge_updated_by(update_params)
95-
end
96-
97-
def update_predicate
98-
@invoice.assign_attributes(update_params)
99-
raise(Errors::InvoiceAlreadyPaid, @invoice.id) if @invoice.paid?
100-
101-
@invoice.mark_as_paid_at(Time.zone.now)
102-
end
103-
10442
def authorize_user
10543
authorize! :read, Invoice
10644
end
10745

108-
def authorize_for_update
109-
authorize! :update, @invoice
110-
end
111-
11246
def create_invoice_if_needed
11347
InvoiceCreationJob.perform_later if InvoiceCreationJob.needs_to_run?
11448
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
<%= turbo_frame_tag "modal" do %>
3+
4+
<div data-controller='modals--offer-modal' data-modals--offer-modal-open-class='is-open'>
5+
<div class="c-modal-overlay js-modal-po" data-modals--offer-modal-target='modal'>
6+
<div class="c-modal">
7+
<div class="c-modal__scroll">
8+
<div class="c-modal__scroll__content">
9+
<button class="c-modal__close-btn js-close-modal" data-action='modals--offer-modal#close'>
10+
<span class="o-close-icon"></span>
11+
</button>
12+
<div class="c-modal__header">
13+
<div class="c-modal__header__title">
14+
<span class="o-edit-icon--green"></span>
15+
<span><%= t('invoices.mark_as_paid') %></span>
16+
</div>
17+
<div class="c-modal__header__subject">
18+
<span class="c-modal__header__subject__title"><%= @invoice.number %></span>
19+
<span class="o-io-icon o-io-icon--bg-blue"></span>
20+
</div>
21+
</div>
22+
23+
<div>
24+
<%= form_with model: @invoice, url: admin_invoice_mark_as_paid_path(@invoice), method: :patch, data: { turbo: false } do |f| %>
25+
<div class="c-modal__grid u-flex-wrap u-mt-24 u-mt-48-l">
26+
<%= f.text_area :notes %>
27+
<%= f.date_field :paid_at %>
28+
<%= f.submit t('invoices.mark_as_paid') %>
29+
</div>
30+
<% end %>
31+
</div>
32+
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
</div>
38+
39+
40+
<% end %>

app/views/admin/invoices/show.html.erb

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,77 @@
11
<% content_for :title, t('.title', invoice_number: @invoice&.number) %>
22

33
<div class="o-container">
4-
<div class="c-table__filters" >
5-
<%= link_to t(:versions_name), admin_invoice_versions_path(@invoice), class: "ui button primary" %>
6-
<%= link_to t('invoices.download'), download_admin_invoice_path(@invoice),
7-
{ class: 'ui button secondary', download: true } %>
8-
<% unless @invoice.overdue? || @invoice.paid? %>
9-
<%= link_to t('invoices.mark_as_paid'), edit_admin_invoice_path(@invoice), class: "ui button secondary" %>
10-
<% action = @invoice.partial_payments? ? "disallow" : "allow" %>
11-
<%= button_to t("invoices.#{action}_partial_payments"), toggle_partial_payments_admin_invoice_path(@invoice), class: "ui button secondary", form: { data: { 'turbo-confirm': 'Are you sure?' } } %>
12-
<% end %>
4+
<div class="c-block">
5+
<div style="display: flex; gap: 1rem; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap;">
6+
<%= link_to t(:versions_name), admin_invoice_versions_path(@invoice), class: "c-btn c-btn--ghost c-acount__button c-acount__button--icon" %>
7+
<%= link_to t('invoices.download'), download_admin_invoice_path(@invoice), { class: 'c-btn c-btn--ghost c-acount__button c-acount__button--icon', download: true } %>
8+
9+
<% unless @invoice.overdue? || @invoice.paid? %>
10+
<%= link_to t('invoices.mark_as_paid'), edit_admin_invoice_mark_as_paid_path(@invoice), class: "c-btn c-btn--ghost c-acount__button c-acount__button--icon", data: { turbo_frame: 'modal' } %>
11+
12+
<% action = @invoice.partial_payments? ? "disallow" : "allow" %>
13+
<%= button_to t("invoices.#{action}_partial_payments"), admin_invoice_toggle_partial_payment_path(@invoice), class: "c-btn c-btn--ghost c-acount__button c-acount__button--icon", form: { data: { 'turbo-confirm': 'Are you sure?' } }, method: :patch %>
14+
<% end %>
15+
</div>
1316
</div>
17+
1418
<div class="c-block">
15-
<div class="o-grid o-grid--two-col">
16-
<div>
17-
<div class="header"><%= t('invoices.status') %></div>
18-
<%= I18n.t("activerecord.enums.invoice.statuses.#{@invoice.status}") %>
19-
<div class="header"><%= t('invoices.issued_for') %></div>
20-
<%= @invoice.recipient %><br>
21-
<%= @invoice.address %>
22-
<div class="header"><%= t(:updated_by) %></div>
23-
<%= @invoice.updated_by %>
19+
<h2 class="c-block__title u-h3"><%= t('invoices.info_block') %></h2>
20+
<table class="table--black" style="width:100%">
21+
<tbody>
22+
<tr>
23+
<td><%= t('invoices.status') %></td>
24+
<td><%= I18n.t("activerecord.enums.invoice.statuses.#{@invoice.status}") %></td>
25+
</tr>
26+
<tr>
27+
<td><%= t('invoices.issued_for') %></td>
28+
<td><%= @invoice.recipient %><br><%= @invoice.address %></td>
29+
</tr>
30+
<tr>
31+
<td><%= t(:updated_by) %></td>
32+
<td><%= @invoice.updated_by %></td>
33+
</tr>
2434
<% if @invoice.notes %>
25-
<div class="item">
26-
<div class="header"><%= t('invoices.notes') %></div>
27-
<%= @invoice.notes %>
28-
</div>
35+
<tr>
36+
<td><%= t('invoices.notes') %></td>
37+
<td><%= @invoice.notes %></td>
38+
</tr>
39+
<% end %>
40+
<% if @invoice.paid_at %>
41+
<tr>
42+
<td><%= t('invoices.paid_at') %></td>
43+
<td><%= @invoice.paid_at %></td>
44+
</tr>
2945
<% end %>
30-
</div>
31-
<div>
32-
<div class="header"><%= t('invoices.issuer') %></div>
33-
<%= Setting.find_by(code: 'invoice_issuer').retrieve %>
34-
<div class="header"><%= t('invoices.issue_date') %></div>
35-
<%= @invoice.issue_date %>
36-
<div class="header"><%= t('invoices.due_date') %></div>
37-
<%= @invoice.due_date %>
46+
<tr>
47+
<td><%= t('invoices.issuer') %></td>
48+
<td><%= Setting.find_by(code: 'invoice_issuer').retrieve %></td>
49+
</tr>
50+
<tr>
51+
<td><%= t('invoices.issue_date') %></td>
52+
<td><%= @invoice.issue_date %></td>
53+
</tr>
54+
<tr>
55+
<td><%= t('invoices.due_date') %></td>
56+
<td><%= @invoice.due_date %></td>
57+
</tr>
3858
<% if @invoice.paid_with_payment_order %>
39-
<div class="item">
40-
<div class="header"><%= t('invoices.paid_through') %></div>
41-
<%= @invoice.paid_with_payment_order&.channel %>
42-
</div>
59+
<tr>
60+
<td><%= t('invoices.paid_through') %></td>
61+
<td><%= @invoice.paid_with_payment_order&.channel %></td>
62+
</tr>
4363
<% end %>
44-
</div>
45-
</div>
64+
</tbody>
65+
</table>
4666
</div>
47-
<div class="c-table c-table--sortable">
48-
<h2><%= t('invoices.items') %></h2>
67+
68+
<div class="c-block">
69+
<h2 class="c-block__title u-h3"><%= t('invoices.items') %></h2>
4970
<% header_collection = [{column: nil, caption: '#', options: {}},
5071
{ column: nil, caption: t('invoices.item'), options: { class: "" } },
5172
{ column: nil, caption: '', options: { class: "" } },
5273
{ column: nil, caption: t('invoices.price'), options: { class: "" } },] %>
53-
<%= component 'common/table', header_collection:, options: { class: 'js-table-dt dataTable no-footer' } do %>
74+
<%= component 'common/table', header_collection:, options: { class: 'table' } do %>
5475
<%= tag.tbody class: 'contents' do %>
5576
<% @invoice.items.each_with_index do |item, index| %>
5677
<tr>
@@ -75,7 +96,7 @@
7596
<tr>
7697
<th scope="col"></th>
7798
<th scope="col"><%= t('invoices.total') %></th>
78-
<td></td>
99+
<th></th>
79100
<th scope="col"><%= t('offers.price_in_currency', price: @invoice.total + (@invoice.enable_deposit? ? @invoice.deposit : 0.0)) %></th>
80101
</tr>
81102
<% if @invoice.paid? || @invoice.partial_payments? %>
@@ -108,13 +129,16 @@
108129
<% end %>
109130
</tfoot>
110131
<% end %>
111-
<h2><%= t('payment_orders.title') %></h2>
132+
</div>
133+
134+
<div class="c-block">
135+
<h2 class="c-block__title u-h3"><%= t('payment_orders.title') %></h2>
112136
<% header_collection = [{column: nil, caption: '#', options: {}},
113137
{ column: nil, caption: t('payment_orders.channel'), options: { class: "" } },
114138
{ column: nil, caption: t('payment_orders.status'), options: { class: "" } },
115139
{ column: nil, caption: t('payment_orders.initiated'), options: { class: "" } },
116140
{ column: nil, caption: t('payment_orders.response'), options: { class: "" } },] %>
117-
<%= component 'common/table', header_collection:, options: { class: 'js-table-dt dataTable no-footer' } do %>
141+
<%= component 'common/table', header_collection:, options: { class: 'table--black' } do %>
118142
<%= tag.tbody class: 'contents' do %>
119143
<% @payment_orders.each do |payment_order| %>
120144
<tr>

config/routes.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,13 @@
5555
resources :statistics, only: :index
5656
resources :billing_profiles, only: %i[index show], concerns: %i[auditable]
5757
resources :invoices, except: %i[new create destroy], concerns: %i[auditable] do
58+
scope module: :invoices do
59+
resource :mark_as_paid, only: %i[edit update]
60+
resource :toggle_partial_payment, only: %i[update]
61+
end
62+
5863
member do
5964
get 'download'
60-
post 'toggle_partial_payments'
6165
end
6266
end
6367
resources :jobs, only: %i[index create]

0 commit comments

Comments
 (0)