Ruby on Railsにおけるサービス提供可否判定ロジック解説

この記事では、Ruby on Railsを使用したアプリケーション開発において、特定の日に特定のサービスが提供可能かどうかを判断し、その結果をユーザーに視覚的に表示する方法について解説します。具体的なシナリオとして、ある会社が複数のサービスを提供しており、それぞれのサービスには複数のスタッフが割り当てられています。スタッフは日ごとに異なるスケジュールで働き、サービス提供の可否はスタッフのスケジュールと提供可能なサービスに基づいています。

## 基本概念

- **サービス(Service)**: 会社が提供するサービス。例えば、ヘアカット、マッサージなど。
- **スタッフ(Staff)**: サービスを提供する従業員。各スタッフは特定のサービスを提供する能力があり、日によって異なる時間帯に働きます。
- **スケジュール(Schedule)**: スタッフが特定の日に働く時間帯。`start_time`と`end_time`で定義されます。

## 目標

ある日における各サービスの提供可能性を時間帯ごとに判定し、それを以下の3つの状態で表示する。

- `○`：サービスを提供できるスタッフが2人以上いる時間帯。
- `△`：サービスを提供できるスタッフが1人のみの時間帯。
- `×`：サービスを提供できるスタッフがいない時間帯。

## 実装手順

### 1. コントローラーの設定

```ruby
  def for_customer_index
    @date = params[:date].present? ? Date.parse(params[:date]) : Date.today
    @company = Company.find(params[:company_id] || viewing_company.id)
    @working_staffs_on_date = @company.staffs.joins(:schedules).where(schedules: {at_work: true, date: @date}).includes(:services)
    @time_slots = generate_time_slots(@company)

    @service_availability = @company.services.each_with_object({}) do |service, hash|
      hash[service.id] = @time_slots.each_with_object({}) do |slot, inner_hash|
        available_staff_count = @working_staffs_on_date.select do |staff|
          staff.services.include?(service) && 
          staff.schedules.any? { |s| s.date == @date && s.start_time.strftime("%H:%M") <= slot && s.end_time.strftime("%H:%M") > slot }
        end.count
        inner_hash[slot] = case available_staff_count
                          when 0 then '×'
                          when 1 then '△'
                          else '○'
                          end
      end
    end
  end

  private

  def generate_time_slots(company)
    selected_date = params[:date].present? ? Date.parse(params[:date]) : Date.today
    day_of_week = selected_date.wday # 曜日を取得
    company_business_days = company.company_business_days.joins(:business_day).where(business_days: { day_of_week: day_of_week })
    company_business_hours = company_business_days.flat_map(&:company_business_hours)
  
    # 最も早い開始時間と最も遅い終了時間を見つけるか、デフォルト値を使用する
    start_time = company_business_hours.min_by(&:start_time)&.start_time || Time.zone.parse("09:00")
    end_time = company_business_hours.max_by(&:end_time)&.end_time || Time.zone.parse("19:00")
  
    time_slots = []
    current_time = start_time
    while current_time < end_time do
      time_slots << current_time.strftime("%H:%M")
      current_time += 15.minutes
    end
    time_slots
  end
```

### 2. ビューの設定

ビューファイル（`app/views/schedules/for_customer_index.html.erb`）には、各サービスと時間帯ごとの提供可能性を表示するテーブルを設定します。

