Skip to content

Commit 806b299

Browse files
authored
Merge pull request #126 from LevelCapTech/copilot/update-task-table-relationship
feat: 実績バー優先の実績連動設計(正規化/業務時間帯/シーケンス/稼働時間算出の明確化)
2 parents 35a028e + d878ad4 commit 806b299

1 file changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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

Comments
 (0)