STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

STORES 予約 におけるFullCalendarの活用事例

この記事は STORES アドベントカレンダーの12月20日の記事となります。

はじめに

こんにちは、 STORES 予約 でエンジニアをしているyuta07です。

この度 STORES 予約 では、12月に予約カレンダーを正式リリースしました。

突然ですが、Web上でカレンダーの開発をするとなった場合、ほとんどの場合でまずライブラリの使用が選択肢に上がるのではないでしょうか。

私個人としてはライブラリに頼ることを少なくしたい思いはありますが、実際はライブラリを使用して工数の削減なりメリットを享受することが多いです。 (もちろん仕様を満たすことができなければ自作という選択肢になりますし、ライブラリの機能に合わせて仕様を絞ることもありえます)

実際に STORES 予約 で実装されているカレンダーでもFullCalendarが利用されており、ライブラリの恩恵を多分に受けています。

そこで、今回はFullCalendarについて、STORES 予約 のカレンダーでどのように活用しているか紹介します。

背景

STORES 予約 では、古いRailsの画面をNext.jsのあたらしいUIにリニューアルする取り組みを進めており、2022年9月に基本的な見やすさを改善したカレンダーをβ版として提供していました。

β版の提供ではカレンダーの表示領域が広くなり、予約状況の確認がしやすくなったなど、いい反応を得ることができていました。その一方でカレンダーページへの動線がホーム画面からしかできなかったり、通常利用に至らないオーナー様がまだまだいる状況でした。

そのため、オーナー様により通常的に利用してもらえるようにグローバルナビに動線を表示して、さらに機能を追加した上で正式版としてリリースをする運びとなりました。

β版で提供されていた機能

正式リリース版のカレンダー紹介の前にβ版にて実装されていたカレンダーの機能を一部抜粋すると以下の機能があります。

  • デザインの刷新による見やすさの改善。
    • カラーデザインの変更や表示項目の改善することで予約枠の見やすさの向上。
  • カレンダーの表示設定の詳細カスタマイズを可能にする。
    • カレンダーに表示する時間帯や週の始まりの曜日などの設定。
  • カレンダー上で見たい予約や日程の条件の絞り込み。
    • 特定のスタッフの担当する予約・日程のみを表示できるようにしたり、ブロック時間の表示有無の選択。

β版カレンダー

β版だけでも従来の古いカレンダーよりもかなりの機能改善がされていますが、今回の正式リリースでさらなる機能追加をしました。

正式リリースで追加したメイン機能

正式リリースで追加された主な機能追加は主に以下になります。

  • サイドバーを右側に移して、表示/非表示を切り替えられるようにして画面全体でカレンダーを表示できるようにする。
  • 表示形式に「スタッフ(横)」を追加する。
  • 見やすさ向上のためのデザインの調整・仕様変更。

今回はその中でもメイン機能の1つであるスタッフの横型ビューにフォーカスして、どのようにアプローチして実装を進めたのかを紹介します。

Timeline View

※ 実装の紹介の前に STORES 予約 で使用している FullCalendar のバージョンについてですが、5系の最新を使用していることをご留意ください(2023/12/18現在)

横型ビューはFullCalendarのDocsではTimelineに分類されており、基本的にはDocsを参考にしつつ STORES 予約 の仕様に合わせて実装を進めていきました。*1

fullcalendar.io

Timeline Viewを新しく追加するにあたり、既存のGrid ViewやMonth Viewに影響を与えないようにする必要があります。 STORES 予約 ではカレンダー切り替えをSelectBoxで行っており、ViewTypeによってコンテンツの表示切り替えをしています。

const calendarApiRef = useRef<CalendarApi | undefined>(undefined)

const onCalendarRequireViewTypeChanging = useCallback((viewType: string) => {
  if (!calendarApiRef.current) return

  calendarApiRef.current?.changeView(viewType)
}, [])

上述のViewTypeの切り替えをトリガーとしてカレンダーの表示を切り替えています。そのため、新しくビューを追加する場合でもSelectBoxのoptionに追加するだけでViewTypeの追加は楽にできました。

ViewTypeの切り替えを可能にした後はカレンダーコンテンツの表示切り替えを可能にする必要があります。

FullCalendarではResourceやEventといったデータを通して、カレンダー内のイベントやリソース(STORES 予約 でいうところのスタッフ)を表示でき、見た目のカスタマイズも可能です。*2

見た目のカスタマイズについては別でコンポーネントを用意して、自由度の高いスタイル調整を可能にしています。

// Calendarコンポーネント

