|
| 1 | +# Implementation Plan: 実績開始日/実績終了日/実績工数のバー優先連動 |
| 2 | + |
| 3 | +## 1. 機能要件 / 非機能要件 |
| 4 | +- 機能要件: |
| 5 | + - SSOT を (ActualStart, ActualEnd) とし、ActualEffortHours は派生値として正規化する。 |
| 6 | + - ActualEffortHours は既存の `Task.actualEffort`(hours)を指す表現とし、新しい公開APIの追加は行わない。 |
| 7 | + - 初期表示時に 4 パターンの補完/矛盾解決を実施し、常に矛盾ゼロの表示にする。 |
| 8 | + - 編集時は 2 項目確定で残り 1 項目を自動更新し、バー優先(start/end)を保持する。 |
| 9 | + - ActualEffortHours 編集時は ActualStart を固定し、稼働日計算で ActualEnd を算出する。 |
| 10 | + - 稼働時間/日 (workHoursPerDay) は呼び出し元パラメータで指定可能とし、未指定時は既定の業務時間帯から算出する。 |
| 11 | + - `windowHours = workdayEndTime - workdayStartTime` の duration。 |
| 12 | + - `workHoursPerDay` 未指定時は `defaultBreakHours = 1h` として `workHoursPerDay = windowHours - defaultBreakHours`(既定値は 8h)。 |
| 13 | + - `windowHours <= defaultBreakHours` の場合は `workHoursPerDay = windowHours` とし、`breakHours = 0` とする(業務時間帯が短い場合は休憩を持たない)。 |
| 14 | + - `effectiveWorkHoursPerDay = min(workHoursPerDay, windowHours)`。 |
| 15 | + - `breakHours = windowHours - effectiveWorkHoursPerDay`(暗黙の休憩時間)。 |
| 16 | + - 業務開始/終了時刻 (workdayStartTime/workdayEndTime) を呼び出し元パラメータで指定可能とし、未指定時は 09:00〜18:00 を既定値とする(9h 窓に対して workHoursPerDay 既定 8h を想定し、休憩相当は workHoursPerDay の指定で調整し、breakHours は windowHours と effectiveWorkHoursPerDay の差分として暗黙に決まる)。 |
| 17 | + - workdayStartTime/workdayEndTime は `"HH:mm"` 形式の文字列で受け取り、タスクの日時と同じローカルタイムゾーンで解釈する(例: `"09:00"`)。 |
| 18 | + - 期間は [ActualStart, ActualEnd) の半開区間とし、ActualEffortHours は `q = effort / 0.25` に対して `normalized = Math.floor(q + 0.5) * 0.25` を適用する(round-half-up を明示し、0.5 は上方向)。例: 1.12→1.00、1.13→1.25。なお実装では、`effort` を分(または 15 分単位)の整数に変換してからこの丸めを適用するか、`q` 計算時に微小な epsilon(例: `q = effort / 0.25 + Number.EPSILON`)を加えるなど、二進浮動小数誤差に強い丸め方針を用いること。 |
| 19 | +- 非機能要件: |
| 20 | + - 正規化は冪等で、高頻度呼び出しに耐える軽量な計算であること。 |
| 21 | + - ログ/表示に Secrets/PII を含めない。 |
| 22 | + - 既存 UI/データ互換を壊さず、既存バー表示と一致すること。 |
| 23 | + |
| 24 | +## 2. スコープと変更対象 |
| 25 | +- 変更ファイル(新規/修正/削除): |
| 26 | + - 新規: |
| 27 | + - `src/helpers/actuals-helper.ts`(正規化/補完/丸めの専用ユーティリティ) |
| 28 | + - 修正: |
| 29 | + - `src/types/public-types.ts`(Task に actualStart/actualEnd を追加、正規化オプション workHoursPerDay/workdayStartTime/workdayEndTime を追加) |
| 30 | + - `src/helpers/task-helper.ts`(actualStart/actualEnd の入出力サポート) |
| 31 | + - `src/components/gantt/gantt.tsx`(tasks 受信時の正規化適用) |
| 32 | + - `src/components/task-list/task-list.tsx`(編集確定時に正規化を適用) |
| 33 | + - `src/components/task-list/task-list-table.tsx`(実績開始/終了の表示列追加) |
| 34 | + - `src/components/task-list/overlay-editor.tsx`(実績開始/終了/工数の編集対応) |
| 35 | + - `src/test/task-list-table-editing.test.tsx`(編集時正規化の回帰テスト) |
| 36 | + - `src/test/task-model.test.tsx`(actualStart/actualEnd シリアライズ/表示の回帰テスト) |
| 37 | +- 影響範囲・互換性リスク: |
| 38 | + - Task Table の実績表示、Gantt 実績バー、ロード時の既存データ表示が影響範囲。 |
| 39 | + - 既存データの ActualEffortHours がバーと矛盾する場合、ロード時に補正される。 |
| 40 | + - 既存の `actualEffort` フィールドを hours として扱い、`actualEffortHours` は表示上の呼称に留める。 |
| 41 | +- 外部依存・Secrets の扱い: |
| 42 | + - 稼働日カレンダー、1人固定の前提に依存し、1日あたりの稼働時間は workHoursPerDay で指定 (未指定時は `workHoursPerDay = windowHours - defaultBreakHours` で算出する。`windowHours` は workdayStartTime〜workdayEndTime の duration、`defaultBreakHours` は 1h)。 |
| 43 | + - 業務時間帯は workdayStartTime/workdayEndTime で指定 (未指定時は 09:00〜18:00 を既定値とする)。 |
| 44 | + - Secrets/PII は扱わない。 |
| 45 | + |
| 46 | +## 3. 設計方針 |
| 47 | +- 責務分離 / データフロー(必要なら Mermaid 1 枚): |
| 48 | + - 正規化ロジックは純粋関数として実装し、UI からは `normalizeActuals` を呼び出す。 |
| 49 | + - 正規化は `recalcEffort`, `deriveEnd`, `deriveStart`, `roundEffortToQuarterHour` に責務分割する。 |
| 50 | + - 半開区間を前提に稼働日カレンダー計算 API を利用する。 |
| 51 | + - `normalizeActuals` の引数で `workHoursPerDay` と `calendar`(DisplayOption; 内部的には既存の `CalendarConfig` と同義の稼働日/休日設定)、`workdayStartTime`/`workdayEndTime` を受け取り、Gantt の props から既定値 (8h, 09:00〜18:00) を注入する。 |
| 52 | + - 呼び出しタイミング: |
| 53 | + - 初期表示/再描画: `Gantt` の tasks 受信時に `normalizeActuals` を適用し、正規化後の tasks で `ganttDateRange` と `convertToBarTasks` を生成する。 |
| 54 | + - 編集確定時: `TaskList` の `commitEditing` で該当タスクに `normalizeActuals` を適用し、正規化済みの差分を `onTaskUpdate` / `onCellCommit` へ渡す(内部的には `TaskList` に `onUpdateTask` として渡される)。 |
| 55 | + - ガントバー操作: ガントのドラッグ/リサイズで `onDateChange` が発火し、ホスト側で更新した tasks が再投入されたタイミングで `normalizeActuals` を適用する(`onDateChange` の通知は正規化前。ホスト側で同じ正規化を適用してから tasks を更新してもよい)。 |
| 56 | + - 外部更新: API 再取得や親コンポーネントの更新でも tasks prop の更新で同じ正規化が走る(冪等前提)。 |
| 57 | + |
| 58 | +```mermaid |
| 59 | +flowchart TD |
| 60 | + A[normalizeActuals] --> B{Start+End} |
| 61 | + B -->|Yes| C[recalcEffort] |
| 62 | + B -->|No| D{Start+Effort} |
| 63 | + D -->|Yes| E[deriveEnd -> normalize] |
| 64 | + D -->|No| F{End+Effort} |
| 65 | + F -->|Yes| G[deriveStart -> normalize] |
| 66 | + F -->|No| H[Undetermined] |
| 67 | +``` |
| 68 | + |
| 69 | +- シーケンス図: |
| 70 | + |
| 71 | +```mermaid |
| 72 | +sequenceDiagram |
| 73 | + participant User |
| 74 | + participant Host |
| 75 | + participant Gantt |
| 76 | + participant TaskList |
| 77 | + participant Normalize as normalizeActuals |
| 78 | + participant Calendar |
| 79 | +
|
| 80 | + rect rgb(240, 248, 255) |
| 81 | + Host->>Gantt: tasks 初期投入 |
| 82 | + Gantt->>Normalize: 正規化(workHoursPerDay/workdayStartTime等) |
| 83 | + Normalize->>Calendar: 稼働日計算 |
| 84 | + Normalize-->>Gantt: 正規化済み tasks |
| 85 | + Gantt-->>Host: 表示更新 |
| 86 | + end |
| 87 | +
|
| 88 | + rect rgb(245, 245, 245) |
| 89 | + Host->>TaskList: tasks 再描画 |
| 90 | + TaskList->>Normalize: 編集確定時の正規化 |
| 91 | + Normalize->>Calendar: 稼働日計算 |
| 92 | + Normalize-->>TaskList: 正規化差分 |
| 93 | + TaskList-->>Host: onUpdateTask/onCellCommit |
| 94 | + end |
| 95 | +
|
| 96 | + rect rgb(255, 248, 240) |
| 97 | + User->>Gantt: ガントバー操作 |
| 98 | + Gantt-->>Host: onDateChange (正規化前) |
| 99 | + Host->>Gantt: 更新済み tasks 再投入 |
| 100 | + Gantt->>Normalize: 正規化 |
| 101 | + Normalize-->>Gantt: 正規化済み tasks |
| 102 | + end |
| 103 | +``` |
| 104 | + |
| 105 | +- エッジケース / 例外系 / リトライ方針: |
| 106 | + - ActualStart/ActualEnd のパース不能・範囲外・start > end は「欠落」と同等に扱い、初期表示の補完順序に従う。 |
| 107 | + - ActualEffortHours が負数/NaN は欠落扱いとし補完を試みる。 |
| 108 | + - ActualEffortHours=0 は start=end を許容し、半開区間のため effort は 0 として扱う。 |
| 109 | + - 非稼働日跨ぎはカレンダー API に委譲し、加算/差分計算は稼働日のみを対象にする。 |
| 110 | + - workHoursPerDay が未指定/0 以下/NaN の場合は、既定の算出ルール(`workHoursPerDay = windowHours - defaultBreakHours`、既定値 8h)にフォールバックする。 |
| 111 | + - workHoursPerDay が業務時間帯の長さ(workdayStartTime/workdayEndTime を時間差に換算した値)を超える場合は、`effectiveWorkHoursPerDay = windowHours` を適用して計算し、設定不整合を警告ログで通知する。 |
| 112 | + - 正規化は高頻度呼び出しを前提とするため、既存の `src/helpers/calendar-helper.ts` の `warnOnce` と同様に同一内容の警告は 1 回だけ出力する(プロセス内の記憶で抑制し、永続化はしない)。 |
| 113 | + - workdayStartTime/workdayEndTime が未指定/不正/逆転の場合は既定値 09:00〜18:00 にフォールバックする。 |
| 114 | + - end 算出/丸めは業務時間帯内で完結させ、丸め後の end が workdayEndTime を超える場合は次稼働日の workdayStartTime に繰り越す。 |
| 115 | + - 繰り越し手順: |
| 116 | + - `overflow = roundedEnd - workdayEndTime`(roundedEnd は datetime、workdayEndTime は同日の時刻に変換し、時間差を分単位の duration として扱う)。 |
| 117 | + - 次稼働日の `workdayStartTime + overflow` を end とする。 |
| 118 | + - 例: start 17:45、effort 0.5h=30分 → roundedEnd 18:15、workdayEndTime 18:00 のため overflow 15分、翌稼働日の 09:15 にする。 |
| 119 | +- ログと観測性(漏洩防止を含む): |
| 120 | + - 既存の console.debug / console.warn の構造化ログ方針に合わせる。 |
| 121 | + - 無効値補完や矛盾補正時は rowId・フィールド名・原因のみをログに出し、値本文は必要最小限にする。 |
| 122 | + |
| 123 | +## 4. テスト戦略 |
| 124 | +- テスト観点(正常 / 例外 / 境界 / 回帰): |
| 125 | + - 初期表示 4 パターン: Start+End, Start+Effort, End+Effort, どれも無い。 |
| 126 | + - 編集時 3 パターン: Start 編集, End 編集, Effort 編集。 |
| 127 | + - workHoursPerDay の変更: 6h/8h/10h で end 算出が変わることを確認。 |
| 128 | + - workdayStartTime/workdayEndTime の変更: 08:00〜17:00/09:00〜18:00/10:00〜19:00 で丸め後の end が業務時間内に収束することを確認。 |
| 129 | + - workHoursPerDay > windowHours のクランプ(`effectiveWorkHoursPerDay = windowHours`)と warnOnce 相当の警告が 1 回だけ出ることを確認(警告は「workHoursPerDay が業務時間帯を超過している」旨と採用値を含む)。 |
| 130 | + - windowHours <= defaultBreakHours の場合に `workHoursPerDay = windowHours` となり、休憩時間が 0 になることを確認。 |
| 131 | + - workdayStartTime/workdayEndTime の不正値フォールバックと overflow 繰り越しが次稼働日に反映されることを確認。 |
| 132 | + - 0.25h 丸めを確認: |
| 133 | + - 1.12→1.00、1.13→1.25。 |
| 134 | + - 1.37→1.25、1.38→1.50。 |
| 135 | + - 境界条件: effort/0.25 の小数部が 0.5 以上で上方向(1.124→1.00、1.125→1.25)。 |
| 136 | + - 境界値: 1.125→1.25、1.375→1.50、1.625→1.75、1.875→2.00。 |
| 137 | + - 境界直前/直後: 1.124→1.00、1.126→1.25、1.374→1.25、1.376→1.50。 |
| 138 | + - 祝日/非稼働日を含む期間での effort 再計算/ end 算出。 |
| 139 | + - 無効値(NaN/負数/start>end)入力時の欠落扱い。 |
| 140 | +- モック / フィクスチャ方針: |
| 141 | + - 稼働日カレンダーは既存ユーティリティのモックを用い、固定カレンダーで期待値を確定させる。 |
| 142 | +- テスト追加の実行コマンド(例: `python -m pytest`): |
| 143 | + - `npm run test:unit` (必要に応じて `npm test` で lint/build を含めて実行) |
| 144 | + |
| 145 | +## 5. CI 品質ゲート |
| 146 | +- 実行コマンド(format / lint / typecheck / test / security): |
| 147 | + - lint/build/test: `npm test` (`test:unit` + `test:lint` + `test:build`) |
| 148 | + - security: `npm audit` (既存 CI 運用に合わせて実行) |
| 149 | +- 通過基準と失敗時の対応: |
| 150 | + - すべてのテストが green であること。失敗時は正規化計算/丸め/稼働日処理を見直す。 |
| 151 | + |
| 152 | +## 6. ロールアウト・運用 |
| 153 | +- ロールバック方法: |
| 154 | + - 正規化ロジックを導入したコミットをリバートし、既存表示に戻す。 |
| 155 | +- 監視・運用上の注意: |
| 156 | + - 既存データの effort がロード時に補正される可能性をリリースノートで周知する。 |
| 157 | + |
| 158 | +## 7. オープンな課題 / ADR 要否 |
| 159 | +- 未確定事項: |
| 160 | + - なし。 |
| 161 | +- ADR に残すべき判断: |
| 162 | + - なし (本仕様で SSOT/丸め/半開区間が確定済み)。 |
0 commit comments