```erb
<table style="border-collapse: collapse;">
  <thead>
    <tr>
      <th rowspan="2" style="width: 75px; border: 1px solid black;">時間</th>
      <th colspan="<%= @company.services.count %>" style="border: 1px solid black;"><%= @company.name %></th>
      <th rowspan="2"

 style="width: 75px; border: 1px solid black;">時間</th>
    </tr>
    <tr>
      <% @company.services.each do |service| %>
        <th style="width: 100px; border: 1px solid black;"><%= service.name %></th>
      <% end %>
    </tr>
  </thead>
  <tbody>
    <% @time_slots.each do |slot| %>
      <tr>
        <td style="border: 1px solid black;"><%= slot %></td>
        <% @company.services.each do |service| %>
          <td style="border: 1px solid black; text-align: center;">
            <%= @service_availability[service.id][slot] %>
          </td>
        <% end %>
        <td style="border: 1px solid black;"><%= slot %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

このビューでは、各時間帯におけるサービスの提供可能性（`○`、`△`、`×`）を表形式で表示します。サービスごと、時間帯ごとに提供可能性の状態が分かるようになっています。

## （補足）判定ロジックの詳細解説

このセクションでは、サービス提供可能性の判定に関連するコードの各部分について、深く掘り下げて説明します。

### 外側の`each_with_object`：`|service, hash|`
この構文は、`@company.services`の各サービスに対してループを行います。`each_with_object({})`は、空のハッシュ`{}`を初期値として、ループの各イテレーションで更新していきます。ここでの`service`は現在のサービスオブジェクトを指し、`hash`はサービスIDごとにそのサービスの時間帯ごとの空き状況（`○×△`）を格納するためのハッシュです。

### 内側の`each_with_object`：`|slot, inner_hash|`
この構文は、`@time_slots`の各時間帯に対してループを行います。`inner_hash`は、特定のサービス`service`（外側の`each_with_object`で定義）に対する各時間帯`slot`の空き状況を格納するハッシュです（※総結果は`hash[service.id]`に格納されます）。このハッシュは、各時間帯におけるサービス提供可能性（`○×△`）をキーとして持ちます。

### `select`メソッド
`select`メソッドは、条件に一致する要素をすべて選択するために使用されます。このコンテキストでは、`@working_staffs_on_date`から特定のサービスを提供でき、かつ特定の時間帯に勤務しているスタッフを選択するために使用されます。

### `include?(service)`の意味
`staff.services.include?(service)`は、特定のスタッフが引数で与えられた`service`を提供する能力があるかどうかを確認します。`include?`メソッドは、指定された要素が配列に含まれているかどうかを真偽値で返します。

### `staff.schedules.any?`の意味
`staff.schedules.any? {...}`は、少なくとも1つのスケジュールが指定された条件を満たしているかどうかを確認します。この場合、条件は特定の日`@date`における特定の時間帯`slot`に勤務しているかどうかです。

### `{ |s| s.date == @date && s.start_time.strftime("%H:%M") <= slot && s.end_time.strftime("%H:%M") > slot }`の`s`の意味
ここでの`s`は、スタッフのスケジュールオブジェクトを表し、特定の日における勤務時間を判定する条件を含みます。

### `end.count`の意味
`end.count`は、`select`メソッドによって選択された要素（この場合はスタッフ）の数をカウントします。これにより、特定の時間帯にサービスを提供できるスタッフの数が決定されます。

### `@working_staffs_on_date`の意味
この変数は、特定の日`@date`に勤務しているスタッフのリストを保持します。`joins(:schedules).where(schedules: {at_work: true, date: @date})`によって、その日に`at_work`が`true`であるスケジュールを持つスタッフが選択されます。

### `@time_slots = generate_time_slots(@company)`の意味
このコードは、会社の営業時間に基づいて、その日のサービス提供可能な時間帯（15分間隔のスロット）を生成します。

### 処理の実行順序
1. 会社が提供する全サービスに対してループ（外側の`each_with_object`）。
2. 各サービスに対して、一日の時間帯ごとにループ（内側の`each_with_object`）。
3. 指定されたサービスを提供でき、特定の時間帯に勤務しているスタッフの選択（`select`）とカウント（`count`）。
4. 各時間帯ごとにサービス提供可能性（`○×△`）の決定。

### `each`と`each_with_object`の違い
`@company.services.each_with_object({}) do |service, hash|`と`@company.services.each do |service|`は似ていますが、`each_with_object`では追加のオブジェクト（この場合はハッシュ）がループの各イテレーションで更新され、最終的にこのオブジェクトが返されます。`each`メソッドではこのような振る舞いはありません。`each_with_object`の`hash`は、各サービスIDごとに時間帯ごとの空き状況を格納するために使用され、ループ開始時に定義された空のハッシュから始まります。

### 2重の`each_with_object`の意味
外側の`each_with_object`は各サービスに対して実行され、内側の`each_with_object`は各サービスの各時間帯に対して実行されます。内側の`|slot, inner_hash|`は特定のサービスの特定の時間帯の空き状況を表し、外側の`|service, hash|`はその情報を各サービスごとに格納します。これにより、各サービスの時間帯ごとの空き状況が効率的に計算され、格納されます。

## まとめ

この実装では、Ruby on Railsの`each_with_object`メソッドを活用して、複雑なデータ構造を効率的に構築しました。サービス提供可能性のロジックを理解し、それをユーザーが直感的に理解できる形で表示する方法を示しました。このアプローチは、予約システムやリソース管理システムなど、さまざまなアプリケーションで応用可能です。