学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Rails6.0 | 民泊サイトの構築(改良版)| 35 |フルカレンダーの実装 | for MacOSX



| 34 |ホームページとAJAX検索 <<  [ホーム] >> | 36 |ページネーションの実装


フルカレンダーをインストールします。


コマンド
一文です。
yarn add @fullcalendar/core@5.9.0 @fullcalendar/daygrid@5.9.0 @fullcalendar/interaction@5.9.0 @fullcalendar/list@5.9.0


Reservationモデルにstatusカラムを追加します。
コマンド
rails g migration AddStatusToReservations status:bigint


記述追加 db\migrate\20200728030757_add_status_to_reservations.rb
3行目に「, default: 0」の記述追加

class AddStatusToReservations < ActiveRecord::Migration[6.0]
  def change
    add_column :reservations, :status, :bigint, default: 0
  end
end



コマンド
rails db:migrate


「app/javascript/packs/application.js」ファイルに以下の記述を追加します。


記述追加 【app/javascript/packs/application.js(21行目)】

window.Calendar = require("@fullcalendar/core").Calendar;
window.DayGridPlugin = require("@fullcalendar/daygrid").default;
window.interactionPlugin = require("@fullcalendar/interaction").default;
window.ListPlugin = require("@fullcalendar/list").default;



【app/javascript/packs/application.js】

// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

require('jquery');
require("jquery-ui/ui/widgets/datepicker");
require("jquery-ui/ui/widgets/slider");

require("raty-js")

window.Noty = require("noty")
window.Dropzone = require("dropzone")
window.BulmaCarousel = require("bulma-extensions/bulma-carousel/dist/js/bulma-carousel")

window.Calendar = require("@fullcalendar/core").Calendar;
window.DayGridPlugin = require("@fullcalendar/daygrid").default;
window.interactionPlugin = require("@fullcalendar/interaction").default;
window.ListPlugin = require("@fullcalendar/list").default;

$(document).on('turbolinks:load', () => {
    $('.toggle').on('click', (e) => {
        e.stopPropagation();
        e.preventDefault();
        $('#' + e.target.getAttribute('aria-controls')).toggleClass('is-hidden');
    })
})

// Uncomment to copy all static images under ../images to the output folder and reference
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
// or the `imagePath` JavaScript helper below.
//
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)

require("trix")
require("@rails/actiontext")



「app\controllers」フォルダに「calendars_controller.rb」ファイルを新規作成します。


app\controllers\calendars_controller.rb(新規作成したファイル)

class CalendarsController < ApplicationController

    before_action :authenticate_user!

    include ApplicationHelper
    
    def host
      @rooms = current_user.rooms
      params[:start_date] ||= Date.current.to_s
      params[:room_id] ||= @rooms[0] ? @rooms[0].id : nil
      if params[:q].present?
        params[:start_date] = params[:q][:start_date]
        params[:room_id] = params[:q][:room_id]
      end
      @search = Reservation.ransack(params[:q])
      if params[:room_id]
        @room = Room.find(params[:room_id])
        start_date = Date.parse(params[:start_date])
        first_of_month = (start_date - 1.months).beginning_of_month # 最初の月の1日から
        end_of_month = (start_date + 1.months).end_of_month # => 3ヶ月後の31まで
        @events = @room.reservations.joins(:user)
                        .select('reservations.*, users.full_name, users.email')
                        .where('(start_date BETWEEN ? AND ?) AND reservations.status <> ?', first_of_month, end_of_month, 2)

      else
        @room = nil
        @events = []
      end
    end
  end
  
  



記述追加 config\routes.rb
12行目に「get '/host_calendar' => "calendars#host"」の記述追加

Rails.application.routes.draw do

  # ルートを app\views\pages\home.html.erb に設定
  root 'pages#home'

  get 'pages/home'
  get '/dashboard', to: 'users#dashboard'
  get '/users/:id', to: 'users#show', as: 'user'
  get '/your_trips' => 'reservations#your_trips'
  get '/your_reservations' => 'reservations#your_reservations'
  get 'search' => 'pages#search'
  get '/host_calendar' => "calendars#host"
  
  post '/users/edit', to: 'users#update'

  devise_for :users, 
  path: '', 
  path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'},
  controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'}

  resources :rooms, except: [:edit] do
    member do
      get 'listing'
      get 'pricing'
      get 'description'
      get 'photo_upload'
      get 'amenities'
      get 'location'
      get 'preload'
      get 'preview'
      delete :delete_photo
      post :upload_photo
    end
    resources :reservations, only: [:create]
  end

  resources :guest_reviews, only: [:create, :destroy]
  resources :host_reviews, only: [:create, :destroy]

  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end



「app\views」フォルダに「calendars」フォルダを新規作成します。
作成した「calendars」フォルダに「host.html.erb」ファイルを新規作成します。



app\views\calendars\host.html.erb(新規作成したファイル)

<% if !@rooms.blank? %>
  <div class="row" style="margin-bottom: 7rem;">
    <%= search_form_for @search, class: 'form-group', remote: true, url: host_calendar_path do |f| %>
      <div class="container">
          <br/>
          <div class="field">
            <div class="control">
              <label>登録している部屋を選択</label><br/>
                <%= f.select :room_id, options_for_select(@rooms.collect {|u| [u.listing_name, u.id]}, params[:room_id]), {}, {
                  onchange: "submit(this.form);",
                  class: "select is-primary",
                  style: "width: 30rem;"
                } %>
            </div>
          </div>
      </div>
      <%= f.hidden_field :start_date, id: "start-date", value: params[:start_date], onchange: "submit(this.form);" %>
    <% end %>
  </div>
<% end %>
<section class="section" style="margin-bottom: 7rem;">
    <div class="container">
<div id="calendar"></div>
</div>
</section>
<!-- moment lib -->
<script src='https://cdn.jsdelivr.net/npm/moment@2.27.0/min/moment.min.js'></script>
<script>
  window.reservations = <%= raw @events.to_json %>
  console.log(reservations);
  function showReservations(data) {
    return data.map(function (e) {
      if (e['start_date'] !== e['end_date']) {
        e['end_date'] = moment(e['end_date']).add(1, 'days').format();
      }
        return {
          title: e.full_name ,
          start: e['start_date'],
          end: e['end_date'],
          allDay: true,  
          
        }
    });
  }  
  $(function() {
    var calendarEl = document.getElementById('calendar');
    var calendar = new Calendar(calendarEl, {
  
      headerToolbar: {
        left: 'prev,next',
        center: 'title',
        right: 'dayGridMonth,listMonth'
      },
        initialDate: $('#start-date').val(),
        timeZone: 'JST',
        locale: 'ja', 
        plugins: [DayGridPlugin, interactionPlugin, ListPlugin],
        initialView: 'dayGridMonth',
        eventColor: 'blue',
        events: showReservations(reservations),
    });
    calendar.render();
  });
  
  $('.fc-prev-button').click(function() {
    var current = new Date($('#start-date').val());
    var prev = new Date(current.getFullYear(), current.getMonth() - 1, 1)
    $('#start-date').val(moment(prev).format('YYYY-MM-DD'))
    $('#start-date').trigger('change')
  });
  $('.fc-next-button').click(function() {
      var current = new Date($('#start-date').val());
      var next = new Date(current.getFullYear(), current.getMonth() + 1, 1)
      $('#start-date').val(moment(next).format('YYYY-MM-DD'))
      $('#start-date').trigger('change')
  });
</script>



「app\views\calendars」フォルダに「host.js.erb」ファイルを新規作成します。


app\views\calendars\host.js.erb(新規作成したファイル)

reservations = <%= raw @events.to_json %>

$('#calendar').fullCalendar('removeEvents');
$('#calendar').fullCalendar('addEventSource', showReservations(reservations));
$('#calendar').fullCalendar('rerenderEvents');



ナビゲーションバーを編集します。


記述追加 「app/views/shared/_navbar.html.erb」
50行目に「 <%= link_to "カレンダー", host_calendar_path, class: "btn btn-warning", style: "margin-left: 0.5rem;" %>」の記述を追加します。

<nav class="navbar navbar-expand-lg navbar-dark bg-dark" style="z-index: 5;">
  <a class="navbar-brand" href="/"><h1 class="navh1"><font style="font-size: 1.2rem"><b>民泊予約サイト</font> <font style="font-size: 0.9rem">Private lodging reservation site</b></font></h1></a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse justify-content-end" id="navbarNavDropdown">
    <ul class="navbar-nav">
    <!-- もしログインしていなかったら-->
    <% if (!user_signed_in?) %>
      <li class="nav-item" style="margin-right: 20px; margin-bottom: 5px;">
        <%= link_to  "まずはユーザ登録 Sign up", new_user_registration_path, class: "btn btn-danger btn-sm" %>
      </li>
      <li class="nav-item">
        <%= link_to  "ユーザ登録がお済みの方はこちら login", new_user_session_path, class: "btn btn-info btn-sm", style: "margin-right: 80px;" %>
      </li>
    <!-- ログインしていたら -->
    <% else %>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

        <figure style="position:relative; top: 0.2rem;" class="avatar <%= current_user.status ? "online" : "offline" %>"></figure>  
        <%= image_tag avatar_url(current_user), class: "bd-placeholder-img figure-img img-fluid rounded-pill", style: "width: 40px; height: 30px; margin-top: 0.5rem;" %>

        <div class="btn btn-light bg-light text-dark"><%= current_user.full_name %></div>
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
        <span class="dropdown-item"><i class="fas fa-user-edit"></i>&nbsp;<%= link_to  "ユーザ登録情報編集", edit_user_registration_path, class: "btn btn-warning" %></span>
          <span class="dropdown-item"><i class="fas fa-sign-out-alt"></i>&nbsp;<%= link_to  "ログアウト", destroy_user_session_path, method: :delete, class: "btn btn-danger" %></span>
        </div>
      </li>
    <% end %>
    </ul>
  </div>
</nav>
<% if (user_signed_in?) %>
  <nav class="navbar navbar-expand-lg navbar-light bg-info" style="z-index: 3;">
    <div class="collapse navbar-collapse justify-content-end" id="navbarNavDropdown">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item" style="margin-top: 7px;">
        <i class="fas fa-tachometer-alt" style="color: white;"></i>&nbsp;<%= link_to 'ダッシュボード Dashboard', dashboard_path, class: "btn btn-light" %>
        </li>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          <i class="fas fa-hospital-symbol" style="color: white;"></i>&nbsp;<div class="btn btn-light bg-light text-dark">ホスト Host</div>
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
            <span class="dropdown-item"><i class="fas fa-edit"></i></i><%= link_to  "部屋を新規登録", new_room_path, class: "btn btn-primary", style: "margin-left: 0.5rem;" %></span>
            <span class="dropdown-item"><i class="far fa-list-alt"></i><%= link_to  "部屋管理", rooms_path, class: "btn btn-success", style: "margin-left: 0.5rem;" %></span>
            <span class="dropdown-item"><i class="far fa-list-alt"></i><%= link_to "受注管理", your_reservations_path, class: "btn btn-danger", style: "margin-left: 0.5rem;" %></span>
            <span class="dropdown-item"><i class="far fa-list-alt"></i><%= link_to "カレンダー", host_calendar_path, class: "btn btn-warning", style: "margin-left: 0.5rem;" %></span>

        </li>&nbsp;&nbsp;
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          <i class="fas fa-user-friends" style="color: white;"></i>&nbsp;<div class="btn btn-light bg-light text-dark">ゲスト Guest</div>
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
            <span class="dropdown-item"><i class="far fa-list-alt"></i><%= link_to "ご予約の確認", your_trips_path, class: "btn btn-warning", style: "margin-left: 0.5rem;" %></span>
          </div>
        </li>
      </ul>
    </div>
  </nav>
<% end %>



ブラウザ確認
http://localhost:3000/host_calendar

フルカレンダー
フルカレンダー



予約のステータスを実装し、受注した予約を完了させる処理をさせます。


記述追加 app\models\reservation.rb(3行目)

   # status: {待ち: 0, 済み: 1}
   enum status: {Waiting: 0, Approved: 1}



app\models\reservation.rb

class Reservation < ApplicationRecord

   # status: {待ち: 0, 済み: 1}
   enum status: {Waiting: 0, Approved: 1}

  belongs_to :user
  belongs_to :room

  has_many :reviews
end



「app\controllers\reservations_controller.rb」ファイルを編集していきます。


1.3行目に以下の記述を追加します。

before_action :set_reservation, only: [:approve]



2.38行目から「approve()」メソッド追加。

    def approve
      @reservation.Approved!
      redirect_to your_reservations_path
    end



3.60行目にプライベートメソッド「set_reservation()」を追加しています。

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



app\controllers\reservations_controller.rb

class ReservationsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_reservation, only: [:approve]

  def create
    room = Room.find(params[:room_id])

    if current_user == room.user
      flash[:alert] = "オーナーが予約することはできません。"
    else

        start_date = Date.parse(reservation_params[:start_date])
        end_date = Date.parse(reservation_params[:end_date])
        days = (end_date - start_date).to_i

        @reservation = current_user.reservations.build(reservation_params)
        @reservation.room = room
        @reservation.price = room.price
        @reservation.total = room.price * days
        @reservation.save

        flash[:notice] = "予約が完了しました。"
      end
      redirect_to room
  end
  def your_trips
    @trips = current_user.reservations.order(start_date: :desc)
    
  end

  def your_reservations
    @rooms = current_user.rooms

    @reservations = Reservation.all

  end

  def approve
    @reservation.Approved!
    redirect_to your_reservations_path
  end

  private
    def reservation_params
      params.require(:reservation).permit(:start_date, :end_date)
    end
    
    def set_reservation
      @reservation = Reservation.find(params[:id])
    end

  end





