ちょっとした社内業務をアプリ化して作業の効率化を目指す①


こんにちは、株式会社 Gaji-Labo フロントエンドエンジニアの上條(@mk-0A0)です。

タイトルにもある通り、ちょっとした社内業務をアプリ化することで作業者の負担を減らせないか試みています。
アプリ化することで、現在スプレッドシートですべて手作業・目視で確認している作業をシステムとして移行し、結果として現在行っている作業はほとんど必要なくなる想定です。また、スプレッドシートではできなかったことができるようになる、という可能性が広がる面でもメリットは大きいと思っています。
現在も絶賛開発中なので実際のところどれくらい負担が減るのかは分かりかねますが、少なくとも今より楽になることは間違いないと思っています。この取り組みは記事としてシリーズ化する予定なのでぜひ追いかけてもらえると嬉しいです!

どんな作業をしている?

前提として、フルリモートで稼働している Gaji-Labo では意識的にコミュニケーションを取る場をいくつか設けており、そのうちの一つにコミュニケーションタイムというものがあります(そのままですね)。

コミュニケーションタイム(以下 CT)とは毎週月曜日にメンバー同士が1対1で雑談する時間です。組み合わせはローテーションで決まっており、全メンバーと満遍なく話せるようになっています。

社内のコミュニケーションに一役買っている CT ですが、最近「メンバーが増えたことでスプレッドシートでの組み合わせ管理が複雑になってきた」という悩みを抱えています。少人数のうちは大きな負担ではなかったのですが、ありがたいことに人数が増え、気づけばなかなかボリューミーな表になっていました。会社の成長を実感すると同時に管理コストが高くなっているのも事実です。

初期の CT 表(2022.09)
現在の CT 表(2025.04)

これらを踏まえて改善したいポイントは大きく3点です。

  • 作業者の負担が大きい
    • メンバーの増減がある度に手動で追加削除を行っている
    • 仕組み的に列・行の追加以外の作業が必要なので属人化している
    • 実施日の更新も手動なので定期的に見直す必要がある
  • すべて手動なのでヒューマンエラーが起こりやすい
  • 単純に見づらい

これからも人が増えると考えるとこのやり方をずっと続けていくのは現実的ではありません。なんとか管理が楽にならないかという気持ちから個人の学習も兼ねてアプリ化してみることにしました。

やることを整理する

アプリ化するにあたり、まずは CT 表が持っている機能を整理します。

CT表のサンプル画像
  • 総当たり表形式で縦を参加者・横をペアの相手とし、自分の行から見て縦列のメンバーがペアになる
  • 実施日を列挙する
  • 実施日とその日の相手をアルファベットで紐づける
    • 実施日にアルファベットを割り当てる
    • 自分の欄のアルファベットから当日の相手を把握する
  • 自分×自分のマスはグレーアウト
  • 奇数の場合は余った一人を「お休み」とする

上記に加え、せっかくアプリ化するなら欲しい機能(できるかどうかは別として)もいくつか挙げてみました。

  • 認証
  • 手動で組み合わせを作成できる
  • 実施日を自動で更新する
  • Slack と連携してペアを通知
  • 参加・不参加の設定
  • 表・リストのビュー切り替え

理想も混ざっていますが、まずは今の CT 表の機能を満たす最低限の実装をするところまでを本記事のゴールとして進めようと思います。

実装

今回は Next.js v15.1.3 のプロジェクトを作成し、デフォルトで Tailwind を有効にした環境を使用します。

1. table レイアウトの作成

最初はベタでメンバーの配列を作成し、表を大まかにスタイリングします。

