Laravel9　Fullcalendarを実装してみた話（実装編）

# 概要

Laravelでのカレンダー機能実装の記録。（実装編）
Fullcalendarの実装の理解については（理解編）にて。

https://qiita.com/tk49/items/5947a0111648e7030983

# 仕様
まずはカレンダーに関して、仕様を記しておく。

1. 縦型、横型を指定して表示できる
2. 予定が人毎に分かれて表示される
3. 人は事業所ごとにグループ分けできる

縦横の表示をユーザが選択でき、予定は人毎に分かれて表示されるようにしたい。
現状のgoogleカレンダーなどは人毎に分割して表示することができないため、
そちらの解決したい目的。

# 完成イメージ
次に、完成イメージを載せる。
画面右上にボタンが大量に並んでいるが、あくまでサンプルとして作成し、運用に合わせて必要なもののみにするイメージ。
**※人(リソース)毎に表示部分が分けるのが、今回の仕様の肝である。**

1.縦型のイメージ：人単位で縦表示する
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284198/f249c3d8-0b26-0c0b-dab3-65977de5cdc0.png">

2.横型のイメージ：人単位で横表示
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284198/dd0ce47c-9f4e-e1ae-c840-ea3cd349d365.png">

3.グループのイメージ：人単位で横表示かつ、人をまとめられる
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284198/2688ed4d-f88d-c473-c73c-4740f298536d.png">

