2018-10-31に更新

サイボウズLiveを作る-第6回-イベント作成

あと一つ大きなメイン機能であるイベント機能が残っていたのでそちらを作成した。

色々見てみた結果、とりあえず全部FullCalendarに置き換えればいいだろうと言う結論に至った。

FullCalendar - JavaScript Event Calendar

期間や範囲切り替えもあるし、これだけで一通りまかなえる気がする。

イベント予定メニューマスタ

本家にはない(?)が、予定メニューにも色を付けられるようにした。

<template>
  <div class="row">
    <div class="col-12">
      <div class="sample">
        <span class="event-color-sample" v-bind:style="{backgroundColor: currentBgColor.hex, color: currentTextColor.hex}">サンプル</span>
      </div>
    </div>
    <div class="col-12 col-sm-4">
      <div>背景色</div>
      <swatches-picker v-model="currentBgColor"></swatches-picker>
    </div>
    <div class="col-12 col-sm-4">
      <div>文字色</div>
      <swatches-picker v-model="currentTextColor"></swatches-picker>
    </div>
    <input type="hidden" :name="bg_color_name" :value="currentBgColor.hex">
    <input type="hidden" :name="text_color_name" :value="currentTextColor.hex">
  </div>
</template>

<style scoped>
div.row {
  margin-bottom: 20px;
}
</style>

<script>
import { Swatches } from 'vue-color'

export default {
  props: ['bg_color_name', 'text_color_name', 'bg_color', 'text_color'],

  components: {
    'swatches-picker': Swatches,
  },

  data () {
    return {
      currentBgColor: {hex: this.bg_color === undefined ? '#3F51B5' : this.bg_color},
      currentTextColor: {hex: this.text_color === undefined ? '#FFFFFF' : this.text_color},
    }
  },

  methods: {
  }
}
</script>

一覧もvuedraggableを使ってドラッグ&ドロップで簡単に並び替えできるようにした。

面倒かと思ったが、元々のテンプレートをとりあえずコピーから始められるのでそれほどでもなかった。

<template>
  <table class="table">
    <thead>
      <tr>
        <th></th>
        <th>予定メニュー名</th>

        <th></th>
      </tr>
    </thead>
    <draggable v-model="scheduleCategories" :element="'tbody'" :options="{handle: '.handle'}" @end="onEnd">
      <tr v-for="scheduleCategory in scheduleCategories" :key="scheduleCategory.id">
        <td class="handle"><i class="material-icons">drag_handle</i></td>
        <td>
          <span
            v-text="scheduleCategory.name"
            class="event-color-sample"
            v-bind:style="{backgroundColor: scheduleCategory.bg_color, color: scheduleCategory.text_color}"
          ></span>
        </td>

        <td class="text-right">
          <span><a :href="`/${group_id}/schedule-categories/${scheduleCategory.id}/edit`" class="btn btn-default btn-xs">編集</a></span>
          <span>
            <a
              href="#"
              data-confirm="削除してよろしいですか?"
              :data-csrf="csrf"
              data-method="delete"
              :data-to="`/${group_id}/schedule-categories/${scheduleCategory.id}`"
              rel="nofollow"
              class="btn btn-danger btn-xs"
            >削除<div class="ripple-container"></div></a>
          </span>
        </td>
      </tr>
    </draggable>
  </table>
</template>

<style scoped>
.handle {
  cursor: crosshair;
}
</style>

<script>
import draggable from 'vuedraggable'
import axios from 'axios'

export default {
  props: ['group_id', 'schedule_categories'],

  components: {draggable},

  data () {
    return {
      scheduleCategories: this.schedule_categories,
      csrf: document.querySelector('meta[name=csrf]').getAttribute('content'),
    }
  },

  methods: {
    onEnd() {
      axios.put(`/${this.group_id}/schedule-categories/update-order`, {
          ids: this.scheduleCategories.map(c => c.id),
        });
    }
  }
}
</script>

保存側。

  def update_schedule_categories_order(group_id, ids) do
    Enum.with_index(ids)
    |> Enum.each(fn{id, index} ->
      schedule_category = get_schedule_category!(id, group_id)
      update_schedule_category(schedule_category, %{"display_order" => index + 1})
    end)
  end

