この記事では、Ruby on Railsを使用したアプリケーション開発において、特定の日に特定のサービスが提供可能かどうかを判断し、その結果をユーザーに視覚的に表示する方法について解説します。具体的なシナリオとして、ある会社が複数のサービスを提供しており、それぞれのサービスには複数のスタッフが割り当てられています。スタッフは日ごとに異なるスケジュールで働き、サービス提供の可否はスタッフのスケジュールと提供可能なサービスに基づいています。
基本概念
- サービス(Service): 会社が提供するサービス。例えば、ヘアカット、マッサージなど。
- スタッフ(Staff): サービスを提供する従業員。各スタッフは特定のサービスを提供する能力があり、日によって異なる時間帯に働きます。
-
スケジュール(Schedule): スタッフが特定の日に働く時間帯。
start_time
とend_time
で定義されます。
目標
ある日における各サービスの提供可能性を時間帯ごとに判定し、それを以下の3つの状態で表示する。
-
○
:サービスを提供できるスタッフが2人以上いる時間帯。 -
△
:サービスを提供できるスタッフが1人のみの時間帯。 -
×
:サービスを提供できるスタッフがいない時間帯。
実装手順
1. コントローラーの設定
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
)には、各サービスと時間帯ごとの提供可能性を表示するテーブルを設定します。
<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分間隔のスロット)を生成します。
処理の実行順序
- 会社が提供する全サービスに対してループ(外側の
each_with_object
)。 - 各サービスに対して、一日の時間帯ごとにループ(内側の
each_with_object
)。 - 指定されたサービスを提供でき、特定の時間帯に勤務しているスタッフの選択(
select
)とカウント(count
)。 - 各時間帯ごとにサービス提供可能性(
○×△
)の決定。
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
メソッドを活用して、複雑なデータ構造を効率的に構築しました。サービス提供可能性のロジックを理解し、それをユーザーが直感的に理解できる形で表示する方法を示しました。このアプローチは、予約システムやリソース管理システムなど、さまざまなアプリケーションで応用可能です。