# 実装
ES6 + vite で実装を行う。こちらの説明は[別記事（理解編）](https://qiita.com/tk49/items/5947a0111648e7030983)参照。

実装の手順としては目次を参照してほしい。

## 実装イメージ

### 全体フロー
全体の流れを絵に書いてみるとこんな感じだろうか。
（ほぼLaravelの話で今回に限った話でないが、イメージとして）
```mermaid
sequenceDiagram
  actor U as User
  participant A as カレンダーページ<br>（view + js）
  participant B as web.php
  participant C as CalendarController.php
  participant D as DB
  U->>+A: Request
  A->>B: POST Request
  B->>C: ルーティング
  C->>D: SQL
  D-->>C: データ
  C-->>B: データ
  B-->>A: データ
  A-->>-U: HTML
```

### DB設計
DBに関しては今回使用するFullCalendarの公式から構造を決めておく。
[(参考) 公式 - カレンダーの構造](https://fullcalendar.io/docs/Event-getResources)

公式の情報を元にサンプルとしたデータ構造がこちら。
```js
var calendar = new Calendar(calendarEl, {
  resources: [
    { id: "a", building: '第1グループ', title: "田中", eventColor: "blue" },
  ],
  events: [
    {
        resourceId: "a",
        title: "打ち合わせ",
        start: "2023-05-19T09:00:00",
        end: "2023-05-19T10:00:00",
    },
  ]
});
```
resourceのテーブルとeventのテーブルに対して、
resources.id = events.resourceId で紐づいている形となるため、以下のイメージ。

```mermaid
erDiagram
    EVENT||--|{ RESOURCE : "1つのイベントに対して0以上のリソースが紐づく"
    EVENT {
        int resourceId
        string title
        date start
        date end
    }
    RESOURCE {
        int id
        string building
        string title
        string eventColor
    }
```

こちらを前提として、実装へ。

## 1. ライブラリインストール
npmを利用してインストールを行う。
今回は横型（Timeline View）も縦型（Vertical Resource View）も使う前提でまとめてインストールする。
> npm install --save \
  @fullcalendar/core \
  @fullcalendar/resource \
  @fullcalendar/resource-timeline \
  @fullcalendar/resource-timegrid

[(参考) 公式 - 横型（Timeline View）](https://fullcalendar.io/docs/timeline-view)
[(参考) 公式 - 縦型（Vertical Resource View）](https://fullcalendar.io/docs/vertical-resource-view)

## 2. viewの作成（カレンダー表示ページ）
[(参考) 公式 - viewサンプル](https://github.com/fullcalendar/fullcalendar-examples/blob/main/webpack-scheduler/src/index.html)を元にcalendar.blade.phpを作成。

いったんid = calendarのdivだけ準備しておく。
``` html:resources/views/calendar.blade.php
<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
  </head>
  <body>
    <div id='calendar'></div>
  </body>
</html>
```

## 3. jsの作成
### 3-1. jsの作成
[(参考) 公式 - jsサンプル](https://github.com/fullcalendar/fullcalendar-examples/blob/main/webpack-scheduler/src/index.js)を元に、今回の要件ようにカスタマイズしたものがこちら。

<details><summary>resources/js/calendar.js（全文）</summary><div>

```resources/js/calendar.js
import { Calendar } from "@fullcalendar/core";
import interactionPlugin from "@fullcalendar/interaction";
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
import resourceTimeGridPlugin from "@fullcalendar/resource-timegrid";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import listPlugin from "@fullcalendar/list";
import axios from 'axios';

var calendarEl = document.getElementById("calendar");

let calendar = new Calendar(calendarEl, {
    plugins: [interactionPlugin, resourceTimelinePlugin, resourceTimeGridPlugin  , dayGridPlugin, timeGridPlugin, listPlugin ],
    schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
    initialView: 'resourceTimeGridOneDay',
    datesAboveResources: true,  // true：日付サマリ、false：リソースサマリ
    resourceAreaHeaderContent: "予定一覧",
    locale: "ja",
    editable: true,
    headerToolbar: {
        left: 'prev,next,today,title',
        right: 'resourceTimeGridOneDay,resourceTimeGridTwoDay,resourceTimeGridThreeDay,resourceTimeGridFourDay,resourceTimeGridFiveDay,resourceTimelineDay,resourceTimelineWeek,resourceTimelineMonth,resourceTimelineDayGroup'
    },

    views: {
        resourceTimeGridOneDay: {
            type: 'resourceTimeGrid',
            duration: { days: 1 },
            buttonText: '1日'
        },
        resourceTimeGridTwoDay: {
            type: 'resourceTimeGrid',
            duration: { days: 2 },
            buttonText: '2日'
        },
        resourceTimeGridThreeDay: {
            type: 'resourceTimeGrid',
            duration: { days: 3 },
            buttonText: '3日'
        },
        resourceTimeGridFourDay: {
            type: 'resourceTimeGrid',
            duration: { days: 4 },
            buttonText: '4日'
        },
        resourceTimeGridFiveDay: {
            type: 'resourceTimeGrid',
            duration: { days: 5 },
            buttonText: '5日'
        },

        resourceTimelineDay:{
            type: 'resourceTimelineDay',
            buttonText: '日'
        },
        resourceTimelineWeek:{
            type: 'resourceTimelineWeek',
            buttonText: '週'
        },
        resourceTimelineMonth:{
            type: 'resourceTimelineMonth',
            buttonText: '月'
        },
        resourceTimelineDayGroup:{
            type: 'resourceTimelineDay',
            resourceGroupField: 'building',
            buttonText: 'グループ'
        },
    },
    resources: function (info, successCallback, failureCallback) {
        axios
            .post("/calendar/resourceGet", {
            })
            .then((response) => {
                successCallback(response.data);
            })
            .catch(() => {
            });
    },
    events: function (info, successCallback, failureCallback) {
        axios
            .post("/calendar/eventGet", {
            })
            .then((response) => {
                calendar.removeAllEvents();
                successCallback(response.data);
            })
            .catch(() => {
            });
    },
});
calendar.render();
```
</div></details>

カスタマイズポイントとしては下記。

1.[(参考) 公式 - datesAboveResourcesオプション](https://fullcalendar.io/docs/datesAboveResources)
日付優先でサマリするか、人（resource）毎優先でサマリするか設定する。
```resources/js/calendar.js
datesAboveResources: true,  // true：日付サマリ、false：リソースサマリ
```

2.[(参考) 公式 - 表示日数を設定](https://fullcalendar.io/docs/custom-view-with-settings)
```resources/js/calendar.js
    views: {
        resourceTimeGridOneDay: {
            type: 'resourceTimeGrid',
            duration: { days: 1 },
            buttonText: '1日'
        },
        ・・・
```

3.[(参考) 公式 - グルーピングの条件設定](https://fullcalendar.io/docs/resourceGroupField)
今回は部署ごとに分けるという想定で。
```resources/js/calendar.js
        resourceTimelineDayGroup:{
            type: 'resourceTimelineDay',
            resourceGroupField: 'building',
            buttonText: 'グループ'
        },
```

4.データの取得方法
DB連携したデータの取得方法について、こちら参考にさせていただきました。
非常にわかりやすくまとめていただいていますので、確認オススメします。

https://chigusa-web.com/blog/laravel-fullcalendar/

ソースのポイントとしてはaxiosにてpost通信しており、
resourceGetとeventGetというルーティングでデータを取得するようにしている。

```resources/js/calendar.js
    resources: function (info, successCallback, failureCallback) {
        axios
            .post("resourceGet", {
        ・・・
    },
    events: function (info, successCallback, failureCallback) {
        axios
            .post("eventGet", {
        ・・・
    },
```
### 3-2. 作成したjsのimport設定
作成したjsをapp.jsにてimportする。

```resource/js/app.js
import './bootstrap';
import './calendar'; // 追加
```
### 3-3. viteにて読み込ませる
表示ページのheadタグの中に、viteを利用してjsを読み込ませる。

``` html:resources/views/calendar.blade.php
  <head>
    <meta charset='utf-8' />
  　@vite('resources/js/app.js')
  </head>
```

## 4. route設定（表示ページへのパス設定）
それぞれの画面・データ取得処理へのルーティングを設定する。
```web.php
    Route::group(['prefix'=>'calendar'], function () {
        Route::get('index', [CalendarController::class, 'index']);
        Route::post('resourceGet', [CalendarController::class, 'resourceGet']);
        Route::post('eventGet', [CalendarController::class, 'eventGet']);
    });
```

なおルーティングはgurupでまとめられるとのこと。（Laravel9.xから？）
[(参考) 公式 - ルーティング](https://readouble.com/laravel/9.x/ja/routing.html?header=%25E3%2582%25B3%25E3%2583%25B3%25E3%2583%2588%25E3%2583%25AD%25E3%2583%25BC%25E3%2583%25A9)

こうしておくと、calendar/index・calendar/resourceGet・calendar/eventGet とすることが可能。

各説明としては下記。
index：カレンダー表示ページへ遷移するルーティング
resourceGet：リソース（今回は人単位を想定）データを取得するPOST通信のルーティング
eventGet：イベントデータを取得するPOST通信のルーティング

## 5. controllerの設定
### 5-1. コントローラー等の作成
続いてコントローラーを作成する。
私はいつも「aオプション」で一式作っているので、コマンドをのせておくがお好きな方法で作ってほしい。
[(参考) 公式 - モデルの作成](https://readouble.com/laravel/9.x/ja/eloquent.html?header=%25E3%2583%25A2%25E3%2583%2587%25E3%2583%25AB%25E3%2582%25AF%25E3%2583%25A9%25E3%2582%25B9%25E3%2581%25AE%25E7%2594%259F%25E6%2588%2590)
``` bat:cmd
rem カレンダーデータ（event）
php artisan make:model calendar -a
rem 従業員データ（resource）
php artisan make:model employee -a
```

### 5-2. コントローラーの内容
コントローラーの内容としては下記。
index：表示画面の設定
resourceGet：リソース取得のSQL
eventGet：イベント取得のSQL

```app/Http/Controllers/CalendarController.php
<?php
namespace App\Http\Controllers;

use App\Models\calendar;
use App\Models\employee;

class EventController extends Controller
{
    public function index()
    {
        return view('calendar');
    }
    /**
     * リソースを取得
     */
    public function resourceGet()
    {
        return employee::query()
            ->select('id' , 'building' , 'title' , 'eventColor')
            ->get();
    }
    /**
     * イベントを取得
     */
    public function eventGet()
    {
        return calendar::query()
            ->select('resourceId' , 'title' , 'start_time as start' , 'end_time as end')
            ->get();
    }
}
```

## 6. DBの設定
### 6-1. tableの設定

リソース（employee）テーブルの設定。
```database/migrations/yyyy_mm_dd_hhmiss_create_employees_table.php
    ・・・
    public function up()
    {
        Schema::create('employees', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('building')->nullable();
            $table->string('title')->nullable();
			$table->string('eventColor')->nullable();
        });
    }
    ・・・
```

イベント（calendar）テーブルの設定。
```database/migrations/yyyy_mm_dd_hhmiss_create_calendars_table.php
    ・・・
    public function up()
    {
        Schema::create('employees', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
			$table->integer('resourceId')->nullable();
            $table->string('building')->nullable();
            $table->timestamp('start_time')->nullable();
			$table->timestamp('end_time')->nullable();
        });
    }
    ・・・
```

### 6-2. seederの設定

リソース（employee）テーブルのシーダーの設定。
```database/seeders/EmployeeSeeder.php
    ・・・
    public function run()
    {
        \DB::table('employees')->truncate();

        $obj = new employee;
        $obj->building = '第1グループ';
        $obj->title = '田中';
        $obj->eventColor = 'blue';
        $obj->save();
    }
    ・・・
```

イベント（calendar）テーブルのシーダーの設定。
```database/seeders/CalendarSeeder.php
    ・・・
    public function run()
    {
        \DB::table('calendars')->truncate();

        $obj = new calendar;
        $obj->title = '打ち合わせ';
        $obj->start_time = '2023/05/19 09:00:00';
        $obj->end_time = '2023/05/19 10:00:00';
        $obj->save();
    }
    ・・・
```

データベースシーダーの設定。
```database/seeders/DatabaseSeeder.php
・・・
    public function run()
    {
        ・・・
        $this->call(EmployeeSeeder::class);
        $this->call(CalendarSeeder::class);
        ・・・
    }
・・・
```

### 6-3. migrationの実行
コマンドにてDB最新化＆データ登録。
``cmd
php artisan migrate:refresh --seed
``

## 7. 動作確認
### 7-1. サーバ起動
コマンドにてサーバを起動する。
（※Laravel9.xからはsailというのが使える模様）
``cmd
php -S localhost:8000 -t public
``

### 7-2. vite起動
またvite利用のためviteを起動する。
[公式 - Viteの実行](https://readouble.com/laravel/9.x/ja/vite.html?header=Vite%25E3%2581%25AE%25E5%25AE%259F%25E8%25A1%258C)
``cmd
npm run dev
``

### 7-3. ローカル環境動作確認
http://localhost:8000/calendar/index に接続し、動作確認。

## 8. リリース
問題なければリリース作業となる。

リリース前としてビルド作業をしておく。
``cmd
npm run build
``

ビルド作業によって、public\build\assets直下のjsが更新される模様。
こちらによってpublicの下のjsにファイルを配置する必要がなくなっている。

# 課題
1. jsのpost通信がroute記述が使用できない

現在の課題として、js内のpost通信する際のパスが設定できない。
``` calendar.js
    ・・・
    resources: function (info, successCallback, failureCallback) {
        axios
            .post("/calendar/resourceGet", {
            })
            .then((response) => {
                successCallback(response.data);
            })
            .catch(() => {
            });
    },
    ・・・
```

このようにハードコーティングになってしまっている状態となっている。
本番環境/検証環境/開発環境等リリース先のベースとなるURLが異なる場合、うまくいかない原因となっている。
環境変数から値を取得したいのだが、ローカル環境だとうまくいくのだが、
私の場合、サクラのレンタルサーバにアップロードするとうまく取得できていない状態。

# まとめ・所感

今回、Fullcalendarを利用したカスタマイズについて検討してみた。

色々と記事を見漁ったのだが、最終的には公式を参照するのが最善と思われる。
（やりたいことがあるのであれば。）

[公式HPのトップ](https://fullcalendar.io/docs#main) にイメージがついているので、そこから選択できる。
その中にdemoがついているので、そちらを見つつ良さそうであれば、組み込むといった流れがよいと思う。

DB連携については、結局のところデータを画面に表示する仕組みは同じであると感じた。
ただし、課題に記載したがjsのルーティングについては改良したい。
（何か情報あればいただけると嬉しいです！）