記述追加 config\routes.rb(40行目)

  resources :reservations, only: [:approve, :decline] do
    member do
      post '/approve' => "reservations#approve"
    end
  end



config\routes.rb

Rails.application.routes.draw do

  # ルートを app\views\pages\home.html.erb に設定
  root 'pages#home'

  get 'pages/home'
  get '/dashboard', to: 'users#dashboard'
  get '/users/:id', to: 'users#show', as: 'user'
  get '/your_trips' => 'reservations#your_trips'
  get '/your_reservations' => 'reservations#your_reservations'
  get 'search' => 'pages#search'
  get '/host_calendar' => "calendars#host"
  
  post '/users/edit', to: 'users#update'

  devise_for :users, 
  path: '', 
  path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'},
  controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'}

  resources :rooms, except: [:edit] do
    member do
      get 'listing'
      get 'pricing'
      get 'description'
      get 'photo_upload'
      get 'amenities'
      get 'location'
      get 'preload'
      get 'preview'
      delete :delete_photo
      post :upload_photo
    end
    resources :reservations, only: [:create]
  end

  resources :guest_reviews, only: [:create, :destroy]
  resources :host_reviews, only: [:create, :destroy]

  resources :reservations, only: [:approve] do
    member do
      post '/approve' => "reservations#approve"
    end
  end

  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end



「app\views\reservations\your_reservations.html.erb」ファイルを編集していきます。


app\views\reservations\your_reservations.html.erb

<div class="row" style="margin-top: 50px; margin-left: 2rem; margin-bottom: 7rem;">
    <div class="card">
    <div class="bg-danger">
        <h3 style="margin-top: 30px; font-size: 1.5rem; color: white; position: relative; bottom: 0.7rem; left: 3rem;">受注 Order received</h3>
    </div>

        <table class="table table-striped text-center">
            <thead>
                <tr>
                    <th>申し込み日</th>
                    <th>部屋</th>
                    <th>ゲスト</th>
                    <th>日程</th>
                    <th>泊数</th>
                    <th>料金</th>
                    <th>レビュー</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <% if @rooms.blank? %>
                  <tr>
                      <td colspan="7"><h5>表示できる受注はありません。</h5></td>
                  </tr>
                <% end %>
                <% @rooms.each do |room| %>
                    <%  room.reservations.sort.reverse.each do |reservation| %>
                    <% if reservation.Waiting? then %>    
                    <tr>
                        <td><%= I18n.l(reservation.created_at, format: :full_date) %></td>

                        <td>
                            <%= link_to room_path(reservation.room), data: { turbolinks: false} do %>                                
                                <%= image_tag room_cover(reservation.room), style: "width: 60px;" %><br/>
                                <span class="badge bg-dark" style="color: white; font-size: 0.5rem;"><%= reservation.room.listing_name %></span>
                            <% end %>
                        </td>
                        <td>
                            <%= link_to user_path(reservation.user), class: "tootip" do %>
                                    <%= image_tag avatar_url(reservation.user), style: "width: 50px;", class: "bd-placeholder-img figure-img img-fluid rounded-pill" %><br/>
                                    <span class="badge bg-dark" style="color: white; font-size: 0.5rem;"><%= reservation.user.full_name %></span>
                            <% end %>
                        </td>
                        <td>
                            <span class="badge bg-success text-light" style="position: relative; top: 0.1rem;">チェックイン</span><%= I18n.l(reservation.start_date, format: :full_date) %><br/>
                            <span class="badge bg-danger text-light" style="position: relative; top: 0.01rem;">チェックアウト</span><%= I18n.l(reservation.end_date, format: :full_date) %>
                        </td>
                        <td><%=reservation.total/reservation.price %></td>
                        <td><%= number_to_currency(reservation.total) %></td>
                        <!-- レビュー -->
                        <td>
                        <% if reservation.end_date < Date.today %>
                            <%= render partial: "reviews/host_form", locals: {reservation: reservation} %>
 
                        <% else %>
                            <span style="font-size: 0.6rem;">チェックアウト後にレビューできます<br/>Can be reviewed after check out</span>
                        <% end %>                        
                        </td>

                        <!-- ステータス -->
                        
                        <td style="padding-top: 30px;">
                            <div class="form-inline">
                            <% if (reservation.end_date < Date.today) %>
                            <% if reservation.Waiting? %>
                                <%= link_to approve_reservation_path(reservation), method: :post do %> <i class="fa fa-thumbs-up fa-lg" style="color: green;"></i> <% end %>
                           
                            <% end %>
                            </div>
                            <% if reservation.Waiting? %>
                            <p class="badge bg-success text-light" style="position: relative; top: 0.1rem;">完了</p>                            
                            <% end  %>
                            <% end %>
                        </td>
                        

                    </tr>
                    <% end %>
                    <% end %>
                <% end %>
            </tbody>
        </table>
    </div>
</div>

<div class="row" style="margin-top: 50px; margin-left: 2rem; margin-bottom: 7rem;">
    <div class="card">
    <div class="bg-dark">
        <h3 style="margin-top: 30px; font-size: 1.5rem; color: white; position: relative; bottom: 0.7rem; left: 3rem;">受注完了 Order completed</h3>
    </div>

        <table class="table table-striped text-center">
            <thead>
                <tr>
                    <th>申し込み日</th>
                    <th>部屋</th>
                    <th>ゲスト</th>
                    <th>日程</th>
                    <th>泊数</th>
                    <th>料金</th>
                    <th>レビュー</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <% if @rooms.blank? %>
                  <tr>
                      <td colspan="7"><h5>表示できる受注はありません。</h5></td>
                  </tr>
                <% end %>
                <% @rooms.each do |room| %>
                    <%  room.reservations.sort.reverse.each do |reservation| %>
                    <% if reservation.Approved? then %>
                    <tr>
                        <td><%= I18n.l(reservation.created_at, format: :full_date) %></td>

                        <td>
                            <%= link_to room_path(reservation.room), data: { turbolinks: false} do %>                                
                                <%= image_tag room_cover(reservation.room), style: "width: 60px;" %><br/>
                                <span class="badge bg-dark" style="color: white; font-size: 0.5rem;"><%= reservation.room.listing_name %></span>
                            <% end %>
                        </td>
                        <td>
                            <%= link_to user_path(reservation.user), class: "tootip" do %>
                                    <%= image_tag avatar_url(reservation.user), style: "width: 50px;", class: "bd-placeholder-img figure-img img-fluid rounded-pill" %><br/>
                                    <span class="badge bg-dark" style="color: white; font-size: 0.5rem;"><%= reservation.user.full_name %></span>
                            <% end %>
                        </td>
                        <td>
                            <span class="badge bg-success text-light" style="position: relative; top: 0.1rem;">チェックイン</span><%= I18n.l(reservation.start_date, format: :full_date) %><br/>
                            <span class="badge bg-danger text-light" style="position: relative; top: 0.01rem;">チェックアウト</span><%= I18n.l(reservation.end_date, format: :full_date) %>
                        </td>
                        <td><%=reservation.total/reservation.price %></td>
                        <td><%= number_to_currency(reservation.total) %></td>
                        <!-- レビュー -->
                        <td>
                        <% if reservation.end_date < Date.today %>
                            <%= render partial: "reviews/host_form", locals: {reservation: reservation} %>
 
                        <% else %>
                            <span style="font-size: 0.6rem;">チェックアウト後にレビューできます<br/>Can be reviewed after check out</span>
                        <% end %>                        
                        </td>

                        <!-- ステータス -->
                        <td style="padding-top: 30px;">
                            <div class="form-inline">
                            <% if reservation.Waiting? %>
                                <%= link_to approve_reservation_path(reservation), method: :post do %> <i class="fa fa-thumbs-up fa-lg"></i> <% end %>
                            <% end %>
                            </div>
                            <% if reservation.Approved? %>
                            <p class="badge bg-secondary text-light" style="font-size: 1rem; margin-top: 0.5rem;">完了</p>
                            <% end  %>
                        </td>

                    </tr>
                    <% end %>
                    <% end %>
                <% end %>
            </tbody>
        </table>
    </div>
</div>



ブラウザ確認
http://localhost:3000/your_reservations


チェックアウトの日付を過ぎると「レビュー」ボタンと「完了」ボタンが表示されます。
完了ボタンを押すと受注完了となります。

受注完了
受注完了



↓↓クリックして頂けると励みになります。


| 34 |ホームページとAJAX検索 <<  [ホーム] >> | 36 |ページネーションの実装