🦏

予約システム 再

2024/08/04に公開

https://qiita.com/sssssatou/items/2e6606e3ddf9b246a0fb
参考にさせていただいた記事

Gem

"simple_calendar"をインストール

gem "simple_calendar", "~> 2.0"
stylesheets/application.css
*= require simple_calendar

追加する

コントローラー作成(一覧のみ記述)

rails g controller reservations
def index
    @reservations = Reservation.all.where("day >= ?", Date.current).where("day < ?", Date.current >> 3).order(day: :desc)
end

あとでコントローラーは追記する

モデル作成

rails g model reservation
db/migrate/xxxxx_create_reservations_rb
class CreateReservations < ActiveRecord::Migration[6.1]
  def change
    create_table :reservations do |t|
      t.date "day", null: false
      t.string "time", null: false
      t.bigint "user_id", null: false
      t.datetime "start_time", null: false
      t.index ["user_id"], name: "index_reservations_on_user_id"
      t.timestamps
    end
  end
end

ルーティング

config/routes.rb
resources :reservations, only: [:index, :new, :create, :show, :edit, :update, :destroy]

一番下に記述(単独でいい。)

ビュー作成

rails g simple_calendar:views

↓ 結果

create  app/views/simple_calendar
create  app/views/simple_calendar/_calendar.html.erb
create app/views/simple_calendar/_month_calendar.html.erb
create  app/views/simple_calendar/_week_calendar.html.erb
views/reservations/index.erb
<div class="container">
  <div class="row">
    <div class="col-12 text-center mt-5">
      <h1>予約画面</h1>
      <p>3ヶ月先まで予約することができます。</p>
    </div>
    <div class="col-12 mt-3">
      <%= week_calendar events: @reservations do |date, reservations| %>
        <%= date.day %>
      <% end %>
    </div>
  </div>
</div>

時間帯を追加する

時間帯の配列を作成

app/helpers/reservations_helper.rb
module ReservationsHelper
  def times
    times = ["9:00",
             "9:30",
             "10:00",
             "10:30",
             "11:00",
             "11:30",
             "13:00",
             "13:30",
             "14:00",
             "14:30",
             "15:00",
             "15:30",
             "16:00",
             "16:30"]
  end 
end
views/simple_calender/_week_calender.html.erb

<div class="simple-calendar">
  <div class="calendar-heading">
    <%= link_to t('simple_calendar.previous', default: '前週'), calendar.url_for_previous_view %>
    <% if calendar.number_of_weeks == 1 %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %> - <%= calendar.end_week %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
  </div>

  <table class="table table-striped">
    <thead>
      <tr>
        <% date_range.slice(0, 7).each do |day| %>
          <th><%= t('date.abbr_day_names')[day.wday] %></th>
        <% end %>
      </tr>
    </thead>

    <tbody>
      <% date_range.each_slice(7) do |week| %>
        <tr>
          <% week.each do |day| %>
            <%= content_tag :td, class: calendar.td_classes_for(day) do %>
              <% if defined?(Haml) && respond_to?(:block_is_haml?) && block_is_haml?(passed_block) %>
                <% capture_haml(day, sorted_events.fetch(day, []), &passed_block) %>
              <% else %>
                <% passed_block.call day, sorted_events.fetch(day, []) %>
              <% end %>
            <% end %>
          <% end %>
        </tr>
< !-- ここから -->
        <% times.each do |time| %>
          <tr>
            <td><%= time %></td>
            <% week.each do |day| %>
              <td></td>
            <% end %>
          </tr>
        <% end %>
<!-- ここまで -->
      <% end %>
    </tbody>
  </table>
</div>
  • helperで定義したtimesメソッドを使用して時間帯を表示する。

予約を取得する

models/reservation.rb
class Reservation < ApplicationRecord
  
  def self.reservations_after_three_month
    reservations = Reservation.all.where("day >= ?", Date.current).where("day < ?", Date.current >> 3).order(day: :desc)
    reservation_data = []
    reservations.each do |reservation|
      reservations_hash = {}
      reservations_hash.merge!(day: reservation.day.strftime("%Y-%m-%d"), time: reservation.time)
      reservation_data.push(reservations_hash)
    end
    reservation_data
  end
end
  • reservationモデルreservationsテーブルから予約を取得するメソッドを作成する。
  • 取得する際にDBアクセスを減らすために配列に必要なデータを追加している。
  • daytimeは予約状況の確認で必要になる。

app/helpers/reservations_helper.rb
  def check_reservation(reservations, day, time)
    start_time = DateTime.parse("#{day} #{time} JST")
    reservations.any? { |reservation| reservation.start_time == start_time }
  end
  • 予約の有無によって画面の表示を変更する

views/simple_calender/_week_calender.html.erb
<div class="simple-calendar">
  <div class="calendar-heading">
    <%= link_to t('simple_calendar.previous', default: '前週'), calendar.url_for_previous_view %>
    <% if calendar.number_of_weeks == 1 %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %> - <%= calendar.end_week %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
  </div>

  <table class="table table-striped">
    <thead>
      <tr>
        <% date_range.slice(0, 7).each do |day| %>
          <th><%= t('date.abbr_day_names')[day.wday] %></th>
        <% end %>
      </tr>
    </thead>

    <tbody>
      <% date_range.each_slice(7) do |week| %>
        <tr>
          <% week.each do |day| %>
            <%= content_tag :td, class: calendar.td_classes_for(day) do %>
              <% if defined?(Haml) && respond_to?(:block_is_haml?) && block_is_haml?(passed_block) %>
                <% capture_haml(day, sorted_events.fetch(day, []), &passed_block) %>
              <% else %>
                <% passed_block.call day, sorted_events.fetch(day, []) %>
              <% end %>
            <% end %>
          <% end %>
        </tr>
        <% times.each do |time| %>
          <tr>
            <td><%= time %></td>
            <% week.each do |day| %>
              <td>
<!--ここから--->
                <% if check_reservation(@reservations, day, time) %>
                  <%= 'x' %>
                <% else %>
                  <%= 'o' %>
                <% end %>
<!--ここまで--->
              </td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
</div>
  • reservations_after_three_monthメソッドを使用して、予約データを取得する
  • reservations_after_three_monthメソッドの返り値をcheck_reservationメソッドの引数にする。
  • trueの場合は予約がある → "x"
  • falseの場合は予約がない → "o"


※ 予約がない状態なので、全て丸

"o"をクリックして、新規予約できるようにする

新規予約画面を作成する(new.html.erb)

views/reservations/new.html.erb
<div class="container">
  <div class="row justify-content-center">
    <div class="col-12 text-center title mt-5">
      <h1>新規予約画面</h1>
    </div>
    <div class="col-6 mt-3 content text-center">
      <%= form_with model: @reservation, local: true, class: 'form' do |form| %>
        <!--%= render 'layouts/error_messages', model: form.object %>-->
          <div class="day form-group ">
            <%= form.label :day, '日付' %>
            <%= form.text_field :day, class: 'form-control', value: @day, readonly: true %>
          </div>
          <div class="time form-group">
            <%= form.label :time, '時間' %>
            <%= form.text_field :time, class: 'form-control', value: @time, readonly: true %>
          </div>
          <%= form.hidden_field :user_id, value: current_user.id %>
          <%= form.hidden_field :start_time, value: @start_time %>
          <div class="submit" >
            <%= form.submit value: '予約する', class: 'btn btn-primary mx-auto d-block' %>
          </div>
      <% end %>
      <div class="col-12 text-right">
        <%= link_to '戻る', reservations_path %>
      </div>
    </div>
  </div>
</div>

予約詳細画面を作成する

views/reservations/show.html.erb
<div class="container">
  <div class="row justify-content-center">
    <div class="col-12 text-center title mt-5">
      <h1>予約詳細画面</h1>
    </div>
    <div class="col-6 mt-3 text-center content">
      <div>
        <label><strong>日付:</strong></label>
        <p style="display:inline;"><%= @reservation.day %></p>
      </div>
      <div>
        <label><strong>時間:</strong></label>
        <p style="display:inline;"><%= @reservation.time %></p>
      </div>
    </div>
    <div class="col-12 text-right">
      <%= link_to '予約画面に戻る', reservations_path %>
    </div>
  </div>
</div>

コントローラー追加

controllers/reservations_controller.rb
 def new
    @reservation = Reservation.new
    @day = params[:day]
    @time = params[:time]
    @start_time = DateTime.parse("#{@day} #{@time} JST")
  end

  def show
    @reservation = Reservation.find(params[:id])
  end

  def create
      @reservation = Reservation.new(reservation_params)
      @reservation.start_time = Time.zone.parse(params[:reservation][:start_time])
      @reservation.user_id = current_user.id
    if @reservation.save
      Rails.logger.debug "Reservation created successfully: #{@reservation.inspect}"
      redirect_to reservation_path(@reservation.id)
    else
      render :new
    end
  end

  private

  def reservation_params
    params.require(:reservation).permit(:day, :time, :user_id, :start_time)
  end

