実用的だよ！Railsのsimple_calendarをカスタマイズして時間帯で予約できる予約機能を作ってみた！

# きっかけ
simple_calendarを使って予約アプリを作って遊ぼうと思った。
↓
simple_calendarで調べてたら出てくる記事が1ヶ月表示の日単位で予約するやつばかりで実用的なレイアウトじゃない。
↓
良いものがないなら自分で作ればいいじゃないか！ということで自分で良い感じの予約機能を作ることにした。

 - イメージ画像
![スクリーンショット 2021-04-04 12.52.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/c3d8849a-de18-c2fb-77ff-4c0070169c01.png)


美容院の予約サイトとかで良く見るレイアウトです。1ヶ月表示するレイアウトよりこっちの方が実用的だと思うんですよ。お店系のアプリとかで使えそうだし。

#### 追記 2021/4/10
 - バリデーションチェックの処理を修正
 - reservationsテーブルを修正
 - 新規予約登録の処理を修正

# 前提条件
 - Railsの知識
 - ある程度コードが読めること
 - もしくは自分で調べて対応できること

simple_calendarの説明をメインにしたいため、アプリの作成方法やマイグレーション等に関する説明はしません。そのため、Railsに関する知識がある方や、必要な知識をご自分で調べていただる方を対象としています。ご了承ください。

また、筆者はRailsを勉強中でこれから転職活動中の身のため、ベストプラクティスでないコードを書いていることがあるかと思います。（作りたいものは実装できるけど、コードの品質は低いというレベル感）その場合はコメントでご指摘いただけると幸いです。

# 内容
バージョン
 - ruby 2.7.2
 - rails 6.1.3.1
 - PostgreSQL 13.2

### simple_calendarをinstall
まずはsimple_calendarをinstallします。
Gemfileに追加して`bundle install`

```
gem "simple_calendar", "~> 2.0"
```

### simple_calendarのviewを作成
今回はsimple_calendarをカスタマイズするためviewを作成します。

```
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
```

### application.scssに追加
下記のコードを追加することでsinple_calendarのcssを読み込みます。（Bootstarpを使用しているためscssファイルですが、使用してない方はcssファイルとなります。）

```application.scss
*= require simple_calendar
```

### コントローラーとモデルを作成
今回はreservationsコントローラとreservationモデルを作成しました。deviseも入れてますが、そちらは各自お願いします。
参考：https://qiita.com/take18k_tech/items/a36d77316e32a6696205

```
rails g controller reservations
rails g model reservation
```

reservationsテーブルはこんな感じ。`start_time`カラムはsimple_calendarで必要となるカラムです。今回は使用しませんが、このカラムがないと予約した時にエラーを吐くので追加しています。

```schema.rb
create_table "reservations", force: :cascade do |t|
    t.date "day", null: false
    t.string "time", null: false
    t.bigint "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.datetime "start_time", null: false
    t.index ["user_id"], name: "index_reservations_on_user_id"
end
```

### simple_calendarを表示
早速sinple_calendarを表示してみましょう。今回は1週間単位で表示するため、_week_calendar.html.erbを使用します。下記のコードを追加するだけでカレンダーが表示されます。

```erb
<%= week_calendar events: @reservations do |date, reservations| %>
    <%= date.day %>
<% end %>
```

```erb
<!-- index.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center">
      <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>
```

simple_calendarを表示するために`@reservations`を定義します。DBアクセス負荷を考えてここでは今日から3ヶ月先までのデータを取得してますが、表示でしか使っていないのでDB負荷さえなければなんでも大丈夫なはずです。

```reservartions_controller.rb
class ReservationsController < ApplicationController
  def index
    @reservations = Reservation.all.where("day >= ?", Date.current).where("day < ?", Date.current >> 3).order(day: :desc)
  end
end
```

![スクリーンショット 2021-04-04 15.28.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/5a32fcd4-ea4f-4614-bfa2-cd4e028585a6.png)


### simple_calendarをカスタマイズ

#### 1.時間帯を追加する
簡単にsinple_calendarを表示することができましたが、今は日付と曜日が表示されているだけです。まずは時間帯も表示できるようにしましょう。

時間帯の配列を作成します。

```rb
#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",
             "15:00",
             "15:30",
             "16:00",
             "16:30"]
  end
end
```

helperで定義した`times`メソッドを使用して時間帯を表示します。

```erb
<!--_week_calendar.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: '1週間') %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: '1週間') %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
  </div>
  <table class="table table-striped">
    <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>
          <td></td>
          <% 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>
```

これで時間帯を表示することができました。以下の画像のようになるかと思います。

![スクリーンショット 2021-04-04 13.46.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/76849f1f-47ca-fb3f-5367-7e291a10e7f7.png)


#### 2.予約を取得する
時間帯が追加できたので次は予約状況を取得しましょう。

reservationモデルにreservationsテーブルから予約を取得するメソッドを作成します。取得する際にDBアクセスを減らすために配列に必要なデータを追加しています。`day`と`time`は予約状況の確認で必要となります。

```reservation.rb
class Reservation < ApplicationRecord

# 略

def self.reservations_after_three_month
    # 今日から3ヶ月先までのデータを取得
    reservations = Reservation.all.where("day >= ?", Date.current).where("day < ?", Date.current >> 3).order(day: :desc)
    # 配列を作成し、データを格納
    # DBアクセスを減らすために必要なデータを配列にデータを突っ込んでます
    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
```

#### 3.予約の有無によって画面の表示を変更する
予約を取得できたので、次は予約状況によって画面表示を変更する処理を実装します。

まずはhelperに予約状況を判定するメソッドを作成します。先ほど作成した`self.reservations_after_three_month`メソッドの返り値の配列とテーブルの`day`と`time`と一致するか判定します。

```reservations_helper.rb
module ReservationsHelper

  # 略

  def check_reservation(reservations, day, time)
    result = false
    reservations_count = reservations.count
    # 取得した予約データにdayとtimeが一致する場合はtrue,一致しない場合はfalseを返します
    if reservations_count > 1
      reservations.each do |reservation|
        result = reservation[:day].eql?(day.strftime("%Y-%m-%d")) && reservation[:time].eql?(time)
        return result if result
      end
    elsif reservations_count == 1
      result = reservations[0][:day].eql?(day.strftime("%Y-%m-%d")) && reservations[0][:time].eql?(time)
      return result if result
    end
    return result
  end
end
```

先ほどモデルで作成した`reservations_after_three_month`メソッドを使用して予約データを取得します。
そして`reservations_after_three_month `メソッドの返り値を`check_reservation`メソッドの引数にします。`true`の場合は予約があるので「×」、falseの場合は予約がないので「○」を表示するように条件分岐します。

```erb
<!--_week_calendar.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: '1週間') %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: '1週間') %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
    <!-- 追加 -->
    <% reservations = Reservation.reservations_after_three_month %>
  </div>
  <table class="table table-striped">
    <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>
          <td></td>
          <% 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) %>
                  <%= '×' %>
                <% else %>
                  <%= '○' %>
                <% end %>
                <!-- ここまで -->
              </td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
</div>
```

ここまでの画面は以下。皆さんは予約が入っていないため全て「○」になっているはずです。

![スクリーンショット 2021-04-04 14.13.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/d30f5d54-5d68-6a53-9319-77ab0546db52.png)


#### 4.予約がない場合に新規予約できるようにする
予約状況によって画面表示を変更する処理が実装できたので、次は新規予約ができるようにします。

まずは新規予約画面を作成します。`start_time`のhidden_fieldで送信している`@start_time`はをコントローラーで日付と時間と整形しDateTime型に変換した値です。

```erb
<!-- new.html.erb -->
<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>新規予約画面</h1>
    </div>
    <div class="col-12 mt-3 content">
      <%= 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>
```

新規登録後に表示する画面も作成します。

```erb
<!-- show.html.erb -->
<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>予約詳細画面</h1>
    </div>
    <div class="col-12 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>
```

```reservations_controller.rb
class ReservationsController < ApplicationController
  before_action :authenticate_user!

  # 略

  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)
    if @reservation.save
      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
end
```
予約がない場合に`link_to`を使用して新規予約画面へのリンクを実装します。その際に`day`と`time`をパラメータで渡します。

```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: '1週間') %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: '1週間') %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
    <% reservations = Reservation.reservations_after_three_month %>
  </div>
  <table class="table table-striped">
    <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>
          <td></td>
          <% 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) %>
                  <%= '×' %>
                <% else %>
                  <!-- 追加 -->
                  <%= link_to new_reservation_path(day: day, time: time) do %>
                    <%= '○' %>
                  <% end %>
                  <!-- ここまで -->
                <% end %>
              </td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
</div>
```

