Slackでスタンプを押すだけで勤怠打刻・勤怠サマリレポートしてくれる仕組みを作った
🐣 はじめに
みなさん、勤怠打刻してますか?
先日、このようなツイートをしたところ、思わぬ反響がありました。
SlackやGASを使ったOps自動化に興味がある人に読んでもらえたら嬉しいです。
きっかけ
そもそもSlackにはfreeeが公式で提供している人事労務用のSlack appがあり、スラッシュコマンドを使って勤怠打刻できます。便利ですね。
ただ、このアプリ、コマンドを打つのがとにかくめんどくさかったりします。
あるとき、同僚が「もっと気軽に勤怠打刻できたらええのになぁ」と言っているのを耳にしました。
そこで、スタンプで勤怠打刻できる仕組みを作り、運用を始めました。
それから数ヶ月後、会社にフレックスタイム制が導入されました。
「ワークライフバランス!!さいこう!」となりつつも「今月はあと何時間働けば良いんだっけ?」をいちいち計算しないといけないつらみが出てきました。
そこで、スタンプ勤怠打刻時に勤怠サマリレポートも通知するように改善しました。
🎯 主な機能
スタンプを押すと、出勤・退勤打刻ができ、勤怠サマリも通知してくれます。
出勤時: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
freeeアプリには以下の権限を付与しています。
- [人事労務] 打刻 の参照・更新
- [人事労務] 勤怠 の参照
注意点として、freeeアプリの認証は人事労務freeeの管理者が行ってください。
アプリを認可しただけでは、APIで権限が足りないと弾かれてしまいます。
このあたりめっちゃハマりました。
なお、Slackユーザーとfreeeユーザーの突合には、メールアドレスを利用しています。
💡 工夫したポイント
その1:シートの設計
こういったGASの自動化においては、シート設計(=DB設計)が肝だと考えています。
今回は以下の構成にしました。
- kintai_log
- 「いつ」「だれが」「出勤・退勤したか」が保存されているシート
- today_shukkin_log
- kintai_logシートから今日の出勤を
QUERY
したシート - 出勤済みユーザーかどうかの判定に利用する
- kintai_logシートから今日の出勤を
- 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には、さまざまなリトライ条件があります。
GASでは頻繁にこれらの条件に引っかかってしまうので、キャッシュを利用して再送時の処理をスキップしています。その3:「労働」ということばを使わない
労働・・・したくないですよね。
人事労務freeeでは、「労働日数」や「所定内労働」など、「労働」が多用されていますが、Slackに通知する際は「勤怠」というやわらかめのワードに置き換えています。(とってもだいじ)
🤝 今後やっていきたいこと
残業時間の計算なども要望として上がっているので、追加実装して試験運用中です。
働きすぎをそっと防止する仕組みを作っていきたいです。
🍵 おわりに
ってなかんじで、Slackbot、GASを使った勤怠Ops自動化について書いてみました。
今回はfreeeでの実装でしたが、ほかの勤怠管理システムでもAPIがあれば実現可能だと思います。
めんどくさい作業はどんどん自動化して気持ちよく働きたいですね。
ちなみに、この仕組みはmicroCMS社に導入して運用しています。
エンジニア企業らしく、社内業務の自動化に積極的に取り組んでいるとても素敵な会社です。
もしこの記事を読んで参考になりましたら、いいねやTwitterのフォローをしていただけるととても嬉しいです。
その他複数の企業で、Ops自動化・効率化をお手伝いしています。
興味がありましたらDM等でご相談ください。
それでは!👋
Discussion