🍣

Slackでスタンプを押すだけで勤怠打刻・勤怠サマリレポートしてくれる仕組みを作った

2022/10/05に公開約7,400字

🐣 はじめに

みなさん、勤怠打刻してますか?

先日、このようなツイートをしたところ、思わぬ反響がありました。
https://twitter.com/paranishian/status/1575646345876340736
そこで、この仕組みの全体像や工夫した点などをまとめることにしました。
SlackやGASを使ったOps自動化に興味がある人に読んでもらえたら嬉しいです。

きっかけ

そもそもSlackにはfreeeが公式で提供している人事労務用のSlack appがあり、スラッシュコマンドを使って勤怠打刻できます。便利ですね。
https://slack.com/apps/AD98ZD3EV-freee

ただ、このアプリ、コマンドを打つのがとにかくめんどくさかったりします。
あるとき、同僚が「もっと気軽に勤怠打刻できたらええのになぁ」と言っているのを耳にしました。

そこで、スタンプで勤怠打刻できる仕組みを作り、運用を始めました。

それから数ヶ月後、会社にフレックスタイム制が導入されました。
「ワークライフバランス!!さいこう!」となりつつも「今月はあと何時間働けば良いんだっけ?」をいちいち計算しないといけないつらみが出てきました。

そこで、スタンプ勤怠打刻時に勤怠サマリレポートも通知するように改善しました。

🎯 主な機能

スタンプを押すと、出勤・退勤打刻ができ、勤怠サマリも通知してくれます。

出勤時:Slack workflowが定時に挨拶してくるので、:shukkin: スタンプを押すと

人事労務freeeへの出勤打刻と勤怠サマリレポートを通知してくれます。

退勤時:Slack workflowが定時に挨拶してくるので、 :taikin: スタンプを押すと、

人事労務freeeへの退勤打刻の旨を通知してくれます。

🌏 実装の全体像

ざっくりとした処理の流れは以下の通りです。

Slackbot、GAS、freee APIで構築しています。
Slackbotでreaction_addedイベントをsubscribeし、リクエストはGASが受け取ります。
そしてGAS側でユーザーの突合、freeeへの勤怠打刻、サマリ取得、Slack通知を実装しています。

一部になりますが、GASのコードも載せておきます。(記事用に少し改変しています)

const SHUKKIN = 'shukkin'
const TAIKIN = 'taikin'
const COMPANY_ID = 1234567

function doPost(e) {
  const params = JSON.parse(e.postData.getDataAsString());

  // SlackのEvent SubscriptionのRequest Verification用
  if (params.type === 'url_verification') {
    return ContentService.createTextOutput(params.challenge);
  }

  const event = params.event
  const user = event.user
  const reaction = event.reaction
  const eventId = params.event_id

  // 特定のチャネル以外のスタンプは無視
  if (event.item.channel !== 'C12345678') return;

  // shukkin, taikin 以外のスタンプは無視
  if (reaction !== SHUKKIN && reaction !== TAIKIN) return;

    // Slackからの再送リクエストを無視するためにキャッシュを使う(10分)
  const cache = CacheService.getScriptCache()
  const cached = cache.get(eventId)
  if (cached) {
    Logger.log(`すでに処理したイベントなのでスルーします。(eventId: ${eventId})`)
    return
  }
  cache.put(eventId, true, 60 * 10) 

  if (reaction === SHUKKIN) {
    const todayShukkinSheet = SpreadsheetApp.getActive().getSheetByName('today_shukkin_log')
    const users = todayShukkinSheet.getRange("B2:B").getValues().flat();
    // すでに出勤打刻済みなら無視
    if (users.includes(user)) return;
  }
  else if (reaction === TAIKIN) {
    // 退勤は上書きしてよい
  }

  // Slack -> Freeeのユーザー突合
  const freeeUser = findFreeeUser(user)
  if (!freeeUser) return;
  
  // freeeに打刻
  const type = reaction === SHUKKIN ? 'clock_in' : 'clock_out'
  timeClocksToFreee(freeeUser.employeeId, type)

  const lines = []
  lines.push(`<@${user}>`)
  if (reaction === SHUKKIN) {
    lines.push(`おはようございます!今日も頑張っていきましょう:muscle:`)
    lines.push(`✅ freee打刻済`)
    // 勤怠サマリを取得してテキスト整形する
    lines.push(getWorkTimeText(freeeUser.employeeId))
  } else {
    lines.push(`お疲れ様でした!また明日:wave:`)
    lines.push(`✅ freee打刻済`)
  }
  const text = lines.join("\n")
  // Slackに通知
  notifyToKintaiChannel(text)

  // logに記録
  const logSheet = SpreadsheetApp.getActive().getSheetByName('kintai_log')
  const datetime = Utilities.formatDate(new Date(event.event_ts * 1000), "Asia/Tokyo", "yyyy-MM-dd HH:mm:ss")
  logSheet.appendRow([datetime, user, reaction])
}