カレンダー

こちらも面倒かと思ったが、データの取得はコールバックに作ればいいだけだったので非常に楽だった。

<template>
  <div id="calendar">
  </div>
</template>

<style scoped>
</style>

<script>
import axios from 'axios'
import moment from 'moment'

export default {
  props: ['group_id', 'month'],

  data () {
    return {
      currentEvents: [],
    }
  },

  mounted() {
    if (this.month !== undefined) {

    }
    $('#calendar').fullCalendar({
      locale: 'ja',
      header: {
        right:  'month,agendaWeek,agendaDay today prev,next'
      },
      views: {
        month: {
          titleFormat: 'YYYY年 MMMM',
        },
      },
      buttonText: {
        today: '今日',
        month: '月',
        week: '週',
        day: '日',
      },
      firstDay: 1,
      timeFormat: 'HH:mm',
      defaultDate: this.getDefaultDate(),
      events: this.loadEvents.bind(this),
      dayClick: this.dayClick.bind(this),
      eventClick: this.eventClick.bind(this),
    });
  },

  methods: {
    dayClick(date, jsEvent, view) {
      const dateText = date.format('YYYY-MM-DD');
      if (view.name == 'month') {
        location.href = `/${this.group_id}/schedules/new?date=${dateText}`; 
      } else {
        const timeText = date.format('HH:mm:ss');
        location.href = `/${this.group_id}/schedules/new?date=${dateText}&time=${timeText}`; 
      }
    },

    eventClick(calEvent, jsEvent, view) {
      location.href = `/${this.group_id}/schedule-posts/${calEvent.schedule.id}`;
    },

    loadEvents(start, end, timezone, callback) {
      const startDate = start.format('YYYY-MM-DD');
      const endDate = end.format('YYYY-MM-DD');
      axios.get(`/${this.group_id}/schedules/events/${startDate}/${endDate}`)
        .then(response => {
          this.$emit('GET_AJAX_COMPLETE');
          callback(response.data);
        });
    },

    getDefaultDate() {
      if (this.month !== undefined) {
        return moment(this.month + '-01');
      } else {
        return moment();
      }
    }
  }
}
</script>

取得側。
こういう重そうな範囲検索は、大規模サービスだとどう実装してるのか気になる。
日毎にデータを分けてるのかな。(日を子データにしてるとか)大変そう。

  def list_schedules_for_range(group_id, start_date, end_date) do
    query = from s in Schedule,
      where: s.group_id == ^group_id
        and (
          (s.start_date < ^start_date and ^end_date < s.end_date)
          or (^start_date <= s.start_date and s.start_date <= ^end_date)
          or (^start_date <= s.end_date and s.end_date <= ^end_date)
        )
        and is_nil(s.deleted_at)
    Repo.all(query)
    |> Repo.preload(:schedule_category)
  end

不足点

リピートの予定とか放置してる。

あと、設備とかどこで使ってるんだろうと思ったら、アクセスの方法によって登録できたりするっぽい。
完全に抜けてる。

わかりづらいなここは。急にグループ関係なしの画面になるし意味が分からない。

テスト放置なので整備しようかと思う。
そっちの方が面白くて書くこと多かったりするかもしれない。
ソース書いてるのVueばっかりだし。

Copying live

ツイッターでシェア
みんなに共有、忘れないようにメモ

view_list [連載] サイボウズLiveを作る
第2回 サイボウズLiveを作る-第2回-グループ登録まで
第3回 サイボウズLiveを作る-第3回-トピック登録まで
第4回 サイボウズLiveを作る-第4回-Todoをざっと
第5回 サイボウズLiveを作る-第5回-グループへ参加
第6回 サイボウズLiveを作る-第6回-イベント作成

だら@Crieit開発者

Crieitの開発者です。 Webエンジニアです(在宅)。大体10年ちょい。 記事でわかりにくいところがあればDMで質問していただくか、案件発注してください。 業務依頼、同業種の方からのコンタクトなどお気軽にご連絡ください。 業務経験有:PHP, MySQL, Laravel, React, Flutter, Vue.js, Node, RoR 趣味:Elixir, Phoenix, Nuxt, Express, GCP, AWS等色々 PHPフレームワークちいたんの作者

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント