Appearance
シフト制と1ヶ月単位の変形労働時間制
- 状態: 決定済み(2026-08-23 起案、同日ユーザー決定で確定。v0.7 マイルストーン)
- フェーズ1〜4 実装済み(フェーズ4 は 2026-08-24。下記「フェーズ4の決定事項」参照)
- 併走: 有給付与の予告→承認→通知フロー(requirements §11)
なぜシフト制は「制度を1つ足す」では済まないか
フレックス・固定時間制は「所定」がテナント/ポリシー単位の定数だった。シフト制では 所定が user × 日付の可変データになり、KIZAMI に初めて「予定」が入る (現在の全データは打刻=実績)。これが波及する範囲:
- エンジン: 時間外判定が3段になる(後述)
- 休日: 曜日固定の法定休日が成立しなくなる(週1日/4週4日の変形休日制)
- UI: シフト表の作成・配布・本人閲覧、予実の乖離表示
- 集計の前提: 「その日に働くべきだったか」がデータで決まる → 遅刻・早退・欠勤の概念
法的枠組み
シフトで週40時間を超える週を組むには 1ヶ月単位の変形労働時間制(労基法32条の2)が ほぼ必然になる。要件:
- 労使協定または就業規則で定める(KIZAMI はテナント属性として「導入済み」を宣言させる — 協定の締結自体は会社の責任。特別条項と同じ扱い)
- 変形期間(1ヶ月以内)を平均して週40時間以内(特例措置対象事業場は44時間)
- 各日・各週の労働時間をあらかじめ特定する(=シフト表の事前確定が法的要件。 後からの変更は「業務の都合による恣意的変更」と評価されないよう限定的であるべき — シフトの変更履歴を持つ理由)
時間外労働の3段判定(通達 昭63.1.1基発1号 の整理に従う)
| 段 | 判定 | 対象 |
|---|---|---|
| ① 日 | 所定が8h超の日は所定超、所定が8h以下の日は8h超 | その日の実労働 |
| ② 週 | 所定が40h超の週は所定超、40h以下の週は40h超(①で時間外にした分は除く) | 週の実労働 |
| ③ 期間 | 変形期間の法定総枠(40×暦日数/7)超(①②で時間外にした分は除く) | 期間合計 |
固定時間制の二段判定(日→週)の自然な拡張だが、「所定と法定の大きい方」を段ごとに 取る点と、期間段が加わる点が新しい。WorkSystem に第3の派生:
ts
| { kind: "monthly_variable"; /* シフト参照。standardDayMinutes は持たない(日ごとにシフトが決める) */ }エンジンはシフト(所定)を EngineInput の新しいタイムラインとして受け取る (打刻・設定・法令と同じ「入力は全部呼び出し側が集めて渡す」原則を維持。エンジンは DB を知らない)。
データモデル
決定版は下記「決定事項 1」の3テーブル(shift_patterns / shift_plans / shift_days、supersedes 型)。 shift_plans は user 単位(グループ単位は将来。同じパターン列を複数人に適用する操作は UI 側の 一括割当で表現し、データは人ごとに持つ — 変形制の「各人の所定の事前特定」に一致する)。
法定休日の扱い
day_type: legal_holiday をシフト側で指定する(曜日固定ルールからの移行)。 週1日(または4週4日)の充足はエンジンが検証し、満たさないシフトには警告。 振替休日はシフト編集(事前の入替=両日の day_type 交換+履歴)として表現でき、 代休は事後の実績(法定休日労働の割増は消えない)として自然に区別される — リーガルレビュー指摘の「打刻だけでは区別できない」問題はシフトというデータが解く。
予実の突合
シフト(予定)と打刻(実績)の乖離は警告として出す(遅刻・早退・欠勤)。 集計への自動反映はしない(遅刻控除などの賃金処理は給与側の責任 — 金額を計算しない 原則の適用)。乖離の種類:
- シフト日に打刻なし → 欠勤の可能性(打刻忘れリマインドの拡張)
- 開始より遅い出勤 / 終了より早い退勤 → 遅刻・早退(分数を表示)
- 非勤務日に打刻 → 予定外労働(管理者へ通知候補)
有給付与フロー(併走、requirements §11 の決定)
- 付与基準日の事前に管理者へ予告(対象者・算定日数・出勤率参考値)
- 管理者が確認して実行(または承認)
- 本人へ通知(leave_alert)
シフト制では出勤率の分母(全労働日)がシフトから正確に出せるようになる — 固定時間制・フレックスより精度が上がる(これがシフト制と同時期にやる理由)。
決定事項(2026-08-23、ユーザー決定)
1. shift_days の訂正モデル = 確定+追記型の変更履歴
打刻と同じ supersedes 型。シフト表(期間単位)を「確定」し、確定後の変更は新しい行が旧行を supersede する形で積む。「誰がいつ何を変えたか」が構造で残り、変形制の「事前特定」要件と 恣意的変更の可視化に適う。版管理(effective-dated)は「この日のシフトを誰が変えたか」が 追いにくいため不採用。
shift_plans — 変形期間の器(tenant, user, period_start, period_end, published_at)
shift_days — user × date の所定(start_minutes, end_minutes, break_minutes,
day_type: work | legal_holiday | non_working, pattern_id?, supersedes_id UNIQUE)
shift_patterns — 早番/遅番/休み等の定義(tenant, name, start/end/break, day_type)2. UI 粒度 = パターン割当+個別編集
パターン(早番/遅番/休み)を定義し、週グリッドに置いていく。例外日だけ個別編集。 実務のシフト表作成に一致し入力コストが低い。CSV インポートは将来の補助手段。
3. 変形期間の起点 = テナント設定(開始日可変)
tenant_setting_versions に変形期間の開始日(1〜28)を持つ(effective-dated)。締めは暦月の ままなので変形期間は月をまたぐ。期間段(③)の時間外は、期間の終了日が属する月の締めに 帰属させる — 期間の総枠超過は期間末に初めて確定する事実であり、「判断される事実が 発生した日」の原則(v01-data-model.md)の適用。①日次・②週次は従来どおり発生日の月。 締め期間そのものを変形期間に揃える設定は将来の選択肢(二重管理の複雑さと引き換え)。
4. 予実乖離 = 警告表示+管理者へ日次通知+本人へ通知
月次に警告行(既存の型)、打刻忘れリマインドと同じ日次スキャンで管理者(スコープ内の 承認権限者)へまとめ通知。集計には自動反映しない(遅刻控除等は給与側 — 金額を計算しない 原則)。申請フロー化は「承認されないと集計から除外」が過少申告リスクになるため不採用。
本人への通知(2026-08-24 追加): 同じ日次スキャンで、乖離のあった本人にも その日の自分の乖離だけをまとめて通知する(通知種別 shift_variance_self)。管理者向けの ダイジェスト(shift_variance_alert)とは別の通知種別で、宛先も文面も別:
| 管理者向けダイジェスト | 本人向け通知 | |
|---|---|---|
| 種別 | shift_variance_alert | shift_variance_self |
| 宛先 | スコープ内に対象者を含む shift.manage 保持者(本人は除く) | 乖離のあった本人 |
| 文面 | 誰の・どんな乖離かを列挙 | 自分の乖離だけを列挙(他人の名前は出ない) |
| チャネル | アプリ内 + テナント共有 Webhook(件数のみ) | アプリ内 + 個人設定(メール/個人 Webhook) |
本人向けの外部チャネルは個人の通知受け取り設定の新カテゴリ shift_variance (既定: アプリ内=ON、メール/個人 Webhook=OFF)に従う。既存カテゴリに相乗りさせなかったのは、 受け手にとって「申請の結果」でも「休暇のお知らせ」でもなく毎日届きうる自分の勤務の指摘であり、 他を止めずにこれだけ止めたい要望が自然に想定できるため。対象は管理者向けと同じく 「日界を過ぎた勤怠日」のみ(当日進行中の勤務は通知しない)。シフト管理権限を持つメンバーは 同じ日に両方を受け取るが、役割が違う別物なので重複排除はしない。
5. エンジン入力の型(技術判断)
ts
// EngineInput に追加(他のタイムラインと同じ「呼び出し側が集めて渡す」契約)
shifts?: ShiftDay[]; // { date, dayType, startMinutes, endMinutes, breakMinutes } 期間内+前後の日跨ぎ分
// WorkSystem の第3派生
| { kind: "monthly_variable"; periodStartDay: number /* 1-28 */ }シフトが無い日(monthly_variable なのに ShiftDay が無い)は警告 missing_shift を出し、 その日は所定0として①②の判定を行う(実労働があれば全量が判定対象 — 保守的)。
実装フェーズ(v0.7)
- エンジン:
monthly_variableの3段判定+シフト入力+予実乖離の警告(純関数・フィクスチャ) - DB/API: shift_patterns / shift_plans / shift_days、確定・supersede、集計への配線、 日次スキャンの乖離通知
- UI: パターン管理・週グリッドでの割当・確定・本人のシフト閲覧・月次の乖離警告
- 有給付与の3段フロー(予告→承認→通知、出勤率参考値 — シフトが分母を与える)
フェーズ4の決定事項(2026-08-24)
6. シフト制ユーザーの有給1日分の分数換算
work_policy_versions.standard_day_minutes は monthly_variable では長らく「NOT NULL 列を 埋めるためだけのプレースホルダ」だったが、フェーズ4で 「1日あたりの基準所定時間(有給換算用)」 という意味を与える。有給1件の分数は次の2段で解決する(apps/api/src/lib/leave-minutes.ts):
- その user × date に有効な
shift_daysがありday_type = workなら、その日のシフト所定 (@kizami/engineのshiftScheduledMinutes— 休憩控除・日跨ぎの扱いは engine の定義に従う) - それ以外(シフトの無い日 / legal_holiday / non_working)は上記の基準所定
一方、残高の日↔分換算(「残り◯日」の表示、calculateBalance の standardDayMinutes)には 規則2の値だけを使う。残日数は特定の日付に紐づかない総量であり、たまたま今日入っている シフトの長短で見え方が変わるのは不自然なため。
これにより GET /leave/balance がシフト制ユーザーで 500 になっていた既知の不具合が解消する。 フレックス・固定時間制の挙動は一切変えない(規則2しか通らず、値もクエリ数も従来と同一)。
基準所定は POST /settings/work-policy と POST /members/:id/work-policy の standardDayMinutes(省略時 480)で設定する。kind ごとの共有ポリシーなので、この値は 同じ制度の全メンバーに及ぶ — 人ごとに所定が違うケースはシフトそのもので表現する。
7. 有給付与の3段フロー = 予告テーブル + 日次ワーカー + 承認 API
leave_grant_proposals(予告)を新設し、日次ワーカー (apps/api/src/leave-grant-proposals.ts)が基準日の 30日前(定数、設定UIは持たない)に 予告行を積む。管理者が承認したときにだけ leave_grants(source = "proposal")が生まれ、 本人へ「◯日付与されました」を leave_alert カテゴリ・個人チャネルで通知する。
- 予告の通知先は本人ではなく
leave.grant.manage保持者(resolveApproversForUserを再利用)。 承認者×基準日で1件にまとめ、テナント共有 Webhook へは件数だけを流す(シフト予実乖離と同じ形) - 冪等: 既に
leave_grantsがある基準日、および superseded 以外の予告がある基準日は再提案しない。 却下も「再提案しない」側に数える(翌日に復活すると却下の意味が無くなるため)。やはり 付与する場合は手動経路POST /leave/grants/autoを使う — そちらが同じ基準日の付与を作ったら 予告はsupersededになる - 基準日より前の承認を許す(事前承認がこのフローの目的の1つ)。ただし付与日は基準日のまま — 時効・年5日期間の起点がずれるため
8. 出勤率は「参考値」であって判定ではない
労基法39条1項の8割出勤要件は、休職・育休期間の取扱い・争議行為の日・全労働日の定義といった データだけでは決まらない要素を含む。よってシステムは検算材料を出すところまでを担い、 可否の判断は人が行う(packages/leave/src/attendance-rate.ts は純関数で、日付ファクトは API 層が集めて渡す)。
- 算定期間 = [基準日 − 1年, 基準日 − 1日]、初回付与は入社日でクリップ
- 分母(全労働日): シフト制は期間内の有効な
shift_daysのday_type = work(basisshift)。 固定時間制・フレックスは暦日から土日と法定休日の曜日を除いた推定(basiscalendar_estimate) —tenant_setting_versionsは所定休日を持たないため。シフト制でも期間内にシフトが1件も 無ければ暦日推定へフォールバックし、basis でそれを明示する - 分子(出勤日): 実労働のあった日(打刻の
clock_inを勤怠日単位でまとめたもの)+ 承認済みの休暇取得日(年休取得日は出勤扱い、通達)。いずれも分母に含まれる日だけを数える workingDaysが 0 ならrateはnull(0 ではない — 「不明」と「0%」を区別する)- UI は 80% 未満に警告表示を出すが、それは注意喚起であって付与のブロックではない