Normalize instance forecast budget: monthly entry and week shaping

Objective

  • Replace the current period-keyed forecast model with a month-first budget model so leadership can enter a specific budget amount for each calendar month at the brand (instance) level.
  • Keep monthly budget values as the authoritative source of truth; weekly numbers are derived from monthly values, not entered as the primary record.
  • Preserve compatibility for existing instance-level forecast consumers so dashboard surfaces such as Revenue, Future Capacity, and Annual Forecast Progress continue to read meaningful forecast data through the current APIs.
  • Support optional segment-level budget rows that sum to the company monthly total for each month.
  • Lay normalized per-instance data for a future admin cross-brand dashboard (ATT-752) — v1 is instance-only.

Background

  • Forecast data is stored per company in attik-backend/src/models/forecastSchema.ts with year, _companyId, totals, and segmentForecasts, where entries are keyed by period-start date strings rather than by month.
  • The settings flow in attik-backend/src/routes/forecast.ts and attik-backend/src/util/functions/forecast/getTotalsWithRange.ts reads those date-keyed totals for arbitrary ranges. GET /forecast/range returns range totals; GET /dashboard/forecast-status in attik-backend/src/routes/dashboard.ts uses the same helper with period division enabled.
  • The UI in attik-frontend/src/app/tools/settings/forecast/ (ForecastBody.tsx, ForecastTable.tsx) is weekly-first (pay-period or custom period length), which does not match how leadership plans in monthly budgets.
  • Product direction (Chris / transcript): monthly entry is primary; week-level shaping redistributes each month's total across weeks without changing the month unless the user edits the month amount.
  • v1 is instance-only — Settings → Forecast per company. Admin aggregation remains follow-on.

Product decisions (v1)

  1. Monthly totals are always locked while adjusting week-level shaping within a month.
  2. Overlap weeks between months — when a calendar week spans two months, allocate that week's budget proportionally by days in each month (per company timezone). Monthly totals remain locked.
  3. Calendar months in company timezone — months are calendar months in the company timezone, consistent with other dashboard date boundaries.
  4. Default week distribution — if the user enters a monthly amount and does not adjust sliders, split that month evenly across weeks that touch the month (before any user reshaping).
  5. Segment budgets sum to company total — optional segment rows allocate the company monthly budget; for each month, segment amounts must sum to the company monthly total. Segment rows are optional; only segments with values appear as selectable categories in widgets.
  6. One-time migration — all instances move to the new experience. Legacy weekly period-keyed data is not converted into authoritative monthly budgets. Monthly values start empty; brands re-enter monthly budgets and optional week shaping.
  7. Empty / unset forecast — when no monthly budget is configured (including post-migration), instance widgets show an empty state prompting setup rather than implying a zero forecast is intentional.

Scope

Backend (attik-backend)

  • Extend or replace src/models/forecastSchema.ts for month-first budgeting per _companyId / year, with derived or persisted weekly breakdown and optional segment allocations that reconcile to company monthly totals.
  • Update src/routes/forecast.ts and src/util/functions/forecast/getTotalsWithRange.ts so range reads honor calendar months, proportional overlap-week allocation, and even-split defaults where shaping was not customized.
  • Keep GET /dashboard/forecast-status and GET /forecast/range returning sensible totals for arbitrary date ranges from the normalized model.
  • Enforce segment-sum-to-company validation on write where segment rows are present.

Frontend (attik-frontend)

  • Rebuild or extend Settings → Forecast in src/app/tools/settings/forecast/ with:
  • Monthly budget grid (calendar months, company timezone context)
  • Week-level shaping controls per month that redistribute without unlocking the monthly total
  • Segment rows (optional) with validation that monthly segment sums match company total
  • Maintain existing access (loginCheck('/forecast', 'settings-admin') on page.tsx).
  • After migration, show setup prompt / empty state when monthly budgets are unset.
  • Ensure instance consumers (Revenue.tsx, ForecastProgress.tsx, FutureCapacity.tsx via forecast/range) handle unset forecast with empty-state UX where appropriate.

Out of scope for v1

  • Admin Attik cross-brand budget entry or dashboard UI
  • ATT-752 company-wide KPI dashboard implementation
  • ATT-753 widget scoping split
  • ATT-1471 forecast/capacity availability bugs
  • Changes to actual revenue calculation logic

Done when

  • [ ] Company admin can enter monthly budget amounts per calendar month (company timezone) for a year in Settings → Forecast.
  • [ ] Monthly total stays locked while week sliders reshape distribution within the month.
  • [ ] Weeks spanning two months split proportionally by days in each month; monthly totals remain correct.
  • [ ] Default distribution for untouched months is even split across weeks touching the month.
  • [ ] Optional segment rows sum to company monthly total each month; validation prevents save when out of balance.
  • [ ] Normalized data persists per instance; forecast/range and forecast-status return sensible range totals for Revenue / forecast progress / Future Capacity.
  • [ ] One-time cutover: legacy weekly rows not auto-converted; monthly fields start empty; brands re-enter budgets.
  • [ ] Unset forecast shows empty state prompting setup in settings and relevant dashboard widgets.
  • [ ] No admin cross-brand UI required to ship v1.

References

  • ATT-752
  • ATT-753
  • ATT-1471
  • ATT-627
  • ATT-1578
  • ATT-734
  • attik-backend/src/models/forecastSchema.ts
  • attik-backend/src/routes/forecast.ts
  • attik-backend/src/util/functions/forecast/getTotalsWithRange.ts
  • attik-backend/src/routes/dashboard.ts
  • attik-frontend/src/app/tools/settings/forecast/ForecastBody.tsx
  • attik-frontend/src/app/tools/settings/forecast/ForecastTable.tsx
  • attik-frontend/src/components/dashboard/Revenue/Revenue.tsx
  • attik-frontend/src/components/dashboard/ForecastProgress/ForecastProgress.tsx
  • attik-frontend/src/components/dashboard/FutureCapacity/FutureCapacity.tsx

Diagram

flowchart TD
    A[Company admin enters monthly budget] --> B[Monthly values stored per company and year]
    B --> C[Optional segment rows sum to company monthly total]
    B --> D[Weekly values derived from monthly totals]
    C --> D
    D --> E[Forecast range reads normalized data]
    D --> F[Forecast status reads normalized data]
    E --> G[Instance Revenue widget]
    F --> H[Instance forecast progress widget]
    B --> I[No admin cross-brand dashboard in v1]
    B --> J[Unset budget shows empty state prompting setup]

Please authenticate to join the conversation.

Upvoters
Status

Triage

Board
🏠

Main App

Date

About 3 hours ago

Author

Linear

Subscribe to post

Get notified by email when there are changes.