ここまで実装すると一番最初のイメージ図通りになります。

![スクリーンショット 2021-04-04 12.52.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/26e7c64e-b3c6-7ab9-66ef-177a5e5c466f.png)

「○」をクリックすると新規予約画面に飛びます。
![スクリーンショット 2021-04-04 14.23.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/57dd1a7a-c0b0-ec53-17bf-ce2d9203f221.png)

予約ボタンをクリックすると登録完了。
![スクリーンショット 2021-04-04 14.23.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/0b2a2330-5bc6-e28c-44a9-c096e3819345.png)

#### カスタムバリデーションを追加
予約データは今日から3ヶ月先まで取得しているので、前日までの日付と3ヶ月以降の日付で登録できないようにする必要があります。また、当日も予約できないようにします。

```reservation.rb
class Reservation < ApplicationRecord
  # 略

  validate :date_before_start
  validate :date_current_today
  validate :date_three_month_end

  def date_before_start
    errors.add(:day, "は過去の日付は選択できません") if day < Date.current
  end

　def date_current_today
    errors.add(:day, "は当日は選択できません。予約画面から正しい日付を選択してください。") if day < (Date.current + 1)
  end

  def date_three_month_end
    errors.add(:day, "は3ヶ月以降の日付は選択できません") if (Date.current >> 3) < day
  end
end
```

画面で実際に見てみます。

<br>

・今日より前の日付を選択した場合
![スクリーンショット 2021-04-04 14.39.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/0959b20d-9b94-c286-e00f-3409a24609dc.png)
・3ヶ月以降の日付を選択した場合
![スクリーンショット 2021-04-04 14.40.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/948f5c55-50ee-f78e-f22f-01d7f85d6fca.png)

バリーションチェックが正常に機能していますが、ページが更新されたためフォームから値が消えてしまっています。また、ユーザ側で入力する必要がないため、バリデーションチェックで弾かれる場合は新規予約画面に遷移する必要はありません。

そのため、以下のように修正します。
予約画面で新規予約画面へのボタン（○）を押下時にチェックが走るようにする。そしてエラーの場合は予約画面でメッセージを表示する。

```reservation.rb
　# 略

　# 追加
　def self.check_reservation_day(day)
    if day < Date.current
      return "過去の日付は選択できません。正しい日付を選択してください。"
    elsif day < (Date.current + 1)
      return "当日は選択できません。正しい日付を選択してください。"
    elsif (Date.current >> 3) < day
      return "3ヶ月以降の日付は選択できません。正しい日付を選択してください。"
    end
  end

　# 略
```

```reservations_controller.rb

　def new
    @reservation = Reservation.new
    @day = params[:day]
    @time = params[:time]
    @start_time = DateTime.parse(@day + " " + @time + " " + "JST")
　　 # 追加 
    message = Reservation.check_reservation_day(@day.to_date)
    if !!message
      redirect_to @reservation, flash: { alert: message }
    end
  end

```

簡単に解説すると、新規予約画面へのボタン（○）が押下された時にストロングパラメータの`day`を引数にし、check_reservation_dayメソッドで正しい日付かチェックし、過去・当日・３ヶ月の場合は条件に応じてメッセージを返します。メッセージが存在する場合はエラーということになるため、メッセージが存在する場合は新規予約画面へ遷移せずに予約画面でエラーメッセージを表示します。

そもそもsimple_calendarで過去と当日、3ヶ月以降は選択できないようにするべきなのですが、実装方法を模索中です。simple_calenderのgithubを見ているのですが、どこを弄れば良いかまだ分かってません。

# おまけ
せっかくなのでマイページで予約一覧と過去の予約履歴も見れるようにします。マイページを作成する方法はこちらを参考にしてください。
参考：https://qiita.com/Densetsu/items/def2189153ddb79d9dce

```users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(current_user.id)
    @user_reservations = current_user.reservations.where("start_time >= ?", DateTime.current).order(day: :desc)
    @visit_historys = current_user.reservations.where("start_time < ?", DateTime.current).where("start_time > ?", DateTime.current << 12).order(day: :desc)
  end
end
```

