E080 管理画面 予約機能の導入

今回は、作成したスケジュールに予約を管理する機能を追加する方法を紹介します。

予約機能作成

①予約データのモデル、マイグレーションファイル、コントローラー一式を作成する。

$sail artisan make:model Reserve --controller --resource --requests --migration

※下記のように表示されていたら成功です。

 INFO  Model [app/Models/Reserve.php] created successfully.  
 INFO  Migration [database/migrations/yyyy_mm_dd_xxxxxx_create_reserves_table.php] created successfully.  
 INFO  Request [app/Http/Requests/StoreReserveRequest.php] created successfully.  
 INFO  Request [app/Http/Requests/UpdateReserveRequest.php] created successfully.  
 INFO  Controller [app/Http/Controllers/ReserveController.php] created successfully.  

②コントローラーのパスを修正する。

app/Http/Controllers/ReserveController.php を app/Http/Controllers/Admin/に移動し、ファイル内のnamespaceを修正します。

※下記、L3~L4を参照

app/Http/Controllers/Admin/ReserveController.php
- namespace App\Http\Controllers;
+ namespace App\Http\Controllers\Admin;

継承クラスのパス対応も追記します。

※下記、L5を参照

app/Http/Controllers/Admin/xxxxxxx.php
  <?php
  
  namespace App\Http\Controllers\Admin;
  
+ use App\Http\Controllers\Controller; 

/*
以下省略
*/

③スケジュールテーブルを作成する。

生成されたマイグレーションファイルを開き、スケジュールテーブルのカラムを作成します。

※下記、L18~L20を参照

database/migrations/yyyy_mm_dd_xxxxxx_create_reserves_table.php
    public function up()
    {
        Schema::create('reserves', function (Blueprint $table) {
            $table->id();
+           $table->foreignId('schedule_id')->constrained();
+           $table->foreignId('user_id')->constrained();
+           $table->timestamp('canceled_at')->nullable();
            $table->timestamps();

            // 同一スケジュールには1つしか予約できないようユニーク制約をつける
            $table->unique(['schedule_id', 'user_id']);
        });
    }

④マイグレーションを実施する。

$sail artisan migrate

⑤ルーティングを追加する。

routes/admin.php に 予約管理機能用のルーティングを追加します。

※下記、L41を参照

routes/admin.php
  Route::middleware('auth:admin')->group(function () {
      // 中略
+    Route::resource('reserve', ReserveController::class);
  });

⑥Controllerの参照を追加する。

※下記、L12を参照

routes/admin.php
  use App\Http\Controllers\Auth\AuthenticatedSessionController;
  use App\Http\Controllers\Auth\ConfirmablePasswordController;
  use App\Http\Controllers\Auth\EmailVerificationNotificationController;
  use App\Http\Controllers\Auth\EmailVerificationPromptController;
  use App\Http\Controllers\Auth\NewPasswordController;
  use App\Http\Controllers\Auth\PasswordController;
  use App\Http\Controllers\Auth\PasswordResetLinkController;
  use App\Http\Controllers\Auth\RegisteredUserController;
  use App\Http\Controllers\Auth\VerifyEmailController;
+ use App\Http\Controllers\Admin\ReserveController

⑦表示用の画面ファイルを作成する。

resources/views/admin/reserve に スケジュール一覧画面、新規登録画面、編集画面の3画面を作成します。

resources/views/admin/reserve/index.blade.php
@extends('adminlte::page')

@section('title', '予約一覧')

@section('content_header')
    <h1>予約一覧</h1>
@stop

