giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

出社日を数えるのが嫌すぎて自動化した話

こんにちは!

ギフティでエンジニアをしている toki です!

ギフティではコロナ禍のなか、多くの社員がリモート勤務メインの状態が長く続いていまして、そうした状況を受けて通勤交通費の支給方法が変わりました。

  • 今まで: 1ヶ月の定期代を申請しておいて、毎月給与と合わせて振り込み
  • これから: 出社した日 x 1日の交通費 (実費請求)

あまり出社していない社員も多いなか、この判断自体はみんな納得なのですが、これによって大きな問題が1つ発生しました。

実費請求をするために、出社日を数えて実費用を計算し、申請を出すという行為が発生します! ナンセンス!

しかし今や時代は2022年。令和に年号も変わり、iPhoneのナンバリングは13になりました。そこで、現代にふさわしい高度なテクノロジーを使ってこれを楽にするものを作りました。

ブックマークレット

こういうやつです↓

javascript:alert("コンニチハ!");

URLの代わりにアドレスバーでjavascriptを実行できる仕組みです。
参考: https://www.ois-yokohama.co.jp/oisblog2018/archives/3441

ためしにこれをアドレスバーに打ち込むと...?

ハハハ、愛い奴め。

これをブックマークに登録すると、ブックマークを開くだけで起動できます。

今回はこのハイテクノロジー(?)を使って、こんなものを作りました。

作ったもの

1ヶ月の出退勤表から、以下のデータを集計して表示しています。 - 物理出社日数(出勤 or 退勤にICの文字が入ってるもの) - 交通費合計 - 金額内訳 (1日交通費 x 物理出社日数)

ただそれだけだと味気ないので、結果表示用のダイアログのデザインを少し頑張ったり、物理出社としてカウントされた行をハイライトしたりもしています。

なお、特定個人の打刻時間は大変センシティブな情報になりますので、テスト / デモ用のサンプルページで動作させています。

どうやって作った?

javascriptをゴリゴリやっています。(ハイテクとは?)

コード的にはこんな感じです。

function displayMonthReport(travelExpensePerDay){
  let countIC = 0;

  // 1ヶ月の打刻リストを取得
  const timerecordsTableDiv = document.querySelector("div.table");
  const timerecordRows = timerecordsTableDiv.querySelectorAll("tbody > tr");

  timerecordRows.forEach((timerecordRow) => {
    // 1日の出勤、退勤レコードを取得
    const startTimerecord = timerecordRow.querySelectorAll(".start_timerecord");
    const endTimerecord = timerecordRow.querySelectorAll(".end_timerecord");

    // 打刻記録がない場合はreturn
    if (!startTimerecord.innerText || !endTimerecord.innerText) {
      return;
    }

    // ICの場合は「IC 08:30」のようなテキストが入るので、数をカウント
    // 出勤か退勤のどちらかがICの場合は物理出社とみなす
    if (startTimerecord.innerText.match(/IC/g) || endTimerecord.innerText.match(/IC/g)) {
      countIC += 1;

      // 目視でも見やすいように物理出社日の行は色を変える
      timerecordRow.style.backgroundColor = "gold"

      return;
    }
  });

  // 表示テキストの作成
  // HTMLなので改行は<br>タグを使う
  const textHtml = `
    【1日の交通費(往復)の設定値】<br>
    ${travelExpensePerDay}円<br>
    <br>
    【物理出社日数(IC)】<br>
    ${countIC}日<br>
    <br>
    【1ヶ月の合計交通費】<br>
    @${travelExpensePerDay}x${countIC}日 = ${travelExpensePerDay * countIC}
  `

  // 結果表示用dialogを作って表示
  showResultDialog(textHtml, timerecordRows);
};