export default function Home() {
  const members = ["🐱", "🐶", "🐷", "🐭", "🐹"];
  const membersWithEmpty = ["", ...members];

  return (
    <main className="grid justify-center mt-10">
      <table className="border-t border-l">
        <tbody>
          {membersWithEmpty.map((colMember, rowIndex) => (
            <tr key={`tr-${rowIndex}`} className="border-b">
              <th className="w-10 h-10 border-r">{colMember}</th>
                {members.map((rowMember, colIndex) =>
                // 縦列の1番最初の要素はth, それ以外はtdを出力
                rowIndex === 0 ? (
                  <th key={`cell-th-${colIndex}`} className="w-10 h-10 border-r">
                    {rowMember}
                  </th>
                ) : (
                  <td
                    key={`cell-${colIndex}`}
                    className={`w-10 h-10 border-r text-center`}
                  ></td>
                )
              )}
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

配列に含まれているメンバーが見出しになる表ができました。
左上の空マスを再現するため、空文字を追加した membersWithEmpty 配列を作成しています。

thにメンバーを表示している表

2. 各マスに実施日とペアを紐づける番号を割り当てる

CT 表のアルファベットにあたる部分を実装します。

export default function Home() {
  const members = ["🐱", "🐶", "🐷", "🐭", "🐹"];
  const membersWithEmpty = ["", ...members];

  return (
    <main className="grid justify-center mt-10">
      <table className="border-t border-l">
        <tbody>
          {membersWithEmpty.map((colMember, rowIndex) => (
            <tr key={`tr-${rowIndex}`} className="border-b">
              <th className="w-10 h-10 border-r">{colMember}</th>
                {members.map((rowMember, colIndex) =>
                rowIndex === 0 ? (
                  <th key={`cell-th-${colIndex}`} className="w-10 h-10 border-r">
                    {rowMember}
                  </th>
                ) : (
                  <td
                    key={`cell-${colIndex}`}
                    className={`w-10 h-10 border-r text-center`}
                  >
                    {/* rowIndexはmembersWithEmptyから取得しており、colIndexより要素数が1つ多いため-1する */}
                    {(colIndex + rowIndex - 1) % members.length}
                  </td>
                )
              )}
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

今回は便宜上アルファベットではなく数字としました。
各マスに割り当てられる colIndex と rowIndex を足して人数で割ると、余りの数が人数以下の0〜4になります。余りを求める計算を「モジュロ演算」「剰余演算」と言うそうです。

マスに数字を割り当てた表

3. 実施日を取得する

CT は毎週月曜日に実施しているため、先2ヶ月分の月曜日を取得してみます。日付の操作には date-fns を使用しました。

import {
  addMonths,
  eachDayOfInterval,
  format,
  getMonth,
  getYear,
  isMonday,
} from "date-fns";
import { ja } from "date-fns/locale";

export default function Home() {
  const members = ["🐱", "🐶", "🐷", "🐭", "🐹"];
  const membersWithEmpty = ["", ...members];

  const today = new Date();
  const thisMonth = getMonth(today) + 1;
  const thisYear = getYear(today);
  // 今月1日〜2ヶ月先の最後日までの日にちを取得
  const mondays = eachDayOfInterval({
    start: new Date(`${thisYear}-${thisMonth}-01`),
    end: addMonths(new Date(`${thisYear}-${thisMonth}-31`), 2),
  })
    .filter((day) => isMonday(day)) // 月曜日を抜き出す
    .map((date) => format(date, "yyyy/M/d(E)", { locale: ja }));

  return (
    <main className="grid justify-center mt-10">
      (略)
    </main>
  );
}

マークアップはこちらです。

import {
  addMonths,
  eachDayOfInterval,
  format,
  getMonth,
  getYear,
  isMonday,
} from "date-fns";
import { ja } from "date-fns/locale";

export default function Home() {
  const members = ["🐱", "🐶", "🐷", "🐭", "🐹"];
  const membersWithEmpty = ["", ...members];

  const today = new Date();
  const thisMonth = getMonth(today) + 1;
  const thisYear = getYear(today);
  const mondays = eachDayOfInterval({
    start: new Date(`${thisYear}-${thisMonth}-01`),
    end: addMonths(new Date(`${thisYear}-${thisMonth}-31`), 2),
  })
    .filter((day) => isMonday(day))
    .map((date) => format(date, "yyyy/M/d(E)", { locale: ja }));

  return (
    <main className="flex gap-10 justify-center mt-10">
      <table className="border-t border-l h-full">
        (略)
      </table>
      <ul>
        {mondays.map(
          (monday, index) =>
            index < members.length && (
              <li key={monday}>
                <span>{index % members.length}:</span>
                <time dateTime={monday}>{monday}</time>
              </li>
            )
        )}
      </ul>
    </main>
  );
}

今のところ取得した2ヶ月分すべてを表示する必要はなさそうなので、index < members.length で組み合わせが1周する実施日を表示します。

実施日にインデックスを振ることで2で実装した各マスの数字と一致します。これで実施日とその日のペアを確認できるようになりました。

日にちを取得した表

最初は today (相対値)から90日先を取得していましたが、4/7を過ぎると4/14が0に繰り上がってしまうという問題がありました。そのため今月1日(絶対値)〜2ヶ月後の最終日を取得するやり方に行き着きました。

4. 自分×自分のマスをグレーアウト&お休みにする

ペアの相手が自分になるマスはグレーアウトして「お休み」と見なします。

import {
  addMonths,
  eachDayOfInterval,
  format,
  getMonth,
  getYear,
  isMonday,
} from "date-fns";
import { ja } from "date-fns/locale";

export default function Home() {
  const members = ["🐱", "🐶", "🐷", "🐭", "🐹"];
  const membersWithEmpty = ["", ...members];

  const today = new Date();
  const thisMonth = getMonth(today) + 1;
  const thisYear = getYear(today);
  const mondays = eachDayOfInterval({
    start: new Date(`${thisYear}-${thisMonth}-01`),
    end: addMonths(new Date(`${thisYear}-${thisMonth}-31`), 2),
  })
    .filter((day) => isMonday(day))
    .map((date) => format(date, "yyyy/M/d(E)", { locale: ja }));

  return (
    <main className="flex gap-10 justify-center mt-10">
      <table className="border-t border-l h-full">
        <tbody>
          {membersWithEmpty.map((colMember, rowIndex) => (
            <tr key={`tr-${rowIndex}`} className="border-b">
              <th className="w-10 h-10 border-r">{colMember}</th>
              {members.map((rowMember, colIndex) =>
                rowIndex === 0 ? (
                  <th key={`cell-th-${colIndex}`} className="w-10 h-10 border-r">
                    {rowMember}
                  </th>
                ) : (
                  <td
                    key={`cell-${colIndex}`}
                    className={`w-10 h-10 border-r text-center ${
                      rowIndex - 1 === colIndex && "bg-gray-100" // グレーアウト
                    }`}
                  >
                    {(colIndex + rowIndex - 1) % members.length}
                  </td>
                )
              )}
            </tr>
          ))}
        </tbody>
      </table>
      <ul>
        (略)
      </ul>
    </main>
  );
}

例えば実施日が0の場合、ペアは 🐶🐹 と 🐷🐭 の二組、🐱 はお休みとなります。

奇数のメンバーでグレーアウトした表

この運用は奇数の時にしか使えないため、偶数の場合は同じくグレーアウトしているマスで同じ数字が振られているメンバーがペアとなります。
例えば実施日が0の場合のペアは 🐱🐷、2の場合は 🐶🐭 となります。

偶数のメンバーでグレーアウトした表

本来の CT 表とは見た目が異なりますが、本記事のゴールである「今の CT 表の機能を最低限で実装する」は達成しました!
まだまだ改善できる点はたくさんありますが、member 配列の操作で表が更新されることと実施日の取得を自動化できたのでさっそく課題クリアです。

次にやること

CT 表を手元の Next.js 環境で再現できたので、次はメンバーの追加・削除を実装したいと思います。

おわりに

本作業はこちらのリポジトリで行っています。
リポジトリ: https://github.com/mk-0A0/ct-schedule
PR: https://github.com/mk-0A0/ct-schedule/pull/9

今回は社内で使っているものをもっと楽にしたいという気持ちから開発を始めました。規模は違えど実際にサービスが生まれる過程は「この課題を解決したい」という思いから始まるのだろうなと実感しています。
そんな中で Gaji-Labo はフロントエンド開発の観点から様々な事業の課題解決に携わっています。興味を持っていただけましたらお気軽にカジュアル面談にお申し込みください!

Gaji-Labo フロントエンドエンジニア向けご案内資料

Gaji-Labo は Next.js 開発の実績と知見があります

フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。

「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」

フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。

オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。

フロントエンドの相談をする!


投稿者 Kamijo Momoka

フロントエンドエンジニア。
HTML/CSS/JavaScript/WordPressでのサイト制作からNext.js/TypeScriptなどを使ったWebアプリ開発、FigmaでのUIデザインまで広く経験しています。 デザインエンジニアと名乗るのが夢です。