カレンダーに追記する

  • 予約がない"o"の場合に、link_toを使用して新規予約画面へのリンクを実装する。
  • daytimeをパラメーターで渡す。
_week_calender.html.erb
<div class="simple-calendar">
  <div class="calendar-heading text-center p-2 mb-4" style="background-color: #f8f9fa; border-radius: 5px;">
    <%= link_to t('simple_calendar.previous', default: '前週'), calendar.url_for_previous_view, class: 'btn btn-sm btn-outline-primary' %>
    <% if calendar.number_of_weeks == 1 %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: 'Week') %> <%= calendar.week_number %> - <%= calendar.end_week %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view, class: 'btn btn-sm btn-outline-primary' %>
  </div>

  <table class="table table-bordered text-center">
    <thead>
      <tr>
        <th></th>
        <% date_range.slice(0, 7).each do |day| %>
          <th><%= t('date.abbr_day_names')[day.wday] %></th>
        <% end %>
      </tr>
    </thead>

    <tbody>
      <% date_range.each_slice(7) do |week| %>
        <tr>
          <th></th>
          <% week.each do |day| %>
            <%= content_tag :td, class: calendar.td_classes_for(day), style: "vertical-align: top; padding: 10px;" do %>
              <div>
                <strong><%= day.day %></strong>
                <ul class="list-unstyled">
                  <% sorted_events.fetch(day, []).each do |reservation| %>
                    <!--<li style="background-color: #fff3cd; border-radius: 5px; padding: 5px; margin-top: 5px;">-->
                      <!--時間: %= reservation.time %> - 予約者: %= reservation.user.name %>-->
                    <!--</li>-->
                  <% end %>
                </ul>
              </div>
            <% end %>
          <% end %>
        </tr>
        <% times.each do |time| %>
          <tr>
            <td><%= time %></td>
            <% week.each do |day| %>
              <td>
                <% if check_reservation(@reservations, day, time) %>
                  <%= content_tag :span, '✖️', style: "color: red;" %>
                <% else %>
<!--ここから--->
                  <%= link_to new_reservation_path(day: day, time: time), style: "text-decoration: none;" do %>
                    <span style="color: green;">◯</span>
                  <% end %>
<!--ここまで--->
                <% end %>
              </td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
</div>
  • 一覧(レイアウト変更した)

  • "o"がまだ予約ないので、"o"をクリックすると、新規予約画面へ遷移

  • 予約ボタンをクリックすると、予約が完了になる

予約をマイページに表示する

コントローラー追記

users_controller.rb
  def show
    @user = User.find(params[:id])
    @current_entry = Entry.where(user_id: current_user.id)
    @another_entry = Entry.where(user_id: @user.id)
<!--ここから--->
    @user_reservations = current_user.reservations.where("start_time >= ?", DateTime.current).order(day: :desc)
    @visit_histroy = current_user.reservations.where("start_time < ?", DateTime.current).where("start_time > ?", DateTime.current << 12).order(day: :desc)
<!--ここまで--->
:
:

キャンセルの為の記述(ネットからは、2日前しかできない。)

users_controller.rb
  def cancel
    if @reservation.user == current_user # 予約がログイン中のユーザーのものであるかを確認
      if @reservation.day >= Date.today + 2.days
        @reservation.update(status: 'キャンセル済み') # キャンセル処理
        redirect_to user_path(current_user), notice: '予約をキャンセルしました。'
      else
        redirect_to user_path(current_user), alert: '2日前を過ぎた予約は電話でのみキャンセルできます。'
      end
    else
      redirect_to root_path, alert: '他のユーザーの予約をキャンセルすることはできません。'
    end
  end

private

  def set_reservation
    @reservation = Reservation.find(params[:id])
  end
reservation.rb
class ReservationsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_reservation, only: [:show, :destroy]

  def index
    @reservations = Reservation.where("day >= ?", Date.current)
                               .where("day < ?", Date.current >> 3)
                               .order(day: :desc)
  end

  def new
    @reservation = Reservation.new
    @day = params[:day]
    @time = params[:time]
    @start_time = DateTime.parse("#{@day} #{@time} JST")
  end

  def show
    @reservation = Reservation.find(params[:id])
  end

  def create
    @reservation = Reservation.new(reservation_params)
    @reservation.start_time = Time.zone.parse(params[:reservation][:start_time])
    @reservation.user_id = current_user.id
    if @reservation.save
      Rails.logger.debug "Reservation created successfully: #{@reservation.inspect}"
      redirect_to reservation_path(@reservation.id)
    else
      render :new
    end
  end

  def today
    @reservations = Reservation.where(day: Date.today) # 今日の予約を取得
  end