<MainCalendar
  initialView={calendarViewType} // resourceTimelineやdayGridMonthなど
  dayHeaderContent={CalendarDayHeaderContent} // カレンダーヘッダー
  dayCellContent={CalendarDayCellContent} // カレンダー内日付
  slotLabelContent={CalendarSlotLabelContent} // 時間帯表示
  resourceLabelContent={CalendarResourceLabelContent} // Resouceデータ(スタッフ)
  eventContent={CalendarEventContent} // カレンダー内イベントコンテンツ(予約枠・ブロック枠)
  events={calendarEvents} // Event表示用データ
  resources={calendarResources} // Resource表示用データ
  ~~~
/>

このような感じでFullCalendarにViewTypeを渡して表示切り替えをしています。 またカスタマイズ用のコンポーネントを別で用意しているため、各カスタマイズ用コンポーネントにTimeline Viewのための表示を追加することで対応しました。

ここまでである程度Timeline Viewのためのカスタマイズは実現できたのですが、一部のオプションやスタイル追加で他のViewと同じ動きをしないものがありました。今回はスタイル変更の影響やTimeline View表示で上手く動かなかった点をどのように回避したかを紹介します。

現在時刻線を表すNow Indicator

現在の時刻線を表示するオプションNow Indicatorを使用しているのですが、Timeline Viewの場合だけ12時30分の場合でも12時の縦軸に表示されてしま問題がありました。

調査をしたところ、Timeline Viewの場合ではslotDurationオプションに分単位で指定する必要があるとわかりました。そこでslotDurationの値を1時間から30分に変更して、イベント幅(時間軸ごとの幅)を表すslotMinWidthも半分にしました。 ただこの対応だけだと、30分毎にボーダーの表示がされてしまい見た目に影響を与えてしまうため、FullCalendar側のCSSを書き換えることで対応しました。

<MainCalendar
  ~~~
  slotDuration={
    calendarViewType === 'resourceTimeline'
      ? '00:30:00'
      : '01:00:00'
  }
  slotMinWidth={68} // 30分毎のイベント幅が68px -> 1時間で136px
  ~~~
/>

これで回避できたと思ったのですが、初期状態での表示はうまくいっても、カレンダーのViewTypeを動的に切り替えるとslotDurationが上手く働かないことに気づきました。

そこでViewTypeの切り替えのタイミングでsetOptionを実行してslotDurationの設定値の動的な上書きを可能にしました。

const onCalendarRequireViewTypeChanging =
  useCallback ((viewType: string) => {
    if (!calendarApiRef.current) return;

    setTimeout(() => {
      calendarApiRef.current?.changeView(viewType);

      if (viewType === "resourceTimeline") {
        calendarApiRef.current?.setOption("slotDuration", "00:30");
      } else {
        calendarApiRef.current?.setOption("slotDuration", "01:00");
      }
    });
  },
  []);

サイドバー表示切り替えアニメーションによるカレンダー幅の更新

正式リリースでは絞り込みサイドバーの表示切り替えを可能にしていましたが、アニメーション自体がカレンダーのサイズ更新に影響を与えていました。 アニメーションがなければ問題なく動いていたので、FullCalendar側の描画タイミングが影響していたものと考えられます。

カレンダーのサイズ更新がうまくいかない問題を解決するため、FullCalendar側で用意されているupdateSizeをアニメーション終了後に走らせるようにしました。

const onOpenCalendarSidebar = () => {
  setIsOpenCalendarSidebar(true);

  // アニメーション終了後にfullcalendarのサイズをupdateする
  setTimeout(() => {
    calendarApiRef.current?.updateSize();
  }, 250);
 };

const onCloseCalendarSidebar = () => {
  setIsOpenCalendarSidebar(false);

  // アニメーション終了後にfullcalendarのサイズをupdateする
  setTimeout(() => {
    calendarApiRef.current?.updateSize();
  }, 200);
};

以上、多少の問題がありつつもFullCalendar側のオプションや実装の工夫によって、カレンダー正式版のリリースができました。

Timeline Viewの追加によって、同じ時間帯の予約を確認するのに横スクロールを必要としていたのが改善され、スタッフを多く登録しているオーナー様にとって予約状況を確認しやすくなりました。

実際に横ビューが可能になったことで見やすくなったとの声もいただいており、今回正式版をリリースできて良かったと実感しています。

まとめ

STORES 予約 のカレンダー正式リリースをFullCalendarの事例とともに紹介してみました。 FullCalendarはライブラリとしては機能が充実しているので、カレンダー機能を検討しているのであれば選択肢に含めるのも良いかと思います。

今回正式リリースしたとはいえ、カレンダーページ自体はまだまだパフォーマンスが良くなかったり、スマホビューが未対応と課題が多いため、今後も改善をしていけたらと思っています。

*1:Timeline ViewはPremiumプラン以上での利用となります。

*2:ResourceはPremiumプラン以上での利用となります。