function findSlackEmail(id) {
  const sheet = SpreadsheetApp.getActive().getSheetByName('slack_users')
  // [[id, name, real_name, email], [id, name, real_name, email], ...]
  const user = sheet.getRange("A2:D").getValues().find((u) => u[0] === id);
  return !!user ? user[3] : undefined
}

function findFreeeUser(slackUserId) {
  const email = findSlackEmail(slackUserId)
  if (!email) return undefined

  const sheet = SpreadsheetApp.getActive().getSheetByName('employee_freee_users')
  // [[メールアドレス, id], ...]
  const row = sheet.getRange("A2:B").getValues().find((u) => u[0] === email);
  return !!row ? {email: row[0], employeeId: row[1]} : undefined
}

GASでfreeeのアクセストークンを取得する方法は、freeeさんが丁寧な記事を書いてくれています。ぜひ参考にしてください。
【freee API】GASを用いてGoogleスプレッドシートと連携する

また、今回使用しているAPIは以下の2つです。

  • タイムレコーダー(打刻) POST
  • 勤怠情報サマリ GET

https://developer.freee.co.jp/reference/hr/reference

freeeアプリには以下の権限を付与しています。

  • [人事労務] 打刻 の参照・更新
  • [人事労務] 勤怠 の参照

注意点として、freeeアプリの認証は人事労務freeeの管理者が行ってください。
アプリを認可しただけでは、APIで権限が足りないと弾かれてしまいます。
このあたりめっちゃハマりました。
https://developer.freee.co.jp/reference/hr#ログインユーザーの情報を取得する

なお、Slackユーザーとfreeeユーザーの突合には、メールアドレスを利用しています。

💡 工夫したポイント

その1:シートの設計

こういったGASの自動化においては、シート設計(=DB設計)が肝だと考えています。
今回は以下の構成にしました。

  • kintai_log
    • 「いつ」「だれが」「出勤・退勤したか」が保存されているシート
  • today_shukkin_log
    • kintai_logシートから今日の出勤をQUERYしたシート
    • 出勤済みユーザーかどうかの判定に利用する
  • slack_users
    • Slackのユーザー情報が保存されているシート
    • SlackのユーザーIDからメールアドレスを取得するために利用する
  • freee_users
    • freeeeのユーザー情報が保存されているシート
    • メールアドレスからfreeeのユーザーIDを取得するために利用する

また、Slackユーザーのメールアドレスは、日次でユーザー一覧を取得するAPIを叩いてslack_usersシートに保存しています。

const SLACK_BOT_TOKEN = 'xoxb-12345'

function fetchSlackUsers() {
  // https://api.slack.com/methods/users.list
  const apiResponse = callWebApi(SLACK_BOT_TOKEN, "users.list", {});
  const json = JSON.parse(apiResponse);

  const users = []
  json.members.filter((m) => !m.deleted && !m.is_bot ).forEach((m) => {
    users.push([m.id, m.name, m.real_name, m.profile.email])
  });

  const sheet = SpreadsheetApp.getActive().getSheetByName('slack_users')
  sheet.getRange("A2:D").clear()
  sheet.getRange(2, 1, users.length, 4).setValues(users)
}

function callWebApi(token, apiMethod, payload) {
  const response = UrlFetchApp.fetch(
    `https://www.slack.com/api/${apiMethod}`,
    {
      method: "post",
      contentType: "application/json; charset=UTF-8",
      headers: { "Authorization": `Bearer ${token}` },
      payload: JSON.stringify(payload),
    }
  );
  return response;
}

その2:Slackの再送イベントの処理

Slack Events APIには、さまざまなリトライ条件があります。
https://dev.classmethod.jp/articles/slack-resend-matome/
GASでは頻繁にこれらの条件に引っかかってしまうので、キャッシュを利用して再送時の処理をスキップしています。

その3:「労働」ということばを使わない

労働・・・したくないですよね。
人事労務freeeでは、「労働日数」や「所定内労働」など、「労働」が多用されていますが、Slackに通知する際は「勤怠」というやわらかめのワードに置き換えています。(とってもだいじ)

🤝 今後やっていきたいこと

残業時間の計算なども要望として上がっているので、追加実装して試験運用中です。
働きすぎをそっと防止する仕組みを作っていきたいです。

🍵 おわりに

ってなかんじで、Slackbot、GASを使った勤怠Ops自動化について書いてみました。
今回はfreeeでの実装でしたが、ほかの勤怠管理システムでもAPIがあれば実現可能だと思います。
めんどくさい作業はどんどん自動化して気持ちよく働きたいですね。

ちなみに、この仕組みはmicroCMS社に導入して運用しています。
エンジニア企業らしく、社内業務の自動化に積極的に取り組んでいるとても素敵な会社です。

・・・というわけで、絶賛エンジニア採用中!興味があれば覗いてみてください。(最下部にMeetyリンクもあります)
https://microcms.co.jp/recruit

もしこの記事を読んで参考になりましたら、いいねやTwitterのフォローをしていただけるととても嬉しいです。

他にもいろいろと自動化に取り組んでいるので、隙を見て発信していきたいと思います。
それでは!👋

Discussion

ログインするとコメントできます