E070 管理画面 スケジュール機能の導入

今回は、予約システムの主データとなるスケジュールの管理機能の作成方法を紹介します。

スケジュール機能作成

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

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

※下記のように表示がされていれば成功

INFO  Model [app/Models/Schedule.php] created successfully.  
INFO  Migration [database/migrations/yyyy_mm_dd_xxxxxx_create_schedules_table.php] created successfully.  
INFO  Request [app/Http/Requests/StoreScheduleRequest.php] created successfully.  
INFO  Request [app/Http/Requests/UpdateScheduleRequest.php] created successfully.  
INFO  Controller [app/Http/Controllers/ScheduleController.php] created successfully.  

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

app/Http/Controllers/ScheduleController.php を app/Http/Controllers/Admin/に移動し、ファイル内のnamespaceを修正します。また、継承クラスのパス解決のために、use use App\Http\Controllers\Controller; の追加も行います。

※下記、L3~L4を参照

app/Http/Controllers/Admin/Auth/×××××××php
- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

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

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

※下記、L16~L20を参照

database/migrations/yyyy_mm_dd_xxxxxx_create_schedules_table.php
  public function up()
  {
      Schema::create('schedules', function (Blueprint $table) {
          $table->id();
+         $table->string('title', 255);
+         $table->datetime('from_date');
+         $table->datetime('due_date');
+         $table->integer('reservation_limit')->nullable();
+         $table->string('staff_name')->nullable();
          $table->timestamps();
      });
  }

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

sail artisan migrate

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

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

※下記、L44を参照

routes/admin.php
  Route::middleware('auth:admin')->group(function () {
      // 中略
      Route::get('/dashboard', function () {
                    return view('dashboard');
      })->middleware(['auth', 'verified'])->name('dashboard');
      
+     Route::resource('schedule', ScheduleController::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\ScheduleController 

管理者ユーザーの認証がされている場合にアクセス可とし、/admin/schedule にアクセスした場合にScheduleContollerの各処理を呼び出すように設定します。Route::resourceは自動でテーブルに帯する各処理のルーティングを生成します。

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

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

resources/views/admin/schedule/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.schedule.index') }}" method="get">
			<div class="form-group form-inline input-group-sm">
			    <label for="title" class="col-md-2 control-label">タイトル</label>
			    <input type="text" class="form-control col-md-5" id="title" name="title" placeholder="タイトル" value="{{ @$search_keywords['title'] }}">
			</div>

			<div class="form-group form-inline input-group-sm">
			    <label for="from_date" class="col-md-2 control-label">実施日</label>
			    <input type="date" class="form-control col-md-3" id="from_date" name="from_date" placeholder="From" value="{{ @$search_keywords['from_date'] }}">
			    <label class="col-md-1 control-label"></label>
			    <input type="date" class="form-control col-md-3" id="due_date" name="due_date" placeholder="To" value="{{ @$search_keywords['due_date'] }}">
			    <div class="col-md-3"></div>
			</div>
            <div class="form-group form-inline input-group-sm">
			    <label for="reservation_limit_from" class="col-md-2 control-label">予約可能数</label>
			    <input type="number" class="form-control col-md-2" id="reservation_limit_from" name="reservation_limit_from" placeholder="下限" value="{{ @$search_keywords['reservation_limit_from'] }}">
				<label class="col-md-1 control-label"></label>
                <input type="number" class="form-control col-md-2" id="reservation_limit_to" name="reservation_limit_to" placeholder="上限" value="{{ @$search_keywords['reservation_limit_to'] }}">
			</div>
            <div class="form-group form-inline input-group-sm">
			    <label for="staff_name" class="col-md-2 control-label">担当者</label>
			    <input type="text" class="form-control col-md-2" id="staff_name" name="staff_name" placeholder="スタッフ名" value="{{ @$search_keywords['staff_name'] }}">
			</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.schedule.create') }}" role="button">新規登録</a>

    <div class="card">
        <div class="card-body">
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>タイトル</th>
                        <th>開始日時</th>
                        <th>終了日時</th>
                        <th>予約可能数</th>
                        <th>スタッフ</th>
                        <th style="width: 70px"></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($schedules as $schedule)
                        <tr>
                            <td>{{ $schedule->id }}</td>
                            <td>{{ $schedule->title }}</td>
                            <td>{{ $schedule->from_date }}</td>
                            <td>{{ $schedule->due_date }}</td>
                            <td>{{ $schedule->reservation_limit }}</td>
                            <td>{{ $schedule->staff_name }}</td>
                            <td>
                                <a class="btn btn-primary btn-sm mb-2" href="{{ route('admin.schedule.edit', $schedule->id) }}"
                                    role="button">編集</a>
                                <form action="{{ route('admin.schedule.destroy', $schedule->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/schedule/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.schedule.store') }}" method="post">
            @csrf
            <div class="card-body">
                <div class="form-group">
                    <label for="name">タイトル</label>
                    <input type="text" class="form-control" id="title" name="title"
                        value="{{ old('title') }}" placeholder="スケジュールタイトル" />
                </div>
                <div class="form-group">
                    <label for="name">開始日時</label>
                    <input type="datetime-local" class="form-control" id="from_date" name="from_date"
                        value="{{ old('from_date') }}" placeholder="2023/03/10 9:00" />
                </div>
                <div class="form-group">
                    <label for="name">終了日時</label>
                    <input type="datetime-local" class="form-control" id="due_date" name="due_date"
                        value="{{ old('due_date') }}" placeholder="2023/03/10 10:00" />
                </div>
                <div class="form-group">
                    <label for="name">予約可能数</label>
                    <input type="number" class="form-control" id="reservation_limit" name="reservation_limit"
                        value="{{ old('reservation_limit') }}" placeholder="予約可能数(1~)" />
                </div>
                <div class="form-group">
                    <label for="price">担当者名</label>
                    <input type="text" class="form-control" id="staff_name" name="staff_name"
                        value="{{ old('staff_name') }}" placeholder="スタッフ名" />
                </div>
            </div>
            <div class="card-footer">
                <div class="row">
                    <a class="btn btn-default" href="{{ route('admin.schedule.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/schedule/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.schedule.update', $schedule->id) }}" method="post">
            @csrf @method('PUT')
            <div class="card-body">
                <div class="form-group">
                    <label for="name">タイトル</label>
                    <input type="text" class="form-control" id="title" name="title"
                        value="{{ old('title', $schedule->title) }}" placeholder="スケジュールタイトル" />
                </div>
                <div class="form-group">
                    <label for="name">開始日時</label>
                    <input type="datetime-local" class="form-control" id="from_date" name="from_date"
                        value="{{ old('from_date', $schedule->from_date) }}" placeholder="2023/03/10 9:00" />
                </div>
                <div class="form-group">
                    <label for="name">終了日時</label>
                    <input type="datetime-local" class="form-control" id="due_date" name="due_date"
                        value="{{ old('due_date', $schedule->due_date) }}" placeholder="2023/03/10 10:00" />
                </div>
                <div class="form-group">
                    <label for="name">予約可能数</label>
                    <input type="number" class="form-control" id="reservation_limit" name="reservation_limit"
                        value="{{ old('reservation_limit', $schedule->reservation_limit) }}" placeholder="予約可能数(1~)" />
                </div>
                <div class="form-group">
                    <label for="price">担当者名</label>
                    <input type="text" class="form-control" id="staff_name" name="staff_name"
                        value="{{ old('staff_name', $schedule->staff_name) }}" placeholder="スタッフ名" />
                </div>
            </div>
            <div class="card-footer">
                <div class="row">
                    <a class="btn btn-default" href="{{ route('admin.schedule.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/ScheduleController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreScheduleRequest;
use App\Http\Requests\UpdateScheduleRequest;
use App\Models\Schedule;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

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

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

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

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

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

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

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

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

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

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

        return view('admin.schedule.index', compact('schedules', 'search_keywords'));
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\View\View
     */
    public function create()
    {
        return view('admin.schedule.create');
    }

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

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

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

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

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

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

※下記、L27~L31を参照

app/Http/Requests/StoreScheduleRequest.php
  public function rules()
  {
      return [
+          'title' => ['required', 'max:255'],
+          'from_date' => ['required', 'date', 'after:2000-01-01' ],
+          'due_date'  => ['required', 'date', 'after:from_date' ],
+          'reservation_limit' => ['numeric', 'max:9999'],
+          'staff_name' => ['string', 'max:255'],
      ];
  }

※下記、L27~L31を参照

app/Http/Requests/UpdateScheduleRequest.php
  public function rules()
  {
      return [
+          'title' => ['required', 'max:255'],
+          'from_date' => ['required', 'date', 'after:2000-01-01' ],
+          'due_date'  => ['required', 'date', 'after:from_date' ],
+          'reservation_limit' => ['numeric', 'max:9999'],
+          'staff_name' => ['string', 'max:255'],
      ];
  }

動作確認

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

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

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

---