Railsのプルダウンの選択肢の動的制御

Railsアプリケーションで、あるフォームの選択肢を他のフォームの選択結果に基づいて動的に表示・非表示する方法について説明します。この記事では、予約システムにおいて、スタッフが提供可能なサービスに応じて利用可能な時間帯の選択肢をプルダウンで表示する実装を例にとります。

## ユースケース

予約システムにおいて、顧客がスタッフとサービスを選択する際、指定されたサービスがそのスタッフによって提供されていない場合、利用可能な時間帯を選択できないようにします。これにより、顧客が予約不可能な組み合わせを選択することを防ぎます。

## データモデル

この機能を実現するためには、スタッフ、サービス、そしてそれらの関連を管理するためのテーブル構造が必要です。以下に例を示します：

```ruby
create_table "available_services", force: :cascade do |t|
  t.bigint "staff_id", null: false
  t.bigint "service_id", null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["service_id"], name: "index_available_services_on_service_id"
  t.index ["staff_id"], name: "index_available_services_on_staff_id"
end

create_table "staffs", force: :cascade do |t|
  # スタッフに関するカラム（省略）
end

create_table "services", force: :cascade do |t|
  # サービスに関するカラム（省略）
end
```

`available_services` テーブルは、スタッフとサービスの多対多の関連を実現する中間テーブルとして機能します。

## モデルの関連設定

このテーブル構造に基づいて、モデル間の関連を設定します：

```ruby
class Staff < ApplicationRecord
  has_many :available_services
  has_many :services, through: :available_services
end

class Service < ApplicationRecord
  has_many :available_services
  has_many :staffs, through: :available_services
end

class AvailableService < ApplicationRecord
  belongs_to :staff
  belongs_to :service
end
```

この設定により、あるスタッフが提供可能なサービスのリストを取得することができるようになります。

## ビューの実装

ユーザーがスタッフを選択し、選択に基づいて利用可能なサービスを動的に表示するフォームを準備します。ここでは、`form_with` ヘルパーを使用して、予約のためのフォームを作成します。

```erb
<h1><%= @reservation.customer.name %>様の予約</h1>
<h2>予約内容</h2>
希望開始日時: <div id= reservation-date><%= @reservation.date.strftime('%Y-%m-%d') %> <%= @reservation.time_slot %>
<br>

<%= form_with url: time_tables_path, method: :post, local: true, data: { controller: "staff-selector" } do |form| %>
  <% @reservation_services.each_with_index do |reservation_service, index| %>
    <!-- サービス名の表示 -->
    <%= reservation_service.service.name %>
    <!-- サービスの所要時間の表示 -->
    <%= format_duration(reservation_service.service.duration) %>
    <!-- スタッフ選択のためのプルダウン -->
    <%= form.label :staff_id, 'スタッフ選択' %>
    <%= form.collection_select :staff_id, @working_staffs, :id, :name, { include_blank: true }, { id: "staff_select_#{index}", data: { staff_selector_target: "staff", action: "change->staff-selector#updateSlots", slots_target_id: "time_slots_select_#{index}" } } %>
    <!-- 利用可能な時間帯選択のためのプルダウン -->
    <%= form.label :available_slots, '利用可能な時間帯' %>
        <!-- 条件分岐でサービスが利用可能かどうかをチェック -->
    <% if @working_staffs.any? { |staff| staff.services.include?(reservation_service.service) } %>
      <%= form.select :time_slot, [], { include_blank: true }, { id: "time_slots_select_#{index}", data: { staff_selector_target: "slots" } } %>
    <% else %>
      なし
    <% end %>
    <br>
  <% end %>
  <%= form.submit "予定を確認" %>
<% end %>

<script type="text/javascript">
  // ERBを使用してRailsのインスタンス変数をJavaScript変数にセット
  var reservationDate = "<%= @reservation.date.strftime('%Y-%m-%d') %>";
</script>
```

このフォームでは、各予約サービスごとにスタッフの選択プルダウンと利用可能な時間帯のプルダウンが設けられています。スタッフの選択によって時間帯のプルダウンの内容が動的に変更されるように、Stimulusのコントローラとアクションが設定されています。

## コントローラのアクション実装

選択されたスタッフに応じて利用可能な時間帯をフェッチするためのアクションを`SchedulesController`に実装します。

```ruby
class SchedulesController < ApplicationController
  def available_slots
    date = params[:date].present? ? Date.parse(params[:date]) : Date.today
    staff_id = params[:staff_id]
    staff = Staff.find(params[:staff_id]) if staff_id
    staff_schedule = Schedule.find_by(date: date, staff_id: staff_id)
    @available_times = calculate_available_times(staff_schedule) if staff_schedule
    render json: @available_times
  end

  private

  def calculate_available_times(staff, date)
    # 利用可能な時間帯を計算するロジック（実装詳細は省略）
  end
end
```

これにより、@working_staffs の中で、現在の予約サービスを提供可能なスタッフがいる場合のみ、利用可能な時間帯のプルダウンが表示されます。そうでない場合は、「なし」と表示されます。

## 注意点
1. `@working_staffs`には、`Service`モデルのインスタンスを含む、各スタッフが提供可能なサービスの情報が必要です。これを満たすために、スタッフをロードする際には`includes(:services)`を用いるなどして、関連するサービス情報も一緒に取得しておく必要があります。
2. 以下のロジックは、指定されたサービスがスタッフによって提供されているかどうかを確認しています。この条件を満たすスタッフが一人でもいる場合、利用可能な時間帯のプルダウンが表示されます。

    ```
    <% if @working_staffs.any? { |staff| staff.services.include?(reservation_service.service) } %>
    ```
    
    このアクションでは、まず指定されたスタッフが選択されたサービスを提供しているかを確認します。もし提供している場合は、その日の利用可能な時間帯を計算して返し、提供していない場合は空のリストを返します。

    これにより、フロントエンドのJavaScript（Stimulusコントローラ）がこのアクションに非同期リクエストを送り、レスポンスに基づいて利用可能な時間帯の選択肢を更新できるようになります。

## 補足: プルダウンに、サービス提供可能なスタッフしか表示しない場合
以下のように記述すれば実装可能です。
ただし、これは処理が複雑になるため、可能な限り避けるべきです。

```erb
<%= form.label :staff_id, 'スタッフ選択' %>
<%= form.collection_select :staff_id, @working_staffs.select { |staff| staff.services.exists?(id: reservation_service.service_id) }, :id, :name, { include_blank: true }, { id: "staff_select_#{index}", data: { staff_selector_target: "staff", action: "change->staff-selector#updateSlots", slots_target_id: "time_slots_select_#{index}" } } %>
```

### 変更前（サービス提供不可なBも表示されている）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116202/37d6540a-1a44-3237-6050-221afa85d9c5.png)

### 変更後（サービス提供不可なBは表示されない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3116202/20411951-9032-bd75-48a1-a85a4508e8af.png)


この例では、`@working_staffs`から、`reservation_service.service_id`で指定されたサービスを提供できるスタッフのみを選択しています。`select`メソッドは配列に対して使うため、`@working_staffs`がActiveRecordの関連オブジェクトである場合は、`to_a`メソッドを使って配列に変換するか、`where`クエリを使ってフィルタリングする必要があります。