```erb
<!-- users/show.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>マイページ</h1>
    </div>
    <div class="col-12 mt-3 text-center content">
      <div>
        <label><strong>名前：</strong></label>
        <p style="display:inline;"><%= @user.name %></p>
      </div>
      <div>
        <label><strong>メールアドレス：</strong></label>
        <p style="display:inline;"><%= @user.email %></p>
      </div>
      <div class="col-12 mt-3 text-center mx-auto">
        <h3>予約一覧</h3>
        <table class="table">
          <thead>
            <tr>
              <th scope="col">予約日</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <% @user_reservations.each do |user_reservation| %>
              <td>
                <%= user_reservation.day %>
                <%= user_reservation.time %>              
              </td>
              <td>
                <%= link_to '削除', reservation_path(user_reservation.id), method: :delete, data: { confirm: "削除してよろしいですか？", commit: "削除する", cancel: "やめる", title: "ご確認ください" } %>
              </td>
            </tr>             
            <% end %>
            <% if @user_reservations.blank? %>
              <tr>
                <td>予約はありません。</td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
      <div class="col-12 mt-3 text-center mx-auto">
        <h3>来院履歴</h3>
        <table class="table">
          <thead>
            <tr>
              <th scope="col">予約日</th>
            </tr>
          </thead>
          <tbody>
            <% @visit_historys.each do |visit_history| %>
              <tr>
                <td>
                  <%= visit_history.day %>
                  <%= visit_history.time %>              
                </td>
              </tr>
            <% end %>
            <% if @visit_historys.blank? %>
              <tr>
                <td>来院履歴はありません。</td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
```

```reservations_controller.rb
class ReservationsController < ApplicationController
  # 略

  def destroy
    @reservation = Reservation.find(params[:id])
    if @reservation.destroy
      flash[:success] = "予約を削除しました。"
      redirect_to user_path(current_user.id)
    else
      render :show
    end
  end
  
  # 略
end
```

実装した画面はこんな感じ。
![スクリーンショット 2021-04-04 14.33.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995665/ddade8ba-4d36-b8ff-3d6e-ccac9c0c38a9.png)

# 最後に
課題は残っていますが、これで今回の予約管理の実装は完了です。一例としてレイアウトのカスタマイズ以降の実装も書きましたが、それぞれお好きなように実装していただければと思います。

 - 実装時間： 約10h（設計等も含めて）
 - Qiita執筆： 約3h（言語化するの難しい）

##### 残っている課題
 - simple_calendarの表示期間の限定化
 - マイページに履歴を追加しているが、キャンセルした場合などのステータスの実装（ステータスとかを加えるとだいぶ実装コストが増えるのでやりたくない）

# 今回実装したコード

```reservations_controller.rb
class ReservationsController < ApplicationController
  before_action :authenticate_user!

  def index
    @reservations = Reservation.all.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")
    message = Reservation.check_reservation_day(@day.to_date)
    if !!message
      redirect_to @reservation, flash: { alert: message }
    end
  end

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

  def create
    @reservation = Reservation.new(reservation_params)
    if @reservation.save
      redirect_to reservation_path @reservation.id
    else
      render :new
    end
  end

  def destroy
    @reservation = Reservation.find(params[:id])
    if @reservation.destroy
      redirect_to user_path(current_user.id), flash: { success: "予約を削除しました" }
    else
      render :show, flash: { error: "予約の削除に失敗しました" }
    end
  end

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

```

```reservation.rb
class Reservation < ApplicationRecord
  belongs_to :user

  validates :day, presence: true
  validates :time, presence: true
  validates :start_time, presence: true, uniqueness: true

  validate :date_before_start
  validate :date_current_today
  validate :date_three_month_end

  def date_before_start
    errors.add(:day, "は過去の日付は選択できません。予約画面から正しい日付を選択してください。") if day < Date.current
  end

  def date_current_today
    errors.add(:day, "は当日は選択できません。予約画面から正しい日付を選択してください。") if day < (Date.current + 1)
  end

  def date_three_month_end
    errors.add(:day, "は3ヶ月以降の日付は選択できません。予約画面から正しい日付を選択してください。") if (Date.current >> 3) < day
  end

  def self.check_reservation_day(day)
    if day < Date.current
      return "過去の日付は選択できません。正しい日付を選択してください。"
    elsif day < (Date.current + 1)
      return "当日は選択できません。正しい日付を選択してください。"
    elsif (Date.current >> 3) < day
      return "3ヶ月以降の日付は選択できません。正しい日付を選択してください。"
    end
  end

  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

```