// 結果表示用dialogを作って表示する
function showResultDialog(textHtml, timerecordRows) {
  // 古いものがあったら消す
  // 更新を確実にする、無駄に増やさないため
  const dialogContainerId = 'bookmarklet-dialog-container';
  const dialogId = 'bookmarklet-dialog';
  const prevDialogContainer = document.querySelector(dialogContainerId);
  if (!!prevDialogContainer) {
    document.body.removeChild(prevDialogContainer);
  }

  // 表示するテキストを作る
  let dialogContainer = document.createElement('div');
  dialogContainer.id = dialogContainerId
  dialogContainer.innerHTML = `
    <form id="${dialogId}">
      <h2>1ヶ月の集計結果</h2>
      <p>${textHtml}</p>
      <input type="button" name="cancel" value="閉じる" />
    </form>
    <style>
      #${dialogId} {
        position: fixed;
        top: 100px;
        left: 50%;
        z-index: 1000;
        background-color: lightgoldenrodyellow;
        width: 280px;
        padding: 20px;
        text-align: right;
        border: 1px solid palegoldenrod;
        border-radius: 10px;
        box-shadow: 2px 2px 4px goldenrod;
      }
      #${dialogId} > p {
        font-size: 16px;
        line-height: 24px;
        margin: 20px 0;
      }
      #${dialogId} > input {
        padding: 8px 16px;
        background-color: gold;
        border: none;
        border-radius: 5px;
      }
    </style>
  `
  document.body.appendChild(dialogContainer);

  // 閉じるボタンの挙動をcallbackでセット
  const form = document.querySelector(`#${dialogId}`);
  form.cancel.addEventListener("click", () => {
    document.body.removeChild(dialogContainer);

    // 物理出社日の色を変えたのを戻す
    timerecordRows.forEach((timerecordRow) => {
      timerecordRow.style.backgroundColor = ""
  });
};

このdisplayMonthReportの中身をブックマークレットに登録すれば動かすことができます。 個人で使う分にはそれで十分なのですが、せっかくなので社内にバラまきたいと思います。

そうすると大きな問題が発生します。

コードを修正するたびに、全ユーザーに最新のコードを配布し、登録し直してもらうという行為が発生します! ナンセンス!

しかし今や時代は2022年。移動通信システムは第5世代目(5G)になり、内閣総理大臣は100代目を迎えました。そこで、現代にふさわしい高度なテクノロジーを使ってこれを解決しました。

Github Pages

Github Pagesとは、Github上にpushしたファイルを静的なWebコンテンツとして公開できる機能です。

公式ドキュメント:
https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages

公開はリポジトリのsettingsからGithub Pagesの設定をONにして、デフォルトブランチへpushするだけです。

便利!!!!!

これを使ってスクリプトをサーバー側とブックマークレット側に分け、ブックマークレットからサーバーにあるスクリプトを呼び出すようにします。

ブックマークレット側のコードはこんな感じです。 やっていることは<script>タグを作ってサーバースクリプトを呼び出し、関数を発火させているだけです。

javascript:(
  (serverUrl, travelExpensePerDay) => {
    const scriptId = 'bookmarklet';

    const prevScriptElement = document.querySelector(`#${scriptId}`);

    // 既にscriptがあったら消しておく
    // scriptは1つでいいので無駄に増やさない
    if (!!prevScriptElement) {
      document.body.removeChild(prevScriptElement);
    }

    // script elementsを作ってサーバから関数を呼ぶ
    scriptElement = document.createElement('script');
    scriptElement.id = scriptId;
    scriptElement.src = serverUrl;
    scriptElement.onload = () => {
      // server.js の displayMonthReport 関数を呼び出す
      displayMonthReport(travelExpensePerDay);
    };
    document.body.appendChild(scriptElement);
  }
)(
  'https://bookmarklet.com/server.js', // サーバースクリプトのURL
  '1000' // 1日の交通費(往復)
  );

こうすれば、サーバー側の処理を変えたとしてもブックマークレットは変更がなく、ユーザーにはそのまま使ってもらえます。

便利!!!!!

こうして人々は物理出社日を指折り数えるという労働から開放されたのでした。

まとめ

昔務めていた会社で同じようなことがありとてもツラかったので、仕事そっちのけで思わず作ってしまいました。 (仕事せえ)

軽い気持ちで作ったものですが思ったより反響があり、結構嬉しいです。

また、ギフティではエンジニア一人一人が好きに使えるAWSアカウントが用意されていたり、会社のGithub Pagesを自由に使えたりするんですが、これがとてもありがたかったです。 (一応上限はあります)

ギフティでは思いついたら自分で色々作ってみる文化と環境があるので、「いいな」と思った方はぜひカジュアル面談にお越しください!