@section('content')
    {{-- 完了メッセージ --}}
    @if (session('message'))
        <div class="alert alert-info alert-dismissible">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">
                ×
            </button>
            {{ session('message') }}
        </div>
    @endif

    {{-- 検索 --}}
    <div class="container" th:fragment="search">
		<form action="{{ route('admin.reserve.index') }}" method="get">
			<div class="form-group form-inline input-group-sm">
			    <label for="id" class="col-md-2 control-label">予約番号</label>
			    <input type="text" class="form-control col-md-5" id="id" name="id" placeholder="予約番号" value="{{ @$search_keywords['id'] }}">
			</div>
            <div class="form-group form-inline input-group-sm">
			    <label for="user_name" class="col-md-2 control-label">ユーザー名</label>
			    <input type="text" class="form-control col-md-5" id="user_name" name="user_name" placeholder="ユーザー名" value="{{ @$search_keywords['user_name'] }}">
			</div>
            <div class="form-group form-inline input-group-sm">
			    <label for="schedule_id" class="col-md-2 control-label">スケジュールID</label>
			    <input type="text" class="form-control col-md-5" id="schedule_id" name="schedule_id" placeholder="スケジュールID" value="{{ @$search_keywords['schedule_id'] }}">
			</div>
			<div class="text-center">
				<button class="btn btn-sm btn-outline-secondary" type="submit">検索</button>
			</div>
		</form>
		<hr>
	</div>

    {{-- 新規登録画面へ --}}
    <a class="btn btn-primary mb-2" href="{{ route('admin.reserve.create') }}" role="button">新規登録</a>

    <div class="card">
        <div class="card-body">
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>予約番号</th>
                        <th>ユーザー名</th>
                        <th>スケジュールID</th>
                        <th style="width: 70px"></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($reserves as $reserve)
                        <tr>
                            <td>{{ $reserve->id }}</td>
                            <td>{{ $reserve->user->name }}</td>
                            <td>{{ $reserve->schedule_id }}</td>
                            <td>
                                <a class="btn btn-primary btn-sm mb-2" href="{{ route('admin.reserve.edit', $reserve->id) }}"
                                    role="button">編集</a>
                                <form action="{{ route('admin.reserve.destroy', $reserve->id) }}" method="post">
                                    @csrf
                                    @method('DELETE')
                                    {{-- 簡易的に確認メッセージを表示 --}}
                                    <button type="submit" class="btn btn-danger btn-sm"
                                        onclick="return confirm('削除してもよろしいですか?');">
                                        削除
                                    </button>
                                </form>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
@stop

resources/views/admin/reserve/create.blade.php
@extends('adminlte::page')

@section('title', '予約登録')

@section('content_header')
    <h1>予約登録</h1>
@stop

@section('content')
    @if ($errors->any())
        <div class="alert alert-warning alert-dismissible">
            {{-- エラーの表示 --}}
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    {{-- 編集画面 --}}
    <div class="card">
        <form action="{{ route('admin.reserve.store') }}" method="post">
            @csrf
            <div class="card-body">
                <div class="form-group">
                    <label for="name">スケジュールID</label>
                    <select class="form-control" id="schedule_id" name="schedule_id">
                        @foreach ($schedules as $schedule)
                            <option value="{{ $schedule->id }}" @selected(old('schedule_id') == $schedule->id)>
                                {{ $schedule->from_date }}{{ $schedule->due_date }}
                            </option>
                        @endforeach
                    </select>
                </div>
                <div class="form-group">
                    <label for="name">ユーザーID</label>
                    <select class="form-control" id="user_id" name="user_id">
                        @foreach ($users as $user)
                            <option value="{{ $user->id }}" @selected(old('user_id') == $user->id)>
                                {{ $user->name }}
                            </option>
                        @endforeach
                    </select>
                </div>
            </div>
            <div class="card-footer">
                <div class="row">
                    <a class="btn btn-default" href="{{ route('admin.reserve.index') }}" role="button">戻る</a>
                    <div class="ml-auto">
                        <button type="submit" class="btn btn-primary">登録</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
@stop

resources/views/admin/reserve/edit.blade.php
@extends('adminlte::page')

@section('title', 'スケジュール編集')

@section('content_header')
    <h1>スケジュール編集</h1>
@stop

@section('content')
    @if ($errors->any())
        <div class="alert alert-warning alert-dismissible">
            {{-- エラーの表示 --}}
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    {{-- 編集画面 --}}
    <div class="card">
        <form action="{{ route('admin.reserve.update', $reserve->id) }}" method="post">
            @csrf @method('PUT')
            <div class="card-body">
                <div class="form-group">
                    <label for="name">スケジュールID</label>
                    <input type="text" class="form-control" id="schedule_id" name="schedule_id"
                        value="{{ old('schedule_id', $reserve->schedule_id) }}" placeholder="スケジュールID" />
                </div>
                <div class="form-group">
                    <label for="name">ユーザーID</label>
                    <input type="text" class="form-control" id="user_id" name="user_id"
                        value="{{ old('user_id', $reserve->user_id) }}" placeholder="ユーザーID" />
                </div>
            </div>
            <div class="card-footer">
                <div class="row">
                    <a class="btn btn-default" href="{{ route('admin.reserve.index') }}" role="button">戻る</a>
                    <div class="ml-auto">
                        <button type="submit" class="btn btn-primary">編集</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
@stop

⑧コントローラーに表示処理を実装する。

※下記、参照し上書きする。

app/Http/Controllers/Admin/ReserveController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\StorereserveRequest;
use App\Http\Requests\UpdatereserveRequest;
use App\Models\Reserve;
use App\Models\Schedule;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class ReserveController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\View\View
     */
    public function index(Request $request)
    {

        // 検索処理の準備
        $query = Reserve::query();
        $search_keywords = array();

        // リクエストの検索キーに値がセットされていたらSQL抽出条件として追加する
        if ($request->filled('id')) {
            $query->where('id', '=', $request->input('id'));
            $search_keywords['id'] = $request->input('id');
        }

        if ($request->filled('user_name')) {
            // リレーション先のテーブルから検索する
            $query->whereHas('user', function ($query) use ($request) {
                $query->where('name', 'LIKE', "%{$request->input('user_name')}%");
            });
            $search_keywords['user_name'] = $request->input('user_name');
        }

        if ($request->filled('schedule_id')) {
            $query->where('schedule_id', '=', $request->input('schedule_id'));
            $search_keywords['schedule_id'] = $request->input('schedule_id');
        }

        // 設定した抽出条件+ソートをしてデータを取得する
        $reserves = $query->orderBy('id', 'asc')->get();

        // 動作確認用デバッグログ
        Log::debug($query->toSql());

        return view('admin.reserve.index', compact('reserves', 'search_keywords'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\View\View
     */
    public function create()
    {
        $schedules = Schedule::orderBy('from_date')->get();
        $users = User::orderBy('id')->get();
        return view('admin.reserve.create', compact('schedules', 'users'));
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \App\Http\Requests\StorereserveRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(StorereserveRequest $request)
    {
        $reserve = new Reserve();
        $reserve->fill($request->all())->save();
        return redirect()->route('admin.reserve.index')->with('message', 'add reserve!');
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Reserve  $reserve
     * @return \Illuminate\View\View
     */
    public function show(reserve $reserve)
    {
        return view('admin.reserve.show', compact('reserve'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Models\Reserve  $reserve
     * @return \Illuminate\View\View
     */
    public function edit(reserve $reserve)
    {
        return view('admin.reserve.edit', compact('reserve'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \App\Http\Requests\UpdatereserveRequest  $request
     * @param  \App\Models\Reserve  $reserve
     * @return \Illuminate\Http\RedirectResponse
     */
    public function update(UpdatereserveRequest $request, reserve $reserve)
    {
        $reserve->update($request->all());
        return redirect()->route('admin.reserve.index')->with('message', 'update reserve!');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Reserve  $reserve
     * @return \Illuminate\Http\RedirectResponse
     */
    public function destroy(reserve $reserve)
    {
        $reserve->delete();
        return redirect()->route('admin.reserve.index')->with('message', 'delete reserve!');
    }
}

⑨新規作成と編集時のリクエスト、バリデーションは以下のファイルで定義する。

※下記、L17~L18、L29~L31を参照

app/Http/Requests/StoreReserveRequest.php
    <?php
    
    namespace App\Http\Requests;
    
    use Illuminate\Foundation\Http\FormRequest;
    use Illuminate\Validation\Rule;
    
    class StorereserveRequest extends FormRequest
    {
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
-           return false;
+           return true;
    }
    
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        return [
+            'schedule_id' => ['required', 'exists:schedules,id' ],
+            'user_id'  => ['required', 'exists:users,id',
+                            Rule::unique('reserves', 'user_id')->where('schedule_id', $this->input('schedule_id')) ],
        ];
    }
  }

※下記、L16~L17、L28~L29を参照

app/Http/Requests/UpdateReserveRequest.php
  <?php
  
  namespace App\Http\Requests;
  
  use Illuminate\Foundation\Http\FormRequest;
  
  class UpdatereserveRequest extends FormRequest
  {
      /**
       * Determine if the user is authorized to make this request.
       *
       * @return bool
       */
      public function authorize()
      {
-         return false;
+         return true;
      }
  
      /**
       * Get the validation rules that apply to the request.
       *
       * @return array<string, mixed>
       */
      public function rules()
      {
          return [
+             'schedule_id' => ['required', 'exists:schedules,id' ],
+             'user_id'  => ['required', 'exists:users,id' ],
          ];
      }
  }

動作確認

・管理画面にて/admin/reserve にアクセスし、一覧画面が表示されるかを確認する。

・新規登録ボタンからスケジュールの予約が登録できるかを確認する。

・編集ボタンでスケジュールの予約が更新ができるかを確認する。

---