diff --git a/app/controllers/admin/legal_entities_controller.rb b/app/controllers/admin/legal_entities_controller.rb
new file mode 100644
index 0000000000..85f0596262
--- /dev/null
+++ b/app/controllers/admin/legal_entities_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Admin
+ class LegalEntitiesController < Admin::BaseController
+ def index
+ @page = params[:page] || 1
+ @per = params[:per] || 20
+
+ relation = LegalEntity.includes(:users, :managing_event, :latest_tax_form)
+
+ @q = params[:q].presence
+ if @q
+ relation = relation.left_joins(:users).where("legal_entities.name ILIKE :q OR users.full_name ILIKE :q OR users.email ILIKE :q", q: "%#{LegalEntity.sanitize_sql_like(@q)}%").distinct
+ end
+
+ @entity_type = params[:entity_type].presence
+ relation = relation.where(entity_type: @entity_type) if @entity_type
+
+ @managed = params[:managed] == "1"
+ relation = relation.managed if @managed
+
+ @archived = params[:archived] == "1"
+ relation = relation.where.not(archived_at: nil) if @archived
+
+ @legal_entities = relation.order(created_at: :desc).page(@page).per(@per)
+ end
+
+ end
+end
diff --git a/app/controllers/admin/payments_controller.rb b/app/controllers/admin/payments_controller.rb
new file mode 100644
index 0000000000..3b88da7a86
--- /dev/null
+++ b/app/controllers/admin/payments_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Admin
+ class PaymentsController < Admin::BaseController
+ def index
+ @page = params[:page] || 1
+ @per = params[:per] || 20
+
+ relation = Payment.includes(:payee, :event)
+
+ @q = params[:q].presence
+ relation = relation.search_recipient(@q) if @q
+
+ @state = params[:state].presence
+ relation = relation.where(aasm_state: @state) if @state
+
+ @payments = relation.order(created_at: :desc).page(@page).per(@per)
+ end
+
+ end
+end
diff --git a/app/controllers/admin/payroll_positions_controller.rb b/app/controllers/admin/payroll_positions_controller.rb
new file mode 100644
index 0000000000..c31a94017d
--- /dev/null
+++ b/app/controllers/admin/payroll_positions_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Admin
+ class PayrollPositionsController < Admin::BaseController
+ def index
+ @page = params[:page] || 1
+ @per = params[:per] || 20
+
+ relation = Payroll::Position.includes(:payee, :event)
+
+ @q = params[:q].presence
+ relation = relation.search_recipient(@q) if @q
+
+ @state = params[:state].presence
+ relation = relation.where(aasm_state: @state) if @state
+
+ @positions = relation.order(Arel.sql("CASE WHEN aasm_state = 'under_review' THEN 0 ELSE 1 END, created_at DESC")).page(@page).per(@per)
+ end
+
+ def reject
+ position = Payroll::Position.find(params[:id])
+ position.mark_rejected!
+ redirect_back fallback_location: admin_payroll_positions_path, flash: { success: "Contractor rejected." }
+ rescue AASM::InvalidTransition
+ redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor can no longer be rejected." }
+ end
+
+ end
+end
diff --git a/app/controllers/admin/tax_forms_controller.rb b/app/controllers/admin/tax_forms_controller.rb
new file mode 100644
index 0000000000..2b210d2a15
--- /dev/null
+++ b/app/controllers/admin/tax_forms_controller.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Admin
+ class TaxFormsController < Admin::BaseController
+ def index
+ @page = params[:page] || 1
+ @per = params[:per] || 20
+
+ relation = Tax::Form.includes(:legal_entity)
+
+ @q = params[:q].presence
+ if @q
+ relation = relation.left_joins(legal_entity: :users).where("legal_entities.name ILIKE :q OR users.full_name ILIKE :q OR users.email ILIKE :q", q: "%#{Tax::Form.sanitize_sql_like(@q)}%").distinct
+ end
+
+ @state = params[:state].presence
+ relation = relation.where(aasm_state: @state) if @state
+
+ @form_type = params[:form_type].presence
+ relation = relation.where(form_type: @form_type) if @form_type
+
+ @tax_forms = relation.order(created_at: :desc).page(@page).per(@per)
+ end
+
+ end
+end
diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb
index 4889cc8b45..e8a6e6f46e 100644
--- a/app/models/admin/nav.rb
+++ b/app/models/admin/nav.rb
@@ -160,6 +160,12 @@ def spending
path: reimbursements_admin_index_path,
count: ->{ Reimbursement::Report.reimbursement_requested.count },
count_type: :tasks
+ ),
+ make_item(
+ name: "Payments",
+ path: admin_payments_path,
+ count: ->{ Payment.under_review.count },
+ count_type: :tasks
)
]
)
@@ -302,21 +308,21 @@ def payroll
name: "Payroll",
items: [
make_item(
- name: "Employees",
- path: employees_admin_index_path,
- count: ->{ Employee.onboarding.count },
+ name: "Contractors",
+ path: admin_payroll_positions_path,
+ count: ->{ Payroll::Position.under_review.count },
count_type: :tasks
),
make_item(
- name: "Payments",
- path: employee_payments_admin_index_path,
- count: ->{ Employee::Payment.paid.count },
+ name: "Legal Entities",
+ path: admin_legal_entities_path,
+ count: ->{ LegalEntity.count },
count_type: :records
),
make_item(
- name: "W9s",
- path: admin_w9s_path,
- count: ->{ W9.count },
+ name: "Tax Forms",
+ path: admin_tax_forms_path,
+ count: ->{ Tax::Form.count },
count_type: :records
)
]
diff --git a/app/views/admin/legal_entities/index.html.erb b/app/views/admin/legal_entities/index.html.erb
new file mode 100644
index 0000000000..74a4c44ff1
--- /dev/null
+++ b/app/views/admin/legal_entities/index.html.erb
@@ -0,0 +1,69 @@
+<% title "Legal Entities" %>
+
+<%= form_with local: true, class: "card", url: admin_legal_entities_path, method: :get do |form| %>
+
+ <%= render "events/filters/search", form: %>
+ <%= form.select :entity_type, [%w[Person person], %w[Business business]], { include_blank: "All types", selected: @entity_type } %>
+
+ <%= form.check_box :managed, checked: @managed %> Managed
+
+
+ <%= form.check_box :archived, checked: @archived %> Archived
+
+
+
+ <%= form.submit "Search", class: "ml-auto" %>
+
+<% end %>
+
+
+
+ <%= page_entries_info @legal_entities, entry_name: "legal entities" %>
+
+ <%= paginate @legal_entities %>
+
+
+
+
+
+ Date
+ Name
+ Type
+ Users / Managing org
+ TIN
+ Payable?
+ Actions
+
+
+
+ <% @legal_entities.each do |legal_entity| %>
+
+ <%= legal_entity.created_at.strftime("%Y-%m-%d") %>
+
+ <%= legal_entity.name || legal_entity.display_name %>
+ <% if legal_entity.archived? %>Archived <% end %>
+ <% if legal_entity.tin_banned? %>Banned <% end %>
+
+ <%= legal_entity.entity_type&.humanize %>
+
+ <% if legal_entity.managed? %>
+ Managed by <%= link_to legal_entity.managing_event.name, legal_entity.managing_event %>
+ <% else %>
+ <%= legal_entity.users.map(&:email).to_sentence.presence || "—" %>
+ <% end %>
+
+ <%= legal_entity.tin_hash.present? ? "On file" : "—" %>
+
+ <% if legal_entity.payable? %>
+ Yes
+ <% else %>
+ No
+ <% end %>
+
+ <%= link_to "View", legal_entity_path(legal_entity) %>
+
+ <% end %>
+
+
+
+<%= paginate @legal_entities %>
diff --git a/app/views/admin/payments/index.html.erb b/app/views/admin/payments/index.html.erb
new file mode 100644
index 0000000000..dc448dafcf
--- /dev/null
+++ b/app/views/admin/payments/index.html.erb
@@ -0,0 +1,50 @@
+<% title "Payments" %>
+
+<%= form_with local: true, class: "card", url: admin_payments_path, method: :get do |form| %>
+
+ <%= render "events/filters/search", form: %>
+ <%= form.select :state, Payment.aasm.states_for_select, { include_blank: "All states", selected: @state } %>
+
+
+ <%= form.submit "Search", class: "ml-auto" %>
+
+<% end %>
+
+
+
+ <%= page_entries_info @payments, entry_name: "payments" %>
+
+ <%= paginate @payments %>
+
+
+
+
+
+ Date
+ Organization
+ Recipient
+ Purpose
+ Amount
+ Status
+ Actions
+
+
+
+ <% @payments.each do |payment| %>
+ ">
+ <%= payment.created_at.strftime("%Y-%m-%d") %>
+ <%= link_to payment.event.name, payment.event %>
+
+ <%= payment.payee.display_name %>
+ <%= payment.payee.email %>
+
+ <%= payment.purpose %>
+ <%= payment.amount.format %>
+ <%= payment.state_text %>
+ <%= link_to "View", payment_path(payment) %>
+
+ <% end %>
+
+
+
+<%= paginate @payments %>
diff --git a/app/views/admin/payroll_positions/index.html.erb b/app/views/admin/payroll_positions/index.html.erb
new file mode 100644
index 0000000000..4a08af9c57
--- /dev/null
+++ b/app/views/admin/payroll_positions/index.html.erb
@@ -0,0 +1,54 @@
+<% title "Contractors" %>
+
+<%= form_with local: true, class: "card", url: admin_payroll_positions_path, method: :get do |form| %>
+
+ <%= render "events/filters/search", form: %>
+ <%= form.select :state, Payroll::Position.aasm.states_for_select, { include_blank: "All states", selected: @state } %>
+
+
+ <%= form.submit "Search", class: "ml-auto" %>
+
+<% end %>
+
+
+
+ <%= page_entries_info @positions, entry_name: "contractors" %>
+
+ <%= paginate @positions %>
+
+
+
+
+
+ Date
+ Organization
+ Contractor
+ Title
+ Rate
+ Period
+ Status
+ Actions
+
+
+
+ <% @positions.each do |position| %>
+ ">
+ <%= position.created_at.strftime("%Y-%m-%d") %>
+ <%= link_to position.event.name, position.event %>
+
+ <%= position.payee.display_name %>
+ <%= position.payee.email %>
+
+ <%= position.title %>
+ <%= position.rate.format %>
+ <%= position.period_label %>
+ "><%= position.under_review? ? "Under review" : position.status_text %>
+
+ <%= link_to position.under_review? ? "Review" : "View", event_payroll_position_path(event_id: position.event.slug, id: position), data: { turbo: false } %>
+
+
+ <% end %>
+
+
+
+<%= paginate @positions %>
diff --git a/app/views/admin/tax_forms/index.html.erb b/app/views/admin/tax_forms/index.html.erb
new file mode 100644
index 0000000000..c36600914b
--- /dev/null
+++ b/app/views/admin/tax_forms/index.html.erb
@@ -0,0 +1,52 @@
+<% title "Tax Forms" %>
+
+<%= form_with local: true, class: "card", url: admin_tax_forms_path, method: :get do |form| %>
+
+ <%= render "events/filters/search", form: %>
+ <%= form.select :state, Tax::Form.aasm.states_for_select, { include_blank: "All states", selected: @state } %>
+ <%= form.select :form_type, Tax::Form.form_types.keys, { include_blank: "All form types", selected: @form_type } %>
+
+
+ <%= form.submit "Search", class: "ml-auto" %>
+
+<% end %>
+
+
+
+ <%= page_entries_info @tax_forms, entry_name: "tax forms" %>
+
+ <%= paginate @tax_forms %>
+
+
+
+
+
+ Date
+ ID
+ Legal entity
+ Form
+ Service
+ Status
+ TaxBandits status
+ TIN match
+ Completed
+
+
+
+ <% @tax_forms.each do |form| %>
+
+ <%= form.created_at.strftime("%Y-%m-%d") %>
+ <%= form.hashid %>
+ <%= link_to form.legal_entity.name || form.legal_entity.display_name, legal_entity_path(form.legal_entity) %>
+ <%= form.form_type || "—" %>
+ <%= form.external_service.humanize %>
+ <%= form.aasm_state.humanize %>
+ <%= form.taxbandits_status&.humanize || "—" %>
+ <%= form.taxbandits_tin_matching_status&.humanize || "—" %>
+ <%= form.completed_at&.strftime("%Y-%m-%d") || "—" %>
+
+ <% end %>
+
+
+
+<%= paginate @tax_forms %>
diff --git a/app/views/payroll/positions/show.html.erb b/app/views/payroll/positions/show.html.erb
index bfe01275f5..cadfb1b965 100644
--- a/app/views/payroll/positions/show.html.erb
+++ b/app/views/payroll/positions/show.html.erb
@@ -50,6 +50,21 @@
+ <% if @position.under_review? %>
+ <% admin_tool("mt-4 flex items-center justify-between") do %>
+ <% if @position.contract.present? %>
+ This contract is awaiting HCB review. Reviewing it means signing it as HCB.
+
+ <%= link_to "Review", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success nowrap", data: { turbo_frame: "_top" } %>
+ <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %>
+
+ <% else %>
+ No contract has been sent for this contractor yet — the organizer needs to retry sending it from the edit page before HCB can review it.
+ <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %>
+ <% end %>
+ <% end %>
+ <% end %>
+
<% if @position.status == :onboarding %>
Onboarding checklist
diff --git a/config/routes.rb b/config/routes.rb
index 8f0d3a13c7..8e4f1a9ad9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -357,6 +357,12 @@
post "submit", on: :member
post "reject", on: :member
end
+ resources :payments, only: [:index]
+ resources :payroll_positions, only: [:index] do
+ post "reject", on: :member
+ end
+ resources :legal_entities, only: [:index]
+ resources :tax_forms, only: [:index]
resources :column_statements, only: :index do
get "bank_account_summary_report"
end