<!--キャンセルをdestroyで定義(cancelをしたい場合は、新しくマイグレーションファイルを作成しないといけない。)--->
  def destroy
    if @reservation.user == current_user
      if @reservation.day >= Date.today + 2.days
        @reservation.destroy
        redirect_to reservations_path, notice: '予約をキャンセルしました。'
      else
        redirect_to reservations_path, alert: '2日前を過ぎた予約は電話でのみキャンセルできます。'
      end
    else
      redirect_to root_path, alert: '他のユーザーの予約をキャンセルすることはできません。'
    end
  end

  private

  def set_reservation
    @reservation = Reservation.find(params[:id])
  end

  def reservation_params
    params.require(:reservation).permit(:day, :time, :user_id, :start_time, menu_ids: [])
  end

  def check_reservation(reservations, day, time)
    start_time = DateTime.parse("#{day} #{time} JST")
    reservations.any? { |reservation| reservation.start_time == start_time }
  end
end

cancel→canselにしてたので、注意。

ビュー追記

users/show.html.rb
<table class="table">
    <h2><strong>予約一覧</strong></h2>
    <thead>
      <tr>
        <th scope="col"><i class="fa-solid fa-scissors"></i> トリミング予約日</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <% @user_reservations.each do |user_reservation| %>
        <tr>
          <td>
            <strong>
            <i class="fa-regular fa-square-check"></i>
              <%= user_reservation.day %>
              <%= user_reservation.time %>
            </strong>
          </td>
          <td>
            <% if user_reservation.day >= Date.today + 2.days %>
              <%= link_to 'キャンセル', cansel_reservation_path, method: :patch, data: { confirm: '本当にキャンセルしますか?' } %>
            <% else %>
              <p>2日前を過ぎたため、キャンセルは電話でのみ受け付けています。</p>
            <% end %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>

トリミング予約日の横に予約したメニューがを表示できるようにしたい。
予約時にラジオボタンでメニュー選べるようにできる。
見た目が質素すぎるけど、練習のため。

おまけ

  • これでできたと思ったけど、お店側が今日の予約確認できるところを作っていなかったので、作りました。
  • 管理者側と会員側と分けてないアプリケーションで練習したので次は、管理者側で一覧を見れるように作る。

ルーティング追加

routes.rb
resources :reservations, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
<!--ココカラ--->
    collection do
      get :today
    end
<!--ここまで--->
  end

コントローラー追加

controllers/reservations_controller.rb
def today
    @reservations = Reservation.where(day: Date.today )
    #今日の予約を取得(reservationsテーブル、予約の日時に関連するカラムは dayなので)
end

ビュー作成

views/reservations/today.html.erb
<div class="container mt-5 p-4" style="background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">
  <h3 class="mb-4 text-center" style="color: #333;">本日の予約一覧</h3>

  <% if @reservations.any? %>
    <div class="list-group">
      <% @reservations.each do |reservation| %>
        <div class="list-group-item list-group-item-action mb-3" style="border-radius: 8px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
          <h5 class="mb-1">ユーザー: <%= reservation.user.name %></h5>
          <p class="mb-1">予約日: <%= reservation.day.strftime('%Y年%m月%d日') %></p>
          <p class="mb-1">予約時間: <%= reservation.time %></p>
        </div>
      <% end %>
    </div>
  <% else %>
    <p class="text-center mt-4" style="color: #666;">本日の予約はありません。</p>
  <% end %>
</div>

予約システムってあと何があったら便利だろう...
メニューによって、一時間じゃ終わらない場合は、自動的に枠を増やしたいけど、それはどうやるのだろう。
トリミングサロンの予約システムで、
犬種と大型(例:12kg~)、中型(例:8kg~)、小型(例:~3kg)、シャンプーのみとか選択したら、
自動的に目安で枠の変動ができたりとか、
性格の問題でトリマーさん一人だとできない子の場合は、トリマー枠を減らすなど、
やりたいことはたくさんあるんだけど、今の頭だとどうやるんだか。

Discussion