ActiveAdmin 力技集

# はじめに
皆さんは、ActiveAdmin使ってますか？

ActiveAdmin、サッと管理画面作るには素晴らしい反面、
ちょっとでも基本的な実装から外れると、途端につまる、、
そんな経験をされた方も多いかと思います。

そして往々として、業務では基本的な実装から外れるものですよね。

そんな格闘の中で得た、「ActiveAdminでちょっと凝ったことをするのに使える(かもしれない)方法」を、
**「ActiveAdmin 力技集」** としてシェアしようと思います。

なお、この方法が正しいというわけではなく、こう乗り切ったよ、くらいの感覚で見ていただけるとありがたいです。
もっといい方法があれば、ぜひコメントをお願いします！

# 環境
Ruby 2.5.5
Rails 5.2.2.1
activeadmin 1.3.1

# ActiveAdmin 力技集
### Model
顧客がいて、接点があって、請求と明細がある

```ruby:app/models/customer.rb
class Customer < ApplicationRecord
  has_many :contacts
  has_many :invoices
end
```
```ruby:app/models/contact.rb
class Contact < ApplicationRecord
  belongs_to :customer
end
```
```ruby:app/models/invoice.rb
class Invoice < ApplicationRecord
  belongs_to :customer
  has_many :items, class_name: 'InvoiceItem'
end
```
```ruby:app/models/invoice_item.rb
class InvoiceItem < ApplicationRecord
  belongs_to :invoice
end
```

### 絞り込まれた一覧へのリンク
例えば顧客の詳細画面に、その顧客との接点一覧へのボタンを置きたい時。

```ruby:app/admin/customers.rb
button_to '接点履歴一覧へ', admin_customer_contacts_path, method: :get, params: { q: { customer_id_equals: customer.id } }
```
検索条件が`params[:q]`に入るのがキモ。
`filter`で検索を実装して実際に検索してみて、そのパラメータを再現すると手っ取り早い。

なお、このパラメータの使用に際して、`filter`で実装されている必要はない。

### 別のカラムでソート
```ruby:app/admin/customers.rb
ActiveAdmin.register Customer do
  index do
    column '顧客ID', :id
    # sortable: にソートしたいカラム名を記述
    column '顧客名', :name, sortable: :kana
  end
end
```
このように漢字を表示しつつ読み仮名でのソートができる。

### formでjsを使う
例えばformで顧客を選択したいがselectだと厳しいので、オートコンプリートを実装したい時などに。

```ruby:app/admin/contacts.rb
ActiveAdmin.register Contact do
  # ...
  form partial: 'form'
end
```

```erb:app/views/admin/contacts/_form.html.erb
<script language="JavaScript">
  // @contactが暗黙的に渡ってくる。
  var contact = <%= raw @contact.to_json %>
  // ...
</script>
```
当然、この例では`@contact`の中身がブラウザで丸見えになるので、セキュリティ等問題がないユースケースか、検証が必要。

### 関連先によるソート・絞り込み
例えば顧客との接点の一覧画面で、顧客によってソート・絞り込みをしたい時。

```ruby:app/admin/contacts.rb
ActiveAdmin.register Contact do
  filter :date, label: '年月日'
  # 関連先の型までは見てくれないようなので、as: :string などの指定が必要
  filter :customer_name, as: :string, label: '顧客名'

  index do
    column '年月日', :date
    column '顧客ID', :customer_id
    # sortableには発行されるSQLで使えるカラム名を渡す。今回はcustomersテーブルをjoinしているので、customers.kana
    # なお、関連名を渡すといい感じにリンク化してくれる
    column '顧客名', :customer, sortable: :'customers.kana'
    column '内容', :memo
  end

  controller do
    def scoped_collection
      # 関連先でソートを行う場合はjoinが必要
      end_of_association_chain.joins(:customer)
    end
  end
end
```
`index`の表は`scoped_collection`で発行されたSQLの結果を使っている、と考えるとイメージしやすい。

### デフォルト検索条件
必ず「ある年月の一覧」を表示したい場合はこのように。

```ruby:app/admin/invoices.rb
ActiveAdmin.register Invoice do
  # ...
  controller do
    # 必ず年月による絞り込みがかかるよう、indexページ表示前にparamsの不足を補う
    before_action only: :index do
      # 最初に表示されるのは先月分。
      prev_month = Time.current.prev_month
      if params[:q].present?
        params[:q].merge!(year_eq: prev_month.year, month_eq: prev_month.month) if params[:q][:year_eq].blank? || params[:q][:month_eq].blank?
      else
        params.merge!(q: { year_eq: prev_month.year, month_eq: prev_month.month })
      end
    end
  end
end
```
他の検索条件を邪魔しないように注意が必要。

### GROUP BYした結果の一覧を作る
例えば版管理を行う請求書の一覧などで、版違いは同じ行でまとめたい時。

```ruby:app/admin/invoices.rb
ActiveAdmin.register Invoice do
  index do
    # ...
    column 'バージョン', class: 'minWidth100' do |latest_invoice|
      Invoice
        .where(customer_id: latest_invoice.customer)
        .where(year: latest_invoice.year)
        .where(month: latest_invoice.month)
        .each do |invoice|
        div { span link_to "第#{invoice.version}版", admin_invoice_path(invoice) }
      end
      nil # nilを返さないと、上の行の結果が出力されてしまう
    end
  end

  controller do
    # ...
    def scoped_collection
      # 一覧には顧客ごとの請求書を出したいのだが、返るレコード数分、表の行が作られてしまう。
      # そのため、orderした上でgroup、顧客ごとに最新の請求書だけを取得
      # 参考: https://qiita.com/ryo-ishii/items/36b878cf2d0bd8ef7e07
      Invoice
        .group(:customer_id, :year, :month)
        .from(
          end_of_association_chain
            .select('invoices.*, customers.name, customers.kana, CONCAT(year, LPAD(month, 2, 0)) AS ym')
            .joins(:customer)
            .order(version: :desc),
          :invoices
        )
    end
  end
end
```

# まとめ
ActiveAdminの良いところでもあり辛いところは、暗黙的に定義されている変数が多いところだと思います。
それらをいかに把握して差し込んでいくかが、ActiveAdminで力技をする際に重要になります。

それ以前に、力技を使わなくても済むような要件、データモデルの定義を心がけていきたいですね。