勤怠管理システムの設計ガイド
外回りの多い営業職の労働時間を、LINEで正確に把握する。
歩合給の割増賃金計算まで、エルメ設定から本番運用まで一冊にまとめました
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
LINE公式アカウント・タグ・リッチメニュー・出退勤分岐ロジックの実装仕様
目的:LINE公式アカウントとエルメッセージを連携し、スタッフが友だち追加してリッチメニューから出退勤・休憩を打刻できる土台を完成させます。
打刻はエルメの回答フォームで行い、「打刻がフォームの回答として連携シートに残り、タグで状態が切り替わる」状態をゴールとします。台帳への転記と計算はPHASE 4で接続します。
勤怠管理専用のアカウントを用意します。集客用アカウントとは分けて作成し、スタッフ向けと顧客向けのメッセージが混ざらないようにします。
| 設定項目 | 推奨値 | 備考 |
|---|---|---|
| アカウント種別 | 勤怠管理専用(新規) | 既存の集客用とは分離 |
| アカウント名 | あおば不動産販売 勤怠 | スタッフが識別しやすい名称 |
| 応答設定 | Webhookオン/チャットオン | エルメ連携の必須設定(応答メッセージはオフ) |
| あいさつメッセージ(LINE公式側) | オフ | 友だち追加時の案内はエルメの「友だち追加時アクション」で送る(§7) |
| 料金プラン | まず無料(月200通) | リマインド通数次第で有料検討 |
※ 料金・通数の条件は改定されることがあります。導入時点でLINE for Businessの公式ページをご確認ください。
PHASE 1の心臓部です。打刻フォームの回答後アクションで「出勤中」「退勤済」「休憩中」を付け外しし、その有無で二重打刻の案内やリマインドを分岐します。最初にタグを正確に設計します。
| タグ名 | 付与タイミング | 解除タイミング | 用途 |
|---|---|---|---|
| 出勤中 | 打刻フォーム「出勤」送信時 | 「退勤」送信時/翌朝5時リセット | 二重打刻の案内・退勤忘れリマインドの判定軸 |
| 退勤済 | 打刻フォーム「退勤」送信時 | 翌朝5時リセット | 当日の勤務終了の判定・日次リセットと日報リマインドの開始条件 |
| 休憩中 | 休憩フォーム「休憩開始」送信時 | 「休憩終了」送信時 | 休憩戻り忘れリマインドの判定軸 |
| スタッフ_在籍 | 勤怠用QRの読み込み時(流入経路アクション) | 退職時に管理者が解除 | リッチメニューの表示条件・すべての配信の絞り込み |
| タグ名 | 付与方法 | 用途 |
|---|---|---|
| 管理者 | 友だち追加後に手動付与 | 管理者向けメニュー・通知の対象 |
| 正社員 / パート・アルバイト | 手動 or 流入経路で付与 | 雇用形態別の集計・運用 |
| 試用期間中 | 手動付与 | 対象者の識別 |
| 歩合給あり | 手動付与(社員マスタH列と一致させる) | 割増賃金の計算方法の切替(PHASE 4 §5) |
| 直行直帰あり | 手動付与 | 外回り中心の営業職の識別・打刻ルールの案内 |
スタッフが毎日使う6ボタンを設計します。画像を3列2行で分割し、各領域にアクションを割り当てます。打刻・休憩・日報・有給・修正はすべてエルメの回答フォームを開くボタンにします(フォームの回答だけがスプレッドシートへ自動記録されるため)。
| 項目 | 仕様 |
|---|---|
| 画像サイズ | 2500 × 1686 px(大サイズ) |
| 分割 | 3列 × 2行(6分割) |
| 1ボタンの目安 | 約833 × 843 px |
| 形式 | JPG / PNG(1MB以下推奨) |
| 表示条件 | タグ「スタッフ_在籍」あり(管理者には「管理者」タグで別メニューを出し分け可) |
| 位置 | ボタン | アクション種別 | 開くもの/動作 |
|---|---|---|---|
| A | 🕘 出勤・退勤 | URLを開く | フォーム「打刻」(区分:出勤/退勤) |
| B | ☕ 休憩 | URLを開く | フォーム「休憩」(区分:休憩開始/休憩終了) |
| C | 📝 日報入力 | テキスト送信 | 「準備中です」仮応答(PHASE3でフォームに差し替え) |
| D | 📊 今月の勤怠確認 | テキスト送信 | 「準備中です」仮応答(PHASE4で閲覧用シートのURLに差し替え) |
| E | 🌴 有給休暇届 | テキスト送信 | 「準備中です」仮応答(PHASE3でフォームに差し替え) |
| F | ⚠️ 修正申請 | テキスト送信 | 「準備中です」仮応答(PHASE3でフォームに差し替え) |
フォーム「打刻」は設問1つ(区分:出勤/退勤)だけの最小構成です。回答日時がそのまま打刻時刻になり、連携シートに記録されます。回答後アクションを選択肢ごとに設定し、タグで状態を切り替えます。
⏰ 出勤:09:00
退勤時も同じボタンから「退勤」を選んでください。
本日の日報が未提出の方は「📝 日報入力」からお願いします。
| ID | 確認内容 | 期待結果 |
|---|---|---|
| P1-01 | 未出勤で「出勤」を送信 | 連携シートに1行追加+「出勤中」タグ付与+完了返信 |
| P1-02 | 出勤中で「退勤」を送信 | 連携シートに1行追加+タグが「退勤済」に切替 |
| P1-03 | 出勤中でもう一度「出勤」 | 「すでに出勤打刻済み」と案内(タグはそのまま) |
| P1-04 | 休憩開始→休憩終了 | 2行記録され「休憩中」が付いて外れる |
| P1-05 | 出勤前に休憩開始 | 「先に出勤打刻を」と案内 |
| P1-06 | 未実装ボタン(C〜F) | 「準備中です」と仮応答 |
| P1-07 | 翌朝5時以降の状態 | 出勤中/退勤済タグがリセットされ、朝の案内が届く |
PHASE 2 への引き継ぎ事項
PHASE 1で「打刻がフォームで記録され、タグで状態が切り替わる土台」が完成しました。次のPHASE 2では、この打刻データを集計するGoogleスプレッドシート(5シート)を作成します。
引き継ぐもの:①打刻状態タグの設計(出勤中/退勤済/休憩中) ②フォーム「打刻」「休憩」の設問文(連携シートの見出しになる) ③友だち登録で集めた氏名(社員マスタの照合キー)。
※ 連携シートから日報データへの転記と、実働時間・変形労働残業の計算はPHASE 4で接続します。
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
5シートの列定義・計算式・社員マスタ・変形労働カレンダーの実装仕様
目的:PHASE 1で打刻した勤怠データを集計するGoogleスプレッドシートの器(台帳)を作成します。フォームの回答はエルメが作る連携シート(4ファイル)に自動で溜まるので、台帳は「連携シートから転記して集計する場所」です。各シートの列定義(見出し・データ型)と計算式の置き場所を確定させることが本フェーズの中心です。
GASによる自動転記・集計(PHASE 4)はまだ行いませんが、列構成を正しく設計しておくことが後工程の前提になります。
| No | シート名 | 役割 | 入力方法 |
|---|---|---|---|
| 1 | 日報データ | 毎日の打刻・日報を1行ずつ蓄積 | PHASE4でGAS自動追記 |
| 2 | 月次集計 | 社員ごと月別の集計結果 | 計算式+GAS集計 |
| 3 | 変形労働カレンダー | 月の所定労働時間・法定総枠を設定 | 手動(年度初め等) |
| 4 | 社員マスタ | スタッフ名簿・LINE紐付け | 手動 |
| 5 | 修正履歴 | 打刻修正の証跡(PHASE3・4連動) | 申請を転記し、管理者が承認者を入力→GASが反映 |
| 項目 | 仕様 |
|---|---|
| ファイル名 | 勤怠データ_2025 |
| 保存場所 | 会社Googleドライブの勤怠管理フォルダ |
| シートタブ | 日報データ/月次集計/変形労働カレンダー/社員マスタ/修正履歴 |
| タイムゾーン | ファイル>設定で「日本標準時 (GMT+9)」に設定 |
| ロケール | 日本(日付・数値の書式を統一) |
最も重要なシート。毎日の打刻が1行ずつ蓄積されます。列の順番・見出しはPHASE 4のGAS書き込みと一致させます。
| 列 | 見出し | データ型 | 入力元 | 例 |
|---|---|---|---|---|
| A | 日付 | 日付 | GAS(打刻日) | 2025/06/10 |
| B | 社員ID | 文字 | GAS(マスタ参照) | 001 |
| C | 氏名 | 文字 | GAS | 山田太郎 |
| D | 出勤 | 時刻 | GAS(出勤打刻) | 9:00 |
| E | 退勤 | 時刻 | GAS(退勤打刻) | 18:30 |
| F | 休憩(分) | 数値 | GAS(休憩集計) | 60 |
| G | 実働(分) | 数値/式 | GAS計算(§PHASE4) | 510 |
| H | 深夜(分) | 数値 | GAS計算 | 0 |
| I | 打ち合わせ | 文字 | GAS(日報) | 山田様 90分 案内 |
| J | 成約内容 | 文字 | GAS(日報) | 白金台101 売買 3200万 |
| K | 報告事項 | 文字 | GAS(日報) | 田中様 再連絡希望 |
| L | 翌日出勤 | 時刻 | GAS(日報) | 9:30 |
| M | 翌日退出 | 時刻 | GAS(日報) | 19:00 |
| N | 翌日予定 | 文字 | GAS(日報) | 午前 内見2件 |
| O | 成約額 | 数値 | GAS(日報) | 32000000 |
| P | 状態 | 文字 | GAS | 自動/修正済 |
社員ごと・月別に集計します。日報データを参照する計算式を置き、PHASE 4のGASでも更新します。
| 列 | 見出し | 算出方法(イメージ) |
|---|---|---|
| A | 対象月 | 2025-06 |
| B | 社員ID | 社員マスタ参照 |
| C | 氏名 | 社員マスタ参照 |
| D | 出勤日数 | =COUNTIFS(日報!日付,当月,日報!氏名,C) |
| E | 実働(h) | =SUMIFS(日報!実働,…)/60 |
| F | 法定総枠(h) | 変形労働カレンダー参照(暦日数で決定) |
| G | 残業(h) | GAS(日→週→月の三段階判定。§PHASE4 §5) |
| H | 深夜(h) | =SUMIFS(日報!深夜,…)/60 |
| I | 打ち合わせ件数 | 当月の打ち合わせ記録を集計 |
| J | 成約件数 | 当月の成約記録を集計 |
| K | 成約額 合計 | =SUMIFS(日報!成約額,…) |
| L | 歩合給 | 成約額×歩合率(給与規程に基づき算出・手入力も可) |
| M | 固定給 | 社員マスタI列(基本給)を参照 |
| N | 歩合給の時間単価 | =L / 総労働時間(E) |
| O | 割増賃金 | 固定給分+歩合給分+休日・深夜を合算(§PHASE4 §5) |
| P | 法定休日(h) | 法定休日の労働時間(時間外とは別枠・3割5分) |
| Q | 最低賃金判定 | (固定給+歩合給)÷総労働時間 ≧ 最低賃金 → OK/要確認 |
// ① 日:所定が8hを超える日はその所定、それ以外は8hを超えた時間 // ② 週:所定が40hを超える週はその所定、それ以外は40hを超えた時間(①を除く) // ③ 月:月の実働合計 − 法定総枠(①②を除く) 残業(h) = ① + ② + ③ ※法定休日の労働は含めず別枠(3割5分) // 法定総枠は暦日数で決まる(変形労働カレンダーから取得) // 31日=177.0 / 30日=171.0 / 29日(うるう年)=165.5 / 28日=160.0
1か月単位変形労働時間制の各日の所定労働時間と、月の法定総枠を設定します。年度初めや月初に手動設定します。
| 列 | 見出し | 例 | 備考 |
|---|---|---|---|
| A | 日付 | 2025/06/02 | 当月の全日 |
| B | 曜日 | 月 | =TEXT(A2,”aaa”) |
| C | 区分 | 通常/特定/休日 | 入力規則でプルダウン |
| D | 所定開始 | 9:00 | 勤務日のみ |
| E | 所定終了 | 18:00 | 勤務日のみ |
| F | 所定労働(h) | 8.0 | =(E−D)*24−休憩 |
| G | 法定休日 | ○ / 空欄 | 週1日の法定休日 |
| 月の暦日数 | 法定総枠 | 該当月 |
|---|---|---|
| 31日 | 177.0 時間 | 1・3・5・7・8・10・12月 |
| 30日 | 171.0 時間 | 4・6・9・11月 |
| 28日(平年2月) | 160.0 時間 | 2月(平年) |
| 29日(うるう年2月) | 165.5 時間 | 2月(うるう年) |
スタッフの名簿。LINE表示名と本名・社員IDを紐付け、PHASE 4でGASがこのマスタを参照して日報データへ氏名を補完します。
| 列 | 見出し | データ型 | 例 | 備考 |
|---|---|---|---|---|
| A | 社員ID | 文字 | 001 | 主キー・重複不可 |
| B | 氏名 | 文字 | 山田太郎 | 正式名称 |
| C | LINE表示名 | 文字 | たろう | 友だち追加時の名前 |
| D | 照合キー(お名前) | 文字 | 山田太郎 | フォームの「お名前」と完全一致させる(空白は無視して比較) |
| E | 雇用形態 | 選択 | 正社員 | 正社員/パート/アルバイト |
| F | 入社日 | 日付 | 2023/04/01 | 有給付与計算等に使用 |
| G | 管理者フラグ | 選択 | ○ / 空欄 | 管理者通知の対象 |
| H | 給与形態 | 選択 | 固定給+歩合給 | 固定給のみ / 固定給+歩合給 / 完全歩合 |
| I | 基本給(月額) | 数値 | 220000 | 固定給部分。割増単価の分子に使う |
| J | 歩合率 | 数値 | 3.0% | 成約額に対する歩合の率(参考値) |
| K | 保障給(時間額) | 数値 | 1200 | 出来高払制の保障給(労基法27条)。最低賃金以上に設定 |
| L | 月平均所定労働時間 | 数値 | 171.0 | 固定給の割増単価の分母(年間所定労働時間÷12) |
打刻の修正申請(PHASE 3)が承認されたとき、PHASE 4のGASが誰がいつ何を修正したかを自動記録します。労務管理上のトレーサビリティを確保する重要なシートです。
| 列 | 見出し | 例 |
|---|---|---|
| A | 申請日時 | 2025/06/19 10:05 |
| B | 対象社員 | 山田太郎 |
| C | 対象日 | 2025/06/18 |
| D | 項目 | 出勤 / 退勤 / 休憩 |
| E | 修正前 | (空欄) |
| F | 修正後 | 9:00 |
| G | 申請者 | 山田太郎 |
| H | 承認者 | 管理者(入力するとGASが反映) |
| I | 反映日時 | 2025/06/19 10:10(GASが記録) |
| 対象 | 設定 | 理由 |
|---|---|---|
| ファイル共有範囲 | 管理者のみ(リンク共有オフ) | 個人情報・労働時間の保護 |
| 編集権限 | 管理者:編集/一般:付与しない | 誤編集の防止 |
| 日報データシート | 保護(GASのみ書き込み) | 手動での誤入力防止 |
| 見出し行 | 保護(編集不可) | 列構成の維持 |
| 社員マスタ | 管理者のみ編集 | 個人情報の管理 |
PHASE 3・4 への引き継ぎ事項
PHASE 2で「データが入る器(5シート)」が完成しました。
PHASE 3ではLINEの日報・有給・修正のフォームを作り込み、PHASE 4で連携シートから本シートへの自動転記と計算を構築します。
引き継ぐもの:①日報データの列構成(A〜P)と列番号 ②月次集計の列(A〜Q) ③社員マスタの照合キー(氏名)と月平均所定労働時間 ④変形労働カレンダーの所定・法定休日・法定総枠。
※ 列の順番・見出しは確定版とし、以降は安易に変更しないでください(GAS連携の前提になります)。
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
エルメッセージの回答フォーム・タグ・ステップ配信の実装仕様
目的:PHASE 1の「準備中です」ボタン(日報入力・有給休暇届・修正申請)を、エルメッセージの回答フォームで実際に動く機能へ作り替える。あわせて打刻漏れリマインドと日次リセットをステップ配信で構築する。
データの扱い:入力値はすべてフォームの回答として連携シートに自動記録する(友だち情報に保存するのは氏名だけ)。PHASE 4のGASは連携シートを読むだけなので、本フェーズでは「フォームの設問文」を確定させておくことが重要。
| No | 仕組み | 入口 | 使用タグ | 記録先 |
|---|---|---|---|---|
| ① | 日報入力(打ち合わせ/成約/報告事項) | リッチメニューC → フォーム「日報」 | 日報_提出済 | 連携シート「日報」 |
| ② | 有給休暇届 | リッチメニューE → フォーム「有給休暇届」 | 有給_申請中/有給_承認済/有給_差戻し | 連携シート「有給」+管理者へアプリ通知 |
| ③ | 修正申請(打刻漏れ対応) | リッチメニューF → フォーム「修正申請」 | 修正_申請中 | 連携シート「修正申請」+管理者へアプリ通知 |
| ④ | リマインド・日次リセット | ステップ配信(タグ追加を開始条件) | 出勤中/休憩中/退勤済/日報_提出済 | —(配信のみ) |
PHASE 3で追加するタグと、いつ付与・いつ解除するかを定義します。「付けて切り替える」のが原則で、付与と解除は必ずセットで設計します。
| タグ名 | 付与タイミング | 解除タイミング | 用途 |
|---|---|---|---|
| 日報_提出済 | フォーム「日報」の回答後アクション | 翌朝5時の日次リセット(ステップ配信) | 退勤後の「日報未提出」リマインドの除外条件 |
| 修正_申請中 | フォーム「修正申請」の回答後アクション | 管理者が反映後に解除 | 申請中の重複防止・未処理の把握 |
| タグ名 | 付与タイミング | 解除タイミング | 用途 |
|---|---|---|---|
| 有給_申請中 | フォーム「有給休暇届」の回答後アクション | 管理者が「有給_承認済」または「有給_差戻し」に付け替え | 申請中の表示・重複防止 |
| 有給_承認済 | 管理者が友だち詳細で付与(タグ追加時アクションで本人へ自動返信) | 取得日経過後に管理者が解除 | 承認状態の管理 |
| 有給_差戻し | 管理者が付与(タグ追加時アクションで本人へ自動返信) | 本人が再申請した時 | 差し戻しの通知と記録 |
各フォームの設問を定義します。設問文がそのまま連携シートの見出しになり、PHASE 4のGASはこの見出し名でデータを読むため、ここで確定させます。「お名前」は全フォーム共通で、友だち情報 staff_name から自動入力します。
| フォーム | 設問文(見出し) | 形式 | 選択肢・例 |
|---|---|---|---|
| 打刻(PHASE1) | お名前 | テキスト(必須・自動入力) | 山田太郎 |
| 区分 | 選択(必須) | 出勤/退勤(休憩フォームは 休憩開始/休憩終了) | |
| 日報 | お名前 | テキスト(必須・自動入力) | 山田太郎 |
| 記録の種類 | 選択(必須) | 打ち合わせ/成約報告/報告事項 | |
| 相手 | テキスト | 山田様(打ち合わせ) | |
| 時間(分) | 数値 | 90(打ち合わせ) | |
| 内容 | テキスト | 白金台マンション案内(打ち合わせ) | |
| 物件名 | テキスト | 〇〇マンション101(成約) | |
| 種別 | 選択 | 売買/賃貸(成約) | |
| 金額(円) | 数値 | 32000000(成約・数字のみ) | |
| 連絡事項 | テキスト | 田中様 再連絡希望(報告) | |
| 翌日の出勤予定 | テキスト | 09:30(報告) | |
| 翌日の退出予定 | テキスト | 19:00(報告) | |
| 翌日の予定 | テキスト | 午前 内見2件(報告) | |
| 有給休暇届 | お名前 | テキスト(必須・自動入力) | 山田太郎 |
| 取得日 | 日付(必須) | 2025/06/20 | |
| 種類 | 選択(必須) | 全日/午前半休/午後半休 | |
| 備考 | テキスト(任意) | 理由は任意(記載を求めない) | |
| 修正申請 | お名前 | テキスト(必須・自動入力) | 山田太郎 |
| 対象日 | 日付(必須) | 2025/06/18 | |
| 項目 | 選択(必須) | 出勤/退勤/休憩 | |
| 正しい時刻・内容 | テキスト(必須) | 9:00(休憩なら分数:60) |
エルメのステップ配信は「タグが付いた時」を開始条件に、N時間後・翌日○時といったタイミングで配信やアクションを実行できます。「時刻を指定してタグを一括解除する」機能はないため、日次リセットもステップ配信で行います。各ステップの配信条件でタグを再判定し、不要な人には送らないようにします。
| ステップ配信 | 開始条件 | タイミング | 配信条件(送る人) | 内容・アクション |
|---|---|---|---|---|
| 日次リセット+朝の案内 | タグ「退勤済」追加 | 翌日 5:00 | スタッフ_在籍 あり | アクション:「出勤中」「退勤済」「休憩中」「日報_提出済」を解除。メッセージ「おはようございます。本日も出勤時に打刻をお願いします」 |
| 退勤打刻忘れ | タグ「出勤中」追加 | 12時間後 | 「出勤中」あり | 「退勤打刻がまだのようです。退勤済みの場合は正しい時刻で修正申請をお願いします」 |
| 休憩戻り忘れ | タグ「休憩中」追加 | 90分後 | 「休憩中」あり | 「休憩終了の打刻がまだです。戻っている場合は『休憩終了』を送信してください」 |
| 日報未提出 | タグ「退勤済」追加 | 30分後 | 「日報_提出済」なし | 「本日の日報が未提出です。『📝 日報入力』からお願いします」 |
| 出勤打刻なし(管理者向け) | GASの時間トリガー(毎日10:30) | 在籍者のうち当日の「出勤」回答がなく、有給の取得日でもない人を管理者へメール(PHASE 4 §8) | ||
| 想定ケース | システムの動作 |
|---|---|
| 入力形式が違う(金額に文字など) | フォームの形式を「数値」「日付」にしておき、送信時にフォーム側で弾く |
| 出勤前に「休憩開始」を送る | 回答後アクションの条件(「出勤中」なし)で「先に出勤打刻をしてください」と返す |
| 同じ有給を二重申請 | 「有給_申請中」ありなら「申請中の届があります」と返し、管理者には通知しない |
| 氏名の表記ゆれ(社員マスタと不一致) | GASは転記せず、月次集計メールに「マスタにない名前」として列挙。管理者が友だち情報 staff_name を修正する |
| 未実装内容を選択 | 「この機能は準備中です」と仮応答(残機能がある場合) |
| 管理者が長時間未承認 | GASの朝のメールに「修正_申請中/有給_申請中」の未処理件数を載せる(台帳の承認者欄が空の行を数える) |
PHASE 4 への引き継ぎ事項
PHASE 3で「LINE上の操作がフォーム回答として記録される仕組み」が完成しました。次のPHASE 4では以下を構築します。
① 4枚の連携シート(打刻/日報/有給/修正申請)をGASが読み、日報データへ転記する処理(時間トリガー・10分おき)。
② 実働時間の計算と、1か月単位変形労働時間制の時間外判定(日→週→月の三段階)、歩合給を含む割増賃金の計算。
③ 修正履歴の承認分を実データへ反映する処理と、管理者への未打刻・集計メール。
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
連携シート→台帳の自動転記・実働時間計算・変形労働時間制の時間外判定・歩合給の割増賃金の実装仕様
目的:PHASE 3でフォーム回答として記録した打刻・日報・有給・修正データを、GASが連携シートから読み取って日報データへ転記し、実働時間の計算と1か月単位変形労働時間制の時間外判定(日→週→月)、歩合給を含む割増賃金の計算を自動化する。
これが完成すると、スタッフがLINEで操作するだけで月次の勤怠集計が自動で出来上がります。
◎ メリット:エルメ側の設定はフォーム連携だけ。Webアプリの公開やトークン管理が不要で、権限は台帳側に閉じる。
△ 注意:反映は最大10分遅れ。氏名で社員を照合するため、フォームの「お名前」を友だち情報から自動入力して表記ゆれを防ぐ。
PHASE 3で定義したフォームの設問(連携シートの見出し)を、PHASE 2の「日報データ」シートの列へ対応させます。GASは見出し名で読み、列番号で書きます。
| 連携シート/見出し | → 日報データの列 | 型/加工 |
|---|---|---|
| 打刻/回答日時 | A:日付 | 0〜5時の打刻は前日の勤務日として扱う |
| (社員マスタ照合) | B:社員ID | 「お名前」でマスタB列を照合(空白を除いて比較) |
| 打刻/お名前 | C:氏名 | マスタの正式名称で補完 |
| 打刻/区分=出勤の回答日時 | D:出勤 | その日の最初の「出勤」 |
| 打刻/区分=退勤の回答日時 | E:退勤 | その日の最後の「退勤」 |
| 打刻/休憩開始〜休憩終了 | F:休憩(分) | 開始→終了の組を順に合計 |
| (GAS計算) | G:実働(分) | §4①で算出 |
| (GAS計算) | H:深夜(分) | §4②で算出 |
| 日報/相手・時間(分)・内容 | I:打ち合わせ | 連結して格納(同日複数は「/」区切り) |
| 日報/物件名・種別 | J:成約内容 | 連結して格納(表示用) |
| 日報/連絡事項 | K:報告事項 | そのまま |
| 日報/翌日の出勤予定 | L:翌日出勤 | そのまま |
| 日報/翌日の退出予定 | M:翌日退出 | そのまま |
| 日報/翌日の予定 | N:翌日予定 | そのまま |
| 日報/金額(円) | O:成約額 | 同日の合計(歩合給の集計用) |
| (GAS管理) | P:状態 | 「自動」または「修正済」。修正済の日は自動転記で上書きしない |
| 設定場所 | キー/列 | 内容 |
|---|---|---|
| スクリプト プロパティ | PUNCH_SHEET_ID | フォーム「打刻」の連携シートのファイルID(休憩フォームも同じ連携先にする) |
| スクリプト プロパティ | REPORT_SHEET_ID | フォーム「日報」の連携シートのファイルID |
| スクリプト プロパティ | LEAVE_SHEET_ID | フォーム「有給休暇届」の連携シートのファイルID |
| スクリプト プロパティ | NOTIFY_EMAIL | 管理者のメールアドレス(未打刻・最低賃金・集計の通知先) |
| 社員マスタ | L列:月平均所定労働時間 | 固定給の割増単価の分母(例:171.0)。年間所定労働時間÷12 |
// ===== ① 設定・共通(勤怠データ_2025 のApps Scriptに貼り付け) ===== // スクリプト プロパティ(プロジェクトの設定 → スクリプト プロパティ) // PUNCH_SHEET_ID : 打刻フォームの連携シート(ファイルID) // REPORT_SHEET_ID : 日報フォームの連携シート(ファイルID) // LEAVE_SHEET_ID : 有給休暇届フォームの連携シート(ファイルID) // NOTIFY_EMAIL : 管理者のメールアドレス const P = PropertiesService.getScriptProperties(); const TZ = 'Asia/Tokyo'; const SHEETS = { daily: '日報データ', monthly: '月次集計', cal: '変形労働カレンダー', master: '社員マスタ', fix: '修正履歴' }; // 日報データの列番号(PHASE 2 §2 と一致させる) const D = { date: 1, id: 2, name: 3, inT: 4, outT: 5, brk: 6, work: 7, night: 8, mtg: 9, deal: 10, note: 11, nIn: 12, nOut: 13, nPlan: 14, amount: 15, state: 16 }; // 連携シートの見出し(フォームの設問文に合わせて変更) const H = { time: '回答日時', name: 'お名前', kind: '区分', // 日報フォーム rType: '記録の種類', mtgWho: '相手', mtgMin: '時間(分)', mtgWhat: '内容', prop: '物件名', dealType: '種別', amount: '金額(円)', note: '連絡事項', nIn: '翌日の出勤予定', nOut: '翌日の退出予定', nPlan: '翌日の予定', // 有給休暇届フォーム leaveDate: '取得日' }; // 月の法定総枠(本設計の設定値。厳密な上限は 177.1 / 171.4 / 165.7 / 160.0) const LEGAL_CAP = { 31: 177.0, 30: 171.0, 29: 165.5, 28: 160.0 }; const WEEK_START = 0; // 週の起算日:0=日曜(就業規則に合わせる) const MIN_WAGE = 1200; // 事業場の最低賃金(円/時)。改定時に更新 function ss_() { return SpreadsheetApp.getActiveSpreadsheet(); } function sh_(name) { return ss_().getSheetByName(name); } function fmt_(d, f) { return Utilities.formatDate(new Date(d), TZ, f); } function norm_(s) { return String(s || '').replace(/[\s ]/g, ''); } function mail_(subject, body) { const to = P.getProperty('NOTIFY_EMAIL'); if (to) MailApp.sendEmail(to, subject, body); } // 連携シートを見出し名で読み、1行=1オブジェクトにする function readSheet_(id) { if (!id) return []; const v = SpreadsheetApp.openById(id).getSheets()[0].getDataRange().getValues(); if (v.length < 2) return []; const head = v[0].map(String); return v.slice(1).map(function (row) { const o = {}; head.forEach(function (k, i) { o[k] = row[i]; }); return o; }); } // 社員マスタ:氏名 → { id, pay, fixedPay, stdHours, commissionRate } function master_() { const m = {}; sh_(SHEETS.master).getDataRange().getValues().slice(1).forEach(function (r) { if (!r[1]) return; m[norm_(r[1])] = { id: r[0], name: r[1], payType: r[7], fixedPay: Number(r[8]) || 0, rate: Number(r[9]) || 0, guarantee: Number(r[10]) || 0, stdHours: Number(r[11]) || 0 }; }); return m; } // 【トリガー】10分おき:打刻・日報・修正をまとめて反映 function syncAll() { syncPunches(); syncReports(); applyFixes(); }
// ===== ② 打刻の転記(連携シート「打刻」→ 日報データ) ===== // 直近3日分を毎回作り直す(何度動かしても同じ結果になる)。修正済の日は触らない function syncPunches() { const daily = sh_(SHEETS.daily); const master = master_(); const since = new Date(); since.setDate(since.getDate() - 3); const byKey = {}; readSheet_(P.getProperty('PUNCH_SHEET_ID')).forEach(function (r) { const t = new Date(r[H.time]); if (isNaN(t.getTime()) || t < since) return; // 深夜0〜5時の打刻は前日の勤務として扱う const workDate = new Date(t); if (t.getHours() < 5) workDate.setDate(workDate.getDate() - 1); const key = norm_(r[H.name]) + '|' + fmt_(workDate, 'yyyy/MM/dd'); (byKey[key] = byKey[key] || []).push({ t: t, kind: String(r[H.kind]) }); }); Object.keys(byKey).forEach(function (key) { const parts = key.split('|'); const m = master[parts[0]]; if (!m) return; // マスタにない名前は無視(表記ゆれは管理者が確認) const ps = byKey[key].sort(function (a, b) { return a.t - b.t; }); let inT = null, outT = null, brkStart = null, brkMin = 0; ps.forEach(function (p) { if (p.kind === '出勤' && !inT) inT = p.t; // 最初の出勤 if (p.kind === '退勤') outT = p.t; // 最後の退勤 if (p.kind === '休憩開始') brkStart = p.t; if (p.kind === '休憩終了' && brkStart) { brkMin += (p.t - brkStart) / 60000; brkStart = null; } }); const row = findDailyRow_(daily, parts[1], m.name); if (daily.getRange(row, D.state).getValue() === '修正済') return; daily.getRange(row, D.date).setValue(parts[1]); daily.getRange(row, D.id).setValue(m.id); daily.getRange(row, D.name).setValue(m.name); if (inT) daily.getRange(row, D.inT).setValue(fmt_(inT, 'HH:mm')); if (outT) daily.getRange(row, D.outT).setValue(fmt_(outT, 'HH:mm')); daily.getRange(row, D.brk).setValue(Math.round(brkMin)); if (inT && outT) { const w = calcWorkMinutes_(inT, outT, Math.round(brkMin)); daily.getRange(row, D.work).setValue(w.work); daily.getRange(row, D.night).setValue(w.night); } daily.getRange(row, D.state).setValue('自動'); }); } // 日報データで(日付, 氏名)の行を探す。なければ末尾に追加して行番号を返す function findDailyRow_(daily, dateStr, name) { const v = daily.getDataRange().getValues(); for (let i = 1; i < v.length; i++) { if (fmt_(v[i][0], 'yyyy/MM/dd') === dateStr && norm_(v[i][2]) === norm_(name)) return i + 1; } return daily.getLastRow() + 1; } // 実働(分)と深夜(22:00〜翌5:00)の分数 function calcWorkMinutes_(inT, outT, breakMin) { const gross = (outT - inT) / 60000; let night = 0; for (let t = new Date(inT); t < outT; t.setMinutes(t.getMinutes() + 1)) { const h = t.getHours(); if (h >= 22 || h < 5) night++; } return { work: Math.max(0, Math.round(gross - breakMin)), night: night }; }
// ===== ③ 日報の転記(連携シート「日報」→ 日報データ I〜O 列) ===== function syncReports() { const daily = sh_(SHEETS.daily); const master = master_(); const since = new Date(); since.setDate(since.getDate() - 3); const byKey = {}; readSheet_(P.getProperty('REPORT_SHEET_ID')).forEach(function (r) { const t = new Date(r[H.time]); if (isNaN(t.getTime()) || t < since) return; const key = norm_(r[H.name]) + '|' + fmt_(t, 'yyyy/MM/dd'); (byKey[key] = byKey[key] || []).push(r); }); Object.keys(byKey).forEach(function (key) { const parts = key.split('|'); const m = master[parts[0]]; if (!m) return; const mtg = [], deal = [], note = []; let amount = 0, nIn = '', nOut = '', nPlan = ''; byKey[key].forEach(function (r) { const type = String(r[H.rType] || ''); if (type === '打ち合わせ') mtg.push(r[H.mtgWho] + ' ' + r[H.mtgMin] + '分 ' + r[H.mtgWhat]); if (type === '成約報告') { deal.push(r[H.prop] + ' ' + r[H.dealType]); amount += Number(r[H.amount]) || 0; } if (type === '報告事項') { note.push(String(r[H.note] || '')); nIn = r[H.nIn] || nIn; nOut = r[H.nOut] || nOut; nPlan = r[H.nPlan] || nPlan; } }); const row = findDailyRow_(daily, parts[1], m.name); daily.getRange(row, D.date).setValue(parts[1]); daily.getRange(row, D.id).setValue(m.id); daily.getRange(row, D.name).setValue(m.name); daily.getRange(row, D.mtg).setValue(mtg.join('/')); daily.getRange(row, D.deal).setValue(deal.join('/')); daily.getRange(row, D.note).setValue(note.join('/')); daily.getRange(row, D.nIn).setValue(nIn); daily.getRange(row, D.nOut).setValue(nOut); daily.getRange(row, D.nPlan).setValue(nPlan); daily.getRange(row, D.amount).setValue(amount); }); }
打刻の転記時(syncPunches)に当日の実働時間を計算し、日報データのG列(実働)とH列(深夜)へ書き込みます。
本システムの核心です。1か月単位変形労働時間制では、時間外労働を日 → 週 → 月の三段階で判定します。「月の総枠を超えた分だけ」を残業とする方式は、所定が8時間を超える日の超過分を取りこぼし、未払いになるので採用できません。
それ以外の日 → 8hを超えた時間
それ以外の週 → 40hを超えた時間(③-1で数えた分を除く)
| 月の暦日数 | 本設計の設定値 | 厳密な上限(40h×暦日数÷7) | 該当月の例 |
|---|---|---|---|
| 31日 | 177.0 時間 | 177.1 時間 | 1・3・5・7・8・10・12月 |
| 30日 | 171.0 時間 | 171.4 時間 | 4・6・9・11月 |
| 29日(うるう年2月) | 165.5 時間 | 165.7 時間 | 2月(うるう年) |
| 28日(平年2月) | 160.0 時間 | 160.0 時間 | 2月(平年) |
// ===== ⑤ 1か月単位変形労働時間制の残業判定(日→週→月)と割増賃金 ===== // cal:{ 'yyyy/MM/dd': { std: 所定(h), legalHoliday: true/false } } function calcOvertime_(rows, cal, daysInMonth) { let dayOt = 0, weekOt = 0, holiday = 0, total = 0; const weeks = {}; rows.forEach(function (r) { const dateStr = fmt_(r.date, 'yyyy/MM/dd'); const c = cal[dateStr] || { std: 0, legalHoliday: false }; const h = r.work / 60; if (c.legalHoliday) { holiday += h; return; } // 法定休日労働は別枠(3割5分) total += h; const dayLimit = Math.max(8, c.std); // 所定が8h超の日はその所定、それ以外は8h const ot = Math.max(0, h - dayLimit); dayOt += ot; const wk = weekKey_(new Date(r.date)); weeks[wk] = weeks[wk] || { work: 0, std: 0, dayOt: 0 }; weeks[wk].work += h; weeks[wk].std += c.std; weeks[wk].dayOt += ot; }); Object.keys(weeks).forEach(function (k) { const w = weeks[k]; const weekLimit = Math.max(40, w.std); // 所定が40h超の週はその所定、それ以外は40h weekOt += Math.max(0, w.work - weekLimit - w.dayOt); }); const cap = LEGAL_CAP[daysInMonth]; const monthOt = Math.max(0, total - cap - dayOt - weekOt); return { total: total, holiday: holiday, dayOt: dayOt, weekOt: weekOt, monthOt: monthOt, overtime: dayOt + weekOt + monthOt }; } // 週の起算日(WEEK_START)でそろえた週キー function weekKey_(d) { const x = new Date(d); x.setDate(x.getDate() - ((x.getDay() - WEEK_START + 7) % 7)); return fmt_(x, 'yyyy/MM/dd'); } // 固定給分(月平均所定労働時間で割る)+歩合給分(その月の総労働時間で割る) function calcPremium_(m, ot, holidayH, nightH, commission, totalH) { const fixedUnit = m.stdHours ? m.fixedPay / m.stdHours : 0; const commUnit = totalH ? commission / totalH : 0; return { fixedUnit: fixedUnit, commUnit: commUnit, overtime: fixedUnit * ot * 1.25 + commUnit * ot * 0.25, holiday: fixedUnit * holidayH * 1.35 + commUnit * holidayH * 0.35, night: (fixedUnit + commUnit) * nightH * 0.25, total: fixedUnit * ot * 1.25 + commUnit * ot * 0.25 + fixedUnit * holidayH * 1.35 + commUnit * holidayH * 0.35 + (fixedUnit + commUnit) * nightH * 0.25 }; }
不動産営業職のように固定給+歩合給で支払う場合、時間外労働の割増賃金は固定給部分と歩合給部分を分けて計算します(労働基準法施行規則19条1項6号)。ここを1本の式で済ませてしまうのが、歩合給のある会社で最も多い設計ミスです。
固定給分の割増 = 時間単価 × 残業時間 × 1.25
歩合給分の割増 = 時間単価 × 残業時間 × 0.25
深夜:(固定給単価 + 歩合給単価)× 深夜時間 × 0.25
日報データを集計し、月次集計シートの当月分を毎晩23時の時間トリガーで作り直します(当月の行を削除して書き直すので、何度実行しても同じ結果になります)。
| 集計項目 | 算出方法 | 出力先(月次集計シート) |
|---|---|---|
| 出勤日数 | 当月の実働がある日数 | D:出勤日数 |
| 総労働時間 | 当月の実働(分)を合算し60で除算(法定休日労働を含む) | E:実働(h) |
| 法定総枠 | 暦日数から総枠を取得(§5) | F:法定総枠(h) |
| 時間外労働 | §5の三段階判定(日+週+月) | G:残業(h) |
| 深夜労働 | §4②を合算 | H:深夜(h) |
| 打ち合わせ/成約 | 当月の記録がある日数 | I・J:件数 |
| 成約額 合計 | 当月の成約額を合算 | K:成約額 |
| 歩合給 | 成約額 × 歩合率(給与規程で確定した額に手で差し替え可) | L:歩合給 |
| 固定給 | 社員マスタI列 | M:固定給 |
| 歩合給の時間単価 | 歩合給 ÷ 総労働時間(§5⑤) | N:時間単価 |
| 割増賃金 | 固定給分1.25+歩合給分0.25+休日・深夜(§5⑦) | O:割増賃金 |
| 法定休日労働 | カレンダーG列「○」の日の実働 | P:休日(h) |
| 最低賃金の検算 | (固定給+歩合給)÷ 総労働時間 ≧ 最低賃金 | Q:最低賃金判定(下回れば管理者へメール) |
// ===== ⑥ 月次集計(毎晩23時のトリガーで当月分を更新) ===== function aggregateMonthly() { const now = new Date(); const ym = fmt_(now, 'yyyy-MM'); const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); const master = master_(); const cal = {}; sh_(SHEETS.cal).getDataRange().getValues().slice(1).forEach(function (r) { if (r[0]) cal[fmt_(r[0], 'yyyy/MM/dd')] = { std: Number(r[5]) || 0, legalHoliday: String(r[6]).trim() === '○' }; }); const byStaff = {}; sh_(SHEETS.daily).getDataRange().getValues().slice(1).forEach(function (r) { if (!r[0] || fmt_(r[0], 'yyyy-MM') !== ym || !r[D.work - 1]) return; const k = norm_(r[D.name - 1]); (byStaff[k] = byStaff[k] || []).push({ date: r[0], work: Number(r[D.work - 1]) || 0, night: Number(r[D.night - 1]) || 0, mtg: r[D.mtg - 1], deal: r[D.deal - 1], amount: Number(r[D.amount - 1]) || 0 }); }); const out = []; const alerts = []; Object.keys(byStaff).forEach(function (k) { const m = master[k]; if (!m) return; const rows = byStaff[k]; const ot = calcOvertime_(rows, cal, daysInMonth); const nightH = rows.reduce(function (s, r) { return s + r.night; }, 0) / 60; const mtgCnt = rows.filter(function (r) { return r.mtg; }).length; const dealCnt = rows.filter(function (r) { return r.deal; }).length; const amount = rows.reduce(function (s, r) { return s + r.amount; }, 0); const commission = Math.round(amount * m.rate); // 歩合給(給与規程で確定した額に手で差し替え可) const totalH = ot.total + ot.holiday; const prem = calcPremium_(m, ot.overtime, ot.holiday, nightH, commission, totalH); const hourly = totalH ? (m.fixedPay + commission) / totalH : 0; const minOk = hourly >= MIN_WAGE ? 'OK' : '要確認'; if (minOk !== 'OK') alerts.push(m.name + ':時間額 ' + Math.round(hourly) + '円'); out.push([ym, m.id, m.name, rows.length, round1_(totalH), LEGAL_CAP[daysInMonth], round1_(ot.overtime), round1_(nightH), mtgCnt, dealCnt, amount, commission, m.fixedPay, Math.round(prem.commUnit), Math.round(prem.total), round1_(ot.holiday), minOk]); }); const sheet = sh_(SHEETS.monthly); const v = sheet.getDataRange().getValues(); for (let i = v.length - 1; i >= 1; i--) { if (v[i][0] === ym) sheet.deleteRow(i + 1); } if (out.length) sheet.getRange(sheet.getLastRow() + 1, 1, out.length, out[0].length).setValues(out); if (alerts.length) mail_('【勤怠】最低賃金の要確認 ' + ym, alerts.join('\n')); } function round1_(x) { return Math.round(x * 10) / 10; }
// ===== ④ 修正申請の反映(修正履歴シートで管理者が承認した行を日報データへ) ===== // 修正履歴:A申請日時 B対象社員 C対象日 D項目 E修正前 F修正後 G申請者 H承認者 I反映日時 function applyFixes() { const fix = sh_(SHEETS.fix); const daily = sh_(SHEETS.daily); const v = fix.getDataRange().getValues(); for (let i = 1; i < v.length; i++) { if (!v[i][7] || v[i][8]) continue; // 未承認、または反映済みは飛ばす const dateStr = fmt_(v[i][2], 'yyyy/MM/dd'); const row = findDailyRow_(daily, dateStr, v[i][1]); const col = { '出勤': D.inT, '退勤': D.outT, '休憩': D.brk }[String(v[i][3])]; if (!col) continue; fix.getRange(i + 1, 5).setValue(daily.getRange(row, col).getValue()); // 修正前を保存 daily.getRange(row, col).setValue(v[i][5]); // 修正後を反映 daily.getRange(row, D.date).setValue(dateStr); daily.getRange(row, D.name).setValue(v[i][1]); daily.getRange(row, D.state).setValue('修正済'); recalcDailyRow_(daily, row); fix.getRange(i + 1, 9).setValue(new Date()); } } // 1行分の実働・深夜を出勤・退勤・休憩から再計算する function recalcDailyRow_(daily, row) { const r = daily.getRange(row, 1, 1, D.state).getValues()[0]; if (!r[D.inT - 1] || !r[D.outT - 1]) return; const base = fmt_(r[0], 'yyyy/MM/dd'); const inT = new Date(base + ' ' + fmt_(r[D.inT - 1], 'HH:mm')); const outT = new Date(base + ' ' + fmt_(r[D.outT - 1], 'HH:mm')); if (outT <= inT) outT.setDate(outT.getDate() + 1); // 日付をまたぐ勤務 const w = calcWorkMinutes_(inT, outT, Number(r[D.brk - 1]) || 0); daily.getRange(row, D.work).setValue(w.work); daily.getRange(row, D.night).setValue(w.night); }
// ===== ⑦ 未打刻アラート(毎日10:30のトリガー:出勤打刻がない在籍者を管理者へメール) ===== function alertUnpunched() { const today = fmt_(new Date(), 'yyyy/MM/dd'); const punched = new Set( readSheet_(P.getProperty('PUNCH_SHEET_ID')) .filter(function (r) { return fmt_(r[H.time], 'yyyy/MM/dd') === today && String(r[H.kind]) === '出勤'; }) .map(function (r) { return norm_(r[H.name]); }) ); const onLeave = new Set( readSheet_(P.getProperty('LEAVE_SHEET_ID')) .filter(function (r) { return fmt_(r[H.leaveDate], 'yyyy/MM/dd') === today; }) .map(function (r) { return norm_(r[H.name]); }) ); const cal = sh_(SHEETS.cal).getDataRange().getValues().slice(1) .filter(function (r) { return r[0] && fmt_(r[0], 'yyyy/MM/dd') === today; })[0]; if (cal && String(cal[2]) === '休日') return; // 会社の休日は送らない const missing = Object.keys(master_()).filter(function (k) { return !punched.has(k) && !onLeave.has(k); }); if (missing.length) mail_('【勤怠】出勤打刻なし ' + today + '(' + missing.length + '名)', missing.join('\n')); }
| 関数 | イベント | タイミング | 役割 |
|---|---|---|---|
| syncAll | 分ベースのタイマー | 10分おき | 打刻・日報・修正の転記 |
| alertUnpunched | 日付ベースのタイマー | 午前10時〜11時 | 出勤打刻がない在籍者を管理者へメール(有給取得日・会社休日は除外) |
| aggregateMonthly | 日付ベースのタイマー | 午後11時〜12時 | 当月の月次集計を作り直し、最低賃金の要確認者をメール |
| テスト項目 | 確認内容 |
|---|---|
| 打刻の転記 | 出勤→退勤のフォーム送信で日報データに正しい時刻・実働が入る |
| 休憩除外 | 休憩開始→終了の分が実働から正しく差し引かれる |
| 変形労働の時間外 | 日8h超・週40h超・月総枠超のそれぞれが取りこぼしなく計上される |
| 法定休日 | G列「○」の日の労働が時間外でなく休日労働として別計上される |
| 月またぎ | 月末→月初で集計が正しくリセットされる |
| うるう年2月 | 29日の月で総枠165.5hが適用される |
| 深夜労働 | 22時以降の労働が深夜列に別集計される |
| 修正申請 | 承認後にデータが変わり、再計算され、履歴に反映日時が残る。以後の自動転記で戻らない |
| 二重送信 | 同じ区分を2回送っても最初の出勤・最後の退勤だけが採用される |
| 表記ゆれ | マスタにない氏名が転記されず、管理者が気づける |
PHASE 5 への引き継ぎ事項
PHASE 4で「LINEの操作が自動で集計されるエンジン」が完成しました。次のPHASE 5(テスト・修正)では、上記§8のテスト観点に沿って実データでの動作確認を行い、計算のズレ・月またぎ・うるう年・深夜労働などのエッジケースを検証・修正します。
その後、PHASE 6でスタッフへの説明と本番運用に進みます。
引き継ぐもの:①GASのコードとトリガー設定 ②連携シートのファイルIDと台帳の共有設定 ③テスト結果の記録。
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
本番前の動作確認・エッジケース検証・不具合修正の進め方
目的:PHASE 1〜4で構築したシステムを本番運用前に総点検します。LINEの操作・データの書き込み・実働時間と変形労働残業の計算・各種配信が、実際のデータですべて正しく動くかを検証し、見つかった不具合を直します。
特に計算ロジック(変形労働時間制の残業判定)と月またぎ・うるう年などのエッジケースを重点的に確認します。
| 準備物 | 内容 |
|---|---|
| テスト用LINEアカウント | スタッフ役・管理者役の2つ以上(本人のLINEで可) |
| テスト用データ | 過去の勤務実績をモデルにした打刻パターン(残業あり/なし両方) |
| テストシート | 本番とは別の検証用スプレッドシート(誤記録防止) |
| 期待値の早見表 | 各テストの「正解の数字」を事前に手計算で用意 |
| 不具合管理表 | §6のフォーマット(発見→修正→再テストを記録) |
各ボタン・各シナリオが想定どおり動くかを1つずつ確認します。
| ID | テスト項目 | 操作 | 期待結果 |
|---|---|---|---|
| T-01 | 出勤打刻 | 出退勤ボタンをタップ | 出勤時刻が記録され「出勤中」タグが付く |
| T-02 | 退勤打刻 | 出勤中に再タップ→確認 | 退勤時刻記録・実働表示・タグが退勤済に |
| T-03 | 休憩トグル | 休憩ボタンを2回 | 開始・終了が記録され休憩分が算出 |
| T-04 | 日報・打ち合わせ | 日報入力→打ち合わせ | 相手/時間/内容が保存される |
| T-05 | 日報・成約報告 | 日報入力→成約報告 | 物件/種別/金額が保存される |
| T-06 | 日報・報告事項+翌日 | 日報入力→報告事項 | 連絡と翌日予定(出勤/退出/予定)が保存 |
| T-07 | 有給休暇届 | 申請→管理者承認 | 通知→承認→本人返信、タグ切替 |
| T-08 | 修正申請 | 打刻種別選択→申請 | 管理者承認でデータ反映・履歴記録 |
| T-09 | スプレッドシート転記 | 各操作後に連携シート→10分後に台帳を確認 | 連携シートに1行入り、項目マッピングどおり日報データに転記される |
実働時間と変形労働時間制の残業判定が正確かを、手計算した期待値と照合します。本システムの心臓部です。
| ID | テスト項目 | テストデータ例 | 期待値 |
|---|---|---|---|
| T-10 | 基本の実働計算 | 9:00出勤・18:30退勤・休憩60分 | 実働 8時間30分 |
| T-11 | 複数休憩の除外 | 休憩を午前午後で計90分 | 90分が実働から除外 |
| T-12 | 残業なし(総枠未満) | 30日の月・各日所定内・月実働168.5h | 残業 0h(日・週の超過なし、171.0h未満) |
| T-12b | 日単位の時間外 | 所定8hの日に9h勤務×21日(189h) | 残業 21.0h(日単位1h×21。月総枠超過としては数えない) |
| T-12c | 法定休日労働 | カレンダーG列「○」の日に6h勤務 | 時間外に含めず、法定休日(h)に6.0h(3割5分) |
| T-13 | 残業あり(総枠超過) | 30日の月・各日所定内で出勤日数が多く月実働180.0h | 残業 9.0h(180.0−171.0) |
| T-14 | 31日の月の総枠 | 31日の月・月実働185.0h | 残業 8.0h(185.0−177.0) |
| T-15 | 平年2月の総枠 | 28日・月実働165.0h | 残業 5.0h(165.0−160.0) |
| T-16 | うるう年2月の総枠 | 29日・月実働170.0h | 残業 4.5h(170.0−165.5) |
| T-17 | 深夜労働の別集計 | 22:00以降に2時間勤務 | 深夜2hが深夜列に別計上 |
| T-18 | 歩合給の割増賃金 | 歩合給30万円・総労働時間180h・残業9h | 時間単価1,667円→割増 約3,750円(×0.25) |
| T-19 | 固定給+歩合給の合算 | 固定給22万円(所定171h)+上記の歩合 | 固定給分(×1.25)と歩合分(×0.25)が合算される |
通常運用では起きにくいが、起きると影響が大きいケースを重点的に確認します。
| ID | エッジケース | 確認内容 |
|---|---|---|
| T-20 | 月またぎ | 月末→月初で集計がリセットされ、繰り越されない |
| T-21 | うるう年の切替 | 2月29日のある年で総枠165.5hが適用される |
| T-22 | 日付をまたぐ勤務 | 23:00出勤→翌2:00退勤で実働・深夜が正しい |
| T-23 | 出勤打刻のし忘れ | 10:30に管理者へメールが届く+修正申請で後から訂正できる |
| T-24 | 退勤打刻のし忘れ | 退勤リマインド+翌日に修正申請で訂正 |
| T-25 | 二重送信 | 「出勤」を2回送っても案内が返り、台帳には最初の出勤だけが採用される |
| T-26 | 入力形式エラー | 金額に文字を入れるとフォーム側で送信できない |
| T-27 | 有給取得当日 | 有給の取得日は未打刻アラートの対象から外れる |
| T-28 | 同時打刻 | 複数人が同時刻に打刻しても取りこぼさない |
| T-29 | 無料プラン上限 | 月の配信通数が上限に近づいたら把握できる |
実際のスタッフ数名に1〜2日使ってもらい、現場で無理なく使えるかを確認します。机上では気づけない使い勝手の問題を洗い出します。
| ID | 観点 | 確認内容 |
|---|---|---|
| T-30 | 分かりやすさ | 説明なしでも打刻・日報入力ができるか |
| T-31 | 入力の手間 | 外回り中でも負担なく入力できるか |
| T-32 | 文面の自然さ | 自動メッセージが分かりにくくないか |
| T-33 | リマインドの頻度 | 多すぎ・少なすぎないか |
| T-34 | 管理者の確認 | 管理者が日次・月次を無理なく確認できるか |
見つかった不具合は、深刻度を付けて一覧で管理します。記録はスプレッドシートやメモで構いません。
| 深刻度 | 意味 | 対応 |
|---|---|---|
| A 重大 | 計算ミス・データ消失など給与に影響 | 本番前に必ず修正 |
| B 中 | 動くが分かりにくい・手間がかかる | 本番前に修正推奨 |
| C 軽微 | 表記ゆれ・見た目の小さな問題 | 運用しながら改善可 |
| No | 関連ID | 内容 | 深刻度 | 状態 |
|---|---|---|---|---|
| B-01 | T-16 | うるう年2月の残業が誤って160hで計算される | A | 修正済→再テストOK |
| B-02 | T-32 | 打ち合わせ入力の例文が分かりにくい | B | 修正中 |
| B-03 | T-26 | 時刻エラー時の文面が冷たい印象 | C | 未対応 |
PHASE 6 への引き継ぎ事項
PHASE 5で「正しく動くことを確認したシステム」が完成しました。深刻度Aの不具合が0件になり、本番GOの判定が出たら、いよいよPHASE 6(スタッフ説明・本番運用)へ進みます。
引き継ぐもの:①テスト結果の記録と残課題(C軽微の改善リスト) ②本番接続の手順 ③運用テストで集めたスタッフの声(説明資料づくりに活用)。
本番では検証シートから本番シートへ切り替え、最初の1か月は計算結果を給与計算と二重チェックすると安心です。
詳細設計図
不動産営業職 歩合給対応 勤怠管理システム
本番への切り替え・スタッフ説明会・安定運用までの最終フェーズ
目的:PHASE 5でテスト合格したシステムを本番に切り替え、スタッフへ使い方を説明して、毎日無理なく使える状態にします。これが導入の最終フェーズです。
ゴールは「全スタッフがLINEで打刻・日報入力を習慣化し、管理者が月次集計を確認できている」状態です。
| 順 | 作業 | 内容・注意点 |
|---|---|---|
| 1 | 本番シートの用意 | テスト用と別の本番スプレッドシートを用意し、見出し・社員マスタ・変形労働カレンダーを正式版で入力 |
| 2 | GAS接続先の切替 | 本番の台帳にGASを置き、スクリプトプロパティの連携シートID・通知先メールを本番用に確定 |
| 3 | テストデータの除去 | 検証中に入った仮データが本番シートに混ざっていないか確認・削除 |
| 4 | 最終疎通テスト | 本番環境で1人分の打刻→記録→集計を1サイクル確認 |
| 5 | 権限・共有の最終確認 | 本番シートの共有範囲を管理者のみに(共有設定はご本人が操作) |
説明会は「その場で全員が実際に打刻してみる」のが定着の近道です。資料を配るだけでなく、手を動かしてもらいます。
| タイミング | やること |
|---|---|
| 出社したら | 「🔄 出退勤」をタップ(出勤打刻) |
| 休憩のとき | 「☕ 休憩」をタップ(戻ったらもう一度) |
| 打ち合わせ・成約のあと | 「📝 日報入力」から種別を選んで入力 |
| 帰るとき | 「🔄 出退勤」をタップ→「退勤する」 |
| 打刻を忘れたとき | 「⚠️ 修正申請」から正しい時刻を申請 |
運用前に、会社として以下のルールを決めて全員に共有します。曖昧だと打刻のばらつきや労務トラブルの原因になります。
| 項目 | 決めること(例) |
|---|---|
| 打刻のタイミング | 出勤=業務開始時/退勤=業務終了時。移動中の打刻可否も明確に |
| 日報の提出期限 | 当日の退勤打刻まで。打ち合わせ等はその都度入力を推奨 |
| 打刻漏れの対応 | 当日中は本人修正可、翌日以降は修正申請+管理者承認 |
| 修正申請の承認者 | 誰が承認するか(管理者・店長など)を決める |
| 有給申請の締切 | 取得希望日の何日前までに申請するか |
| 直行直帰の扱い | 現地で出勤打刻してよいか等を明確に |
| 歩合給の計算期間 | 成約をどの月の歩合に計上するか(契約日/決済日など)を1つに決める |
| 保障給の水準 | 成約がない月の保障給を定め、最低賃金を下回らないか毎月検算する |
| データの確認頻度 | 管理者が日次・月次でいつ確認するか |
| 記録の保存期間 | 出勤簿・賃金台帳などは5年(当分の間3年)の保存義務があります(労働基準法109条)。連携シートと台帳を年度ごとに保管し、途中で削除しない運用に |
| 退職者のデータ | 退職後もタグ・友だちを即削除せず、保存期間を満たすまでシート側の記録を残す |
| 症状 | 確認・対応 |
|---|---|
| 打刻しても台帳に記録されない | 連携シートに行が入っているか(入っていなければフォームのスプレッドシート連携)、入っていればスクリプトプロパティのIDと見出し名(H)、社員マスタの照合キーを確認 |
| 残業時間がおかしい | 変形労働カレンダーの所定・法定休日「○」と、月の暦日数の総枠(177.0/171.0/165.5/160.0h)、週の起算日(WEEK_START)を確認 |
| リマインドが来ない/多すぎる | 配信時刻とタグ絞り込み条件を確認。無料プランの通数上限も確認 |
| 特定スタッフだけ動かない | 友だち追加・社員マスタの紐付け・属性タグを確認 |
| 月次集計が更新されない | GASの時間トリガーが有効か、エラーログを確認 |
| 時期 | フォロー内容 |
|---|---|
| 最初の1週間 | 毎日、打刻漏れがないか管理者が確認。漏れた人には声かけ+修正申請を案内 |
| 最初の1か月 | システムの集計結果を従来の方法と二重チェックし、計算が合うか確認 |
| 1か月後 | スタッフの声を集め、文面・リマインド頻度・ボタン配置を微調整(PHASE 5のC軽微課題も対応) |
| 3か月後 | 運用が安定。二重チェックを終了し、システムへ完全移行 |
導入完了!不動産営業職 歩合給対応 勤怠管理システム 本番稼働
PHASE 1〜6を経て、LINEで打刻・日報・有給・修正が完結し、
1か月単位変形労働時間制の残業計算まで自動化されたシステムが本番稼働しました。
あとは日々の運用の中で少しずつ磨き込み、現場にフィットさせていきましょう。










