# Design: Enhanced Allocation Fidelity ## Context The allocation experience must reflect a strict planning-to-execution model. Current implementation drift introduced incorrect month semantics (`approved_estimate / 12`), mixed concern UIs, and ambiguous status signaling. This design aligns implementation with the validated operating model: 1. **Projects (Lifecycle)**: `approved_estimate` is the lifecycle total. 2. **Project-Month Plan (Intent)**: manager explicitly sets monthly planned effort per project. 3. **Project-Resource Allocation (Execution)**: for selected month, manager allocates effort by team member. 4. **Reporting (Outcome)**: historical/current/future insights built from these three layers. ## Goals 1. Implement explicit project-month planning as first-class data. 2. Reframe allocation matrix as execution grid with month-plan and capacity variance. 3. Support untracked effort (`team_member_id = null`) end-to-end. 4. Implement partial bulk success for allocations. 5. Keep visual signaling minimal and decision-focused. ## Non-Goals - Real-time collaboration/websockets. - Notification systems. - Export workflows. - Cosmetic UI experimentation beyond fidelity requirements. ## Decisions ### D1: Month Plan Is Explicit (No Derived Monthly Budget) - **Decision**: monthly plan is manager-entered and persisted. - **Rejected**: deriving monthly budget from `approved_estimate / 12`. - **Reason**: project phasing is dependency-driven, not uniform. ### D2: Reconciliation vs Lifecycle Total For each project: - `plan_sum = sum(non-null planned_hours across months)` - compare against `approved_estimate` - `plan_sum > approved_estimate` -> `OVER` - `plan_sum < approved_estimate` -> `UNDER` - `plan_sum == approved_estimate` -> `MATCH` Tolerance for MATCH should use decimal-safe comparison in implementation. ### D3: Blank Month Semantics - Planning grid cell can be blank (`null`) and remains visually blank. - Allocation variance treats missing month plan as `0`. - Allocation is allowed when month plan is blank. ### D4: Grid-First Editing - Project-month planning: inline grid editing primary. - Allocation matrix: inline grid editing primary. - Modal is optional/fallback, not primary path. ### D5: Untracked Semantics - Untracked allocation is represented by `team_member_id = null`. - Included in project totals and grand totals. - Excluded from team member capacity/utilization calculations. ### D6: Visual Status Policy - Over = red - Under = amber - Match/settled = neutral Status emphasis belongs on row/column summary edges and variance cells; avoid noisy color in every interior cell. ### D7: Forecasted Effort Deprecation - `projects.forecasted_effort` is deprecated for this planning workflow. - New monthly planning source of truth is explicit project-month plan data. ## Data Model ### New Table: `project_month_plans` Recommended schema: - `id` (uuid) - `project_id` (uuid FK -> projects.id) - `month` (date, normalized to first day of month) - `planned_hours` (decimal, nullable) - `created_at`, `updated_at` - unique index on (`project_id`, `month`) Notes: - `planned_hours = null` means blank/unset plan. - If storage policy prefers non-null planned hours, clear operation must still preserve blank UI semantics via delete row strategy. ### Existing Tables - `projects.approved_estimate`: lifecycle cap (unchanged semantics) - `allocations.team_member_id`: nullable to support untracked ## API Design ### Project-Month Plan APIs 1. `GET /api/project-month-plans?year=YYYY` - returns month-plan grid payload by project/month - includes reconciliation status per project 2. `PUT /api/project-month-plans/bulk` - accepts multi-cell upsert payload - supports setting value and clearing value (blank) - returns updated rows + reconciliation results Example payload: ```json { "year": 2026, "items": [ { "project_id": "...", "month": "2026-01", "planned_hours": 1200 }, { "project_id": "...", "month": "2026-02", "planned_hours": 1400 }, { "project_id": "...", "month": "2026-03", "planned_hours": 400 }, { "project_id": "...", "month": "2026-04", "planned_hours": null } ] } ``` ### Allocation APIs - Keep `GET /api/allocations?month=YYYY-MM` and CRUD endpoints. - Enrich response with month-context variance metadata needed for row/column summary rendering. - `POST /api/allocations/bulk` must support partial success. Partial bulk response contract: ```json { "data": [{ "index": 0, "id": "...", "status": "created" }], "failed": [{ "index": 1, "errors": { "allocated_hours": ["..."] } }], "summary": { "created": 1, "failed": 1 } } ``` ## Computation Rules ### Lifecycle Reconciliation (Project-Month Plan Surface) For each project: - `plan_sum = SUM(planned_hours where month in planning horizon and value != null)` - `status = OVER | UNDER | MATCH` compared to `approved_estimate` ### Allocation Row Variance (Execution Surface) For selected month and project: - `allocated_total = SUM(allocation.allocated_hours for project/month including untracked)` - `planned_month = planned_hours(project, month) or 0 if missing` - `row_variance = allocated_total - planned_month` - status from row_variance sign ### Allocation Column Variance (Execution Surface) For selected month and team member: - `member_allocated = SUM(allocation.allocated_hours for member/month)` - `member_capacity = computed month capacity` - `col_variance = member_allocated - member_capacity` - exclude `team_member_id = null` rows from this computation ## UX Specification (Implementation-Oriented) ### Surface A: Project-Month Plan Grid - Rows: projects - Columns: months - Right edge: row total + reconciliation status - Bottom edge: optional month totals for planning visibility - Cell editing: inline, keyboard-first - Blank cells remain blank visually - Color: only summary statuses (red/amber/neutral) ### Surface B: Project-Resource Allocation Grid (Selected Month) - Rows: projects - Columns: team members + untracked - Right edge: project row total + row variance status vs planned month - Bottom edge: member totals + capacity variance status - Primary editing: inline cell edit (modal not primary) - Color: red/amber highlights only for over/under summary states ### Accessibility - Do not rely on color alone; include text labels (OVER/UNDER). - Maintain focus and keyboard navigation in both grids. - Preserve contrast and readable status labels. ## Risk Register 1. **Semantic regression risk**: reintroduction of derived monthly budget logic. - Mitigation: explicit tests and prohibition in specs. 2. **Blank vs zero confusion**. - Mitigation: explicit API contract and UI behavior tests. 3. **Untracked leakage into capacity metrics**. - Mitigation: query filters and dedicated tests. 4. **Bulk partial side effects**. - Mitigation: per-item validation and clear response contract. ## Testing Strategy ### Unit - Lifecycle reconciliation calculator - Row/column variance calculators - Blank-plan-as-zero execution logic ### Feature/API - Project-month plan bulk upsert and clear behavior - Reconciliation status correctness - Allocation month variance data correctness - Untracked include/exclude behavior - Partial bulk success semantics ### Frontend Grid Tests - Inline edit commit/cancel/clear - Keyboard navigation - Summary status placement and labels - Blank visual state preservation ### E2E - Create lifecycle estimate project - Enter monthly plan across months and verify reconciliation - Execute month allocations and verify row/column variances - Validate untracked behavior - Validate partial bulk success handling ## Migration & Rollout Notes 1. Introduce project-month plan model and APIs. 2. Remove derived budget rendering in allocation UI. 3. Wire allocation UI to explicit month plan for variance. 4. Deprecate `forecasted_effort` usage in this workflow path. 5. Keep backward compatibility for existing allocation records.