```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",
             "15:00",
             "15:30",
             "16:00",
             "16:30"]
  end

  def check_reservation(reservations, day, time)
    result = false
    reservations_count = reservations.count
    if reservations_count > 1
      reservations.each do |reservation|
        result = reservation[:day].eql?(day.strftime("%Y-%m-%d")) && reservation[:time].eql?(time)
        return result if result
      end
    elsif reservations_count == 1
      result = reservations[0][:day].eql?(day.strftime("%Y-%m-%d")) && reservations[0][:time].eql?(time)
      return result if result
    end
    return result
  end
end
```

```erb
<!-- users/_week_calendar.html.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: '1週間') %></span>
    <% else %>
      <span class="calendar-title"><%= t('simple_calendar.week', default: '1週間') %></span>
    <% end %>
    <%= link_to t('simple_calendar.next', default: '翌週'), calendar.url_for_next_view %>
    <% reservations = Reservation.reservations_after_three_month %>
  </div>
  <table class="table table-striped">
    <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>
          <td><%= t('date.month_names')[start_date.month] %></td>
          <% 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) %>
                  <%= '×' %>
                <% else %>
                  <%= link_to new_reservation_path(day: day, time: time) do %>
                    <%= '○' %>
                  <% end %>
                <% end %>
              </td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
</div>

```

```erb
<!-- index.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center">
      <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>
```

```erb
<!-- new.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>新規予約画面</h1>
    </div>
    <div class="col-12 mt-3 content">
      <%= 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>
```

```erb
<!-- show.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>予約詳細画面</h1>
    </div>
    <div class="col-12 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>
```

```schema.rb
ActiveRecord::Schema.define(version: 2021_04_03_160857) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "reservations", force: :cascade do |t|
    t.date "day", null: false
    t.string "time", null: false
    t.bigint "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.datetime "start_time"
    t.index ["user_id"], name: "index_reservations_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "name", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  add_foreign_key "reservations", "users"
end
```

```users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(current_user.id)
    @user_reservations = current_user.reservations.where("day >= ?", Date.current).order(day: :desc)
    @visit_historys = current_user.reservations.where("day < ?", Date.current).where("day > ?", Date.today << 12).order(day: :desc)
  end
end
```

```erb
<!-- users/show.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-12 text-center title">
      <h1>マイページ</h1>
    </div>
    <div class="col-12 mt-3 text-center content">
      <div>
        <label><strong>名前：</strong></label>
        <p style="display:inline;"><%= @user.name %></p>
      </div>
      <div>
        <label><strong>メールアドレス：</strong></label>
        <p style="display:inline;"><%= @user.email %></p>
      </div>
      <div class="col-12 mt-3 text-center mx-auto">
        <h3>予約一覧</h3>
        <table class="table">
          <thead>
            <tr>
              <th scope="col">予約日</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <% @user_reservations.each do |user_reservation| %>
              <td>
                <%= user_reservation.day %>
                <%= user_reservation.time %>              
              </td>
              <td>
                <%= link_to '削除', reservation_path(user_reservation.id), method: :delete, data: { confirm: "削除してよろしいですか？", commit: "削除する", cancel: "やめる", title: "ご確認ください" } %>
              </td>
            </tr>             
            <% end %>
            <% if @user_reservations.blank? %>
              <tr>
                <td>予約はありません。</td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
      <div class="col-12 mt-3 text-center mx-auto">
        <h3>来院履歴</h3>
        <table class="table">
          <thead>
            <tr>
              <th scope="col">予約日</th>
            </tr>
          </thead>
          <tbody>
            <% @visit_historys.each do |visit_history| %>
              <tr>
                <td>
                  <%= visit_history.day %>
                  <%= visit_history.time %>              
                </td>
              </tr>
            <% end %>
            <% if @visit_historys.blank? %>
              <tr>
                <td>来院履歴はありません。</td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
```

```routes.rb
Rails.application.routes.draw do
  get "users/show"
  root to: "home#index"
  devise_for :users
  resources :users, :only => [:show]
  resources :reservations
end
```