1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravel9 Fullcalendarを実装してみた話(実装編)

Last updated at Posted at 2023-05-28

概要

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

仕様

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

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

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

完成イメージ

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

1.縦型のイメージ:人単位で縦表示する

2.横型のイメージ:人単位で横表示

3.グループのイメージ:人単位で横表示かつ、人をまとめられる

実装

ES6 + vite で実装を行う。こちらの説明は別記事(理解編)参照。

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

実装イメージ

全体フロー

全体の流れを絵に書いてみるとこんな感じだろうか。
(ほぼLaravelの話で今回に限った話でないが、イメージとして)

DB設計

DBに関しては今回使用するFullCalendarの公式から構造を決めておく。
(参考) 公式 - カレンダーの構造

公式の情報を元にサンプルとしたデータ構造がこちら。

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 で紐づいている形となるため、以下のイメージ。

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

1. ライブラリインストール

npmを利用してインストールを行う。
今回は横型(Timeline View)も縦型(Vertical Resource View)も使う前提でまとめてインストールする。

npm install --save
@fullcalendar/core
@fullcalendar/resource
@fullcalendar/resource-timeline
@fullcalendar/resource-timegrid

(参考) 公式 - 横型(Timeline View)
(参考) 公式 - 縦型(Vertical Resource View)

2. viewの作成(カレンダー表示ページ)

(参考) 公式 - viewサンプルを元にcalendar.blade.phpを作成。

いったんid = calendarのdivだけ準備しておく。

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サンプルを元に、今回の要件ようにカスタマイズしたものがこちら。

resources/js/calendar.js(全文)
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();

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

1.(参考) 公式 - datesAboveResourcesオプション
日付優先でサマリするか、人(resource)毎優先でサマリするか設定する。

resources/js/calendar.js
datesAboveResources: true,  // true:日付サマリ、false:リソースサマリ

2.(参考) 公式 - 表示日数を設定

resources/js/calendar.js
    views: {
        resourceTimeGridOneDay: {
            type: 'resourceTimeGrid',
            duration: { days: 1 },
            buttonText: '1日'
        },
        ・・・

3.(参考) 公式 - グルーピングの条件設定
今回は部署ごとに分けるという想定で。

resources/js/calendar.js
        resourceTimelineDayGroup:{
            type: 'resourceTimelineDay',
            resourceGroupField: 'building',
            buttonText: 'グループ'
        },

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

ソースのポイントとしては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を読み込ませる。

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から?)
(参考) 公式 - ルーティング

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

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

5. controllerの設定

5-1. コントローラー等の作成

続いてコントローラーを作成する。
私はいつも「aオプション」で一式作っているので、コマンドをのせておくがお好きな方法で作ってほしい。
(参考) 公式 - モデルの作成

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の実行
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のトップ にイメージがついているので、そこから選択できる。
その中にdemoがついているので、そちらを見つつ良さそうであれば、組み込むといった流れがよいと思う。

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?