Files
pn-new-crm/docs/reports-content-spec.md

525 lines
28 KiB
Markdown
Raw Normal View History

feat(reports-overhaul): sales + operational + custom reports, templates, schedules, exports End-to-end reports build covering Phases 1, 2, 5, 6, 7 of Initiative 1 in docs/launch-readiness.md. Phases 3 (Marketing) + 4 (Financial) remain deferred per the gap audit at the bottom of that doc. Highlights: - Sales performance report: 7 KPI tiles, pipeline funnel + stage velocity + win-rate-over-time + source conversion + rep leaderboard charts, deal-heat section, 5 detail tables, stage / lead-cat / outcome filters. - Operational report: 7 KPIs, 7 charts (heatmap, status mix, tenancy churn, tenure histogram, signing box plot, occupancy by area, docs in pipeline), 4 tables. Module-OFF banner when tenancies disabled. - Custom (ad-hoc) builder v1: 4 entities (clients, interests, berths, tenancies), column-whitelist composer, date filter, CSV download, save-as-template. Registry-only extension path for the remaining 6 entities documented at src/lib/reports/custom/registry.ts. - Templates: load / modify / save / save-as on Sales / Operational / Custom. ?templateId= URL deep-link hydration via useRef guard. Active-template badge clears when the user drives view-state via wrapped setters; raw setters used on template apply so the badge survives. - Scheduled runs: BullMQ poll fires due schedules, mints report_runs, renders, optionally emails. Recipients optional (zero-recipient schedules archive without sending). PDF-only output for v1. Schedule dialog re-mounts via key prop on schedule.id transitions to avoid setState-in-effect reset patterns. - Server-side PDF endpoint + shared payload renderer (lib/pdf/reports/payload-report.tsx) so client + scheduler share one rendering path. - Shared currency formatter (lib/reports/format-currency.ts) consolidates 5 duplicated formatMoney helpers; fixes hardcoded 'USD' in detail tables; pre-formats money rows so PDF export (which strips column.format callbacks at the JSON boundary) renders consistently with CSV / XLSX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:41:53 +02:00
# Reports — content spec (draft for review)
> Source of truth for what each report category will contain. Driven by
> the actual data we have in the schema; nothing here is aspirational
> data we'd need to start collecting. Once locked, this drives the
> builder implementations.
---
## Raw materials — data we already capture
The proposals below are bounded by what we already store. A quick map of
the load-bearing fields per entity:
### `interests` (the sales pipeline source of truth)
- `pipelineStage` — one of 7 canonical stages
- Per-stage timestamps: `dateFirstContact`, `dateLastContact`,
`dateEoiSent`, `dateEoiSigned`, `dateReservationSigned`,
`dateContractSent`, `dateContractSigned`, `dateDepositReceived`
- `outcome` (won/lost variants), `outcomeReason`, `outcomeAt`
- `source` (website/manual/referral/broker), `leadCategory`
(general/qualified/hot)
- `assignedTo` (rep), `clientId`, `yachtId`
- `depositExpectedAmount` + currency
- Per-doc status fields: `eoiDocStatus`, `reservationDocStatus`,
`contractDocStatus` (pending/sent/signed/declined/voided)
- `archivedAt`
### `interest_berths` (multi-berth pipeline)
- `is_primary`, `is_specific_interest`, `is_in_eoi_bundle`
- One interest can target N berths; status of those berths drives
"Under Offer" public flag
### `berths`
- `status` (available/under_offer/sold)
- `area`, `mooringNumber`
- `price`, `priceCurrency`
- `lengthFt/widthFt/draftFt` + metric counterparts
- `tenureType`, `tenureYears`, `tenureStartDate`, `tenureEndDate`
- `statusLastModified`, `statusLastChangedReason`
### `tenancies`
- `status` (pending/active/ended/cancelled)
- `startDate`, `endDate`, `tenureType`
- Links to `berthId`, `clientId`, `yachtId`, `interestId`
- `previousTenancyId` (chain), `transferredFromTenancyId`
### `clients`
- `nationalityIso`, `preferredContactMethod`, `source`
- `createdAt`, `archivedAt`
- `clientContacts` (email/phone/whatsapp values)
- `clientNotes`, `clientTags` (categorisation)
### `invoices` + `payments` + `expenses`
- Invoices: status (draft/sent/paid/overdue/cancelled), `total`,
`subtotal`, `currency`, `dueDate`, `paymentDate`, `paymentTerms`,
`kind` (general/deposit), linked `interestId`
- Payments: amounts, dates, method, linked invoice
- Expenses: `amount`, `amountUsd`, `category`, `paymentStatus`,
`expenseDate`, `establishmentName`, `payer`
### `documents` + `document_signers` + `document_events`
- Send timestamps, sign timestamps, status per signer
- Document type, template id
- Full event audit (sent/viewed/signed/declined per recipient)
- `signedFileId`, `currentPdfVersionId`
### `websiteSubmissions` (inquiry intake)
- Source page, UTM-style attribution columns, raw payload, conversion
state (linked to which interest / client / berth)
- `convertedAt`, `convertedToInterestId`
### `audit_logs`
- Every entity mutation with `action`, `actor`, `oldValue`, `newValue`,
`createdAt` — full timeline of who-changed-what
### Already-aggregated data (existing dashboard endpoints we can reuse)
- `/api/v1/dashboard/forecast` — revenue forecast by stage × probability
- `/api/v1/dashboard/pipeline` — count + value per stage
- `/api/v1/dashboard/hot-deals` — high-pulse deals
- `/api/v1/dashboard/tenancy-occupancy` — occupancy timeline by area
- `/api/v1/dashboard/tenancy-revenue` — recognised revenue by month
- `/api/v1/dashboard/tenancy-renewals` — upcoming renewals
- `/api/v1/dashboard/tenancy-tenure` — tenure distribution
- `/api/v1/dashboard/source-conversion` — funnel by source
- `/api/v1/dashboard/clients-by-country` — geographic distribution
- `/api/v1/dashboard/berth-status` — status mix
- `/api/v1/dashboard/berth-heat` — recommender heat scores
- `/api/v1/dashboard/activity` — activity feed
- `/api/v1/dashboard/kpis` — top-line numbers
---
## Cross-cutting capabilities (apply to every report)
- **Date range filter** — preset (last 7d / 30d / quarter / year / YTD)
plus custom range picker.
- **Period comparison** — toggle to show "this period vs prior period"
(same length window immediately before). Drives delta arrows on KPI
cards.
- **Rep / assignee filter** — multi-select. Defaults to "all". For
ports with one rep this is hidden.
- **Source filter** — multi-select on `source` (website / referral /
broker / manual). Defaults to "all".
- **Currency normalization** — money values render in port-default
currency; underlying records may be USD/EUR/etc., conversion already
exists on expenses and can be extended to invoices.
- **Empty state** — every report renders gracefully on a port with no
data yet (e.g. fresh deploys) with a "this report needs data first"
hint pointing at the right onboarding step.
---
## Report 01 — Sales performance ✅ LOCKED 2026-05-27
**Purpose:** answer "how is the sales team doing, who is doing the
work, where are deals stuck."
### KPI strip (7 tiles)
| # | Tile | Formula | Notes |
| --- | --------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Active interests** | `count(interests) WHERE archivedAt IS NULL AND outcome IS NULL` | All stages incl. nurturing |
| 2 | **Won this period** | `count(interests) WHERE outcome='won' AND outcomeAt IN range` | |
| 3 | **Lost this period** | `count(interests) WHERE outcome LIKE 'lost_%' OR outcome='cancelled' AND outcomeAt IN range` | **Breakdown chip:** `Lost: 8 (3 to competitor · 2 unqualified · 2 no-response · 1 cancelled)` |
| 4 | **Win rate** | `won / (won + lost_*) × 100%` — excludes `cancelled` | Render `—` when denom = 0. Period-over-period delta arrow when comparison toggle is on (`↑ +12pp`) |
| 5 | **Pipeline value** | `Σ ((berth.price OR depositExpectedAmount) × STAGE_WEIGHTS[stage])` for active interests | Berth price used when an `is_primary` interest_berth is set; else depositExpectedAmount; else 0. Currency normalised to port-default. Footnote: "X of Y interests have no value and aren't included." |
| 6 | **Avg time-to-close** | `median(outcomeAt - dateFirstContact)` for won deals in window | Adaptive unit: days (<60) / weeks (<24) / months. Skip interests with null `dateFirstContact`; footnote "based on N of M won deals." |
| 7 | **New leads** | `count(interests) WHERE createdAt IN range`**includes archived** | **Breakdown chip:** `New leads: 24 (10 website · 8 referral · 4 broker · 2 manual)` |
### Charts (5)
1. **Pipeline funnel** (echarts horizontal funnel)
- **Frame:** counts per stage, all 7 stages including `nurturing` as its own step
- **Active interests only** (`archivedAt IS NULL AND outcome IS NULL`)
- **Drop-off label** on each connector: `Enquiry 24 → Qualified 12 (50%)`
2. **Stage velocity** (recharts horizontal bar)
- Median days in each stage + faint p90 mark per bar
- Source: `audit_logs WHERE action='interest.stage_changed'` for transition timestamps
- Exclude stages with no exits yet (interests still sitting there)
3. **Win rate over time** (recharts line + faint area underlay)
- Line: win rate per bucket
- Underlay: total deals closed per bucket (gives volume context)
- **Bucket granularity (auto):** weekly ≤6mo · monthly ≤2yr · quarterly beyond
- Sparse buckets render as gaps, not zero
4. **Source → win conversion** (recharts stacked horizontal bar)
- One bar per source (website / referral / broker / manual)
- Segments coloured by outcome (won / lost-\* / cancelled / in-flight)
- PDF-friendly (no sankey)
5. **Rep leaderboard** (table with embedded mini-bars)
- Columns: rep · new · won · lost · in-flight · pipeline value · win rate · avg time-to-close
- Sortable by any numeric column
- **Single-rep collapse:** when only one rep has deals in the window, skip this chart and render the Rep performance detail (Table 1) directly
- **Attribution:** current `assignedTo` gets full credit; tooltip flags deals that were reassigned mid-cycle
### Deal heat section (between leaderboard and tables)
Folded-in pulse data from existing dashboard infrastructure.
- **Hot deals count** — KPI-style tile, count of interests above `pulse_label_hot` threshold
- **Pulse distribution** — 3-segment horizontal bar (hot / warm / cold counts)
- **Hottest deals right now** — top 5 by pulse score: client · stage · value · pulse · rep
### Tables (5)
1. **Rep performance detail** — leaderboard columns + expandable open-deals list per rep
- Open deals list columns: client · primary berth · stage · stage value · days in stage · last contact
- **Web:** collapsed by default, expand chevron
- **PDF:** always rendered inline (no expander affordance possible in print)
2. **Stalled deals** — active interests not contacted within stage-aware thresholds
- **Thresholds:** enquiry 21d · qualified 14d · nurturing 60d · eoi 10d · reservation 7d · deposit_paid 7d · contract 5d (admin-configurable later)
- Columns: client · stage · days since last contact · days in stage · value · rep · quick "log contact" button
- Sort: stage value desc (most valuable stalled deals first)
- **Null `dateLastContact`** → treat as never contacted → always stalled
3. **Closing this month** — late-stage active deals (`reservation` / `deposit_paid` / `contract`) sorted by stage value desc
- The inverse of stalled; the "don't drop these" list
- Same columns as stalled minus the "days since contact" column
4. **Recent wins** — last 5 won deals, celebratory strip
- Columns: client · primary berth · final value · days to close · rep
- Source: `interests WHERE outcome='won' ORDER BY outcomeAt DESC LIMIT 5`
5. **Lost-reason breakdown** — detail of the KPI 3 chip
- Columns: outcome reason · count · total value lost · avg days from first contact to loss
- Source: group `interests WHERE outcome LIKE 'lost_%' OR outcome='cancelled'` AND outcomeAt IN range by `outcome`
### Filters
- **Cross-cutting** (every report): date range preset/custom, period comparison toggle, rep multi-select (hidden when 1 rep), source multi-select (hidden when 1 source)
- **Sales-specific:**
- **Stage filter** — restrict funnel + tables to subset of stages
- **Lead category filter** — general / qualified / hot
- **Outcome filter** — won / each lost-reason variant (mostly for the lost-reason breakdown post-mortem)
### Currency handling
- All monetary values render in port-default currency (per branding settings)
- Underlying records can be in any currency; convert at render time
- Render with thousand-separator + currency symbol (e.g. `€1,250,000`)
---
## Report 02 — Financial
**Purpose:** answer "what revenue did we collect, what's outstanding,
where is the cash flow going."
### KPI strip
| Metric | Source | Notes |
| ----------------------------- | ---------------------------------------------------------------------------------- | --------------------------------- |
| Revenue collected | Σ `invoices.total WHERE paymentStatus='paid' AND paymentDate IN range` | Sum across currencies, normalised |
| Pipeline (forecasted revenue) | Existing dashboard `forecast` endpoint | Σ deposit_expected × stage weight |
| Deposits collected | Σ `invoices.total WHERE kind='deposit' AND status='paid' AND paymentDate IN range` | |
| Outstanding AR | Σ `invoices.total WHERE status IN ('sent','overdue') AND archivedAt IS NULL` | |
| Overdue AR | Σ above filtered to `dueDate < today` | |
| Expenses (period) | Σ `expenses.amountUsd WHERE expenseDate IN range AND archivedAt IS NULL` | USD-normalised |
| Net contribution | revenue - expenses | Optional |
### Charts
1. **Revenue by month** (bar chart) — Stacked by `kind` (general vs
deposit). 12 months trailing window default.
2. **Revenue by quarter / year** (toggleable granularity) — Same data,
different bucket.
3. **Funnel: EOI → Deposit → Contract → Revenue** (funnel chart,
echarts) — Counts at each stage in the period to highlight leakage.
4. **AR aging** (stacked horizontal bar) — Buckets: current, 1-30,
31-60, 61-90, 90+. Per bucket: count + total value.
5. **Cash flow** (line chart, two series) — Inflow (payments received)
and outflow (expenses paid) over time.
6. **Expense breakdown** (donut) — By `category` for the period.
### Tables
1. **Outstanding invoices** — Invoice #, client, due date, days
overdue, amount, payment terms. Sort by overdue desc.
2. **Recent payments** — Date, invoice, client, amount, method.
3. **Refund / write-off log** — Cancelled invoices with reasons.
4. **Expense ledger** — Date, payer, category, amount, payment status,
linked trip.
### Filters
- Invoice kind (deposit / general)
- Payment status
- Currency
- Billing entity type (client / company)
---
## Report 03 — Marketing & funnel
**Purpose:** answer "where are leads coming from, which sources are
worth the marketing spend, where do we lose people in the funnel."
### KPI strip
| Metric | Source | Notes |
| -------------------------------- | ------------------------------------------------------------------- | ---------------------- |
| Inquiries this period | `count(websiteSubmissions WHERE createdAt IN range)` | |
| Inquiries → interest conversion | `count(websiteSubmissions WHERE convertedAt IN range) / count(...)` | % |
| Inquiries → EOI conversion | Same with `interest.dateEoiSent NOT NULL` | |
| Inquiries → won conversion | Same with `interest.outcome='won'` | |
| Top source | `source` with highest converted count | Card with name + count |
| Avg time inquiry → first contact | Median(`interest.dateFirstContact - websiteSubmission.createdAt`) | Hrs / days |
### Charts
1. **Inquiries by source** (donut + bar) — Count per source for the
period.
2. **Source ROI** (stacked horizontal bar) — Per source: total count,
won count, won value. Sort by value desc.
3. **Funnel: Inquiry → Qualified → EOI → Reservation → Won** (vertical
funnel) — Conversion at each stage.
4. **Conversion trend** (line chart) — Inquiry → won conversion %
plotted weekly.
5. **Country of origin** (geo map via `react-simple-maps`, already
approved) — Inquiries by `nationalityIso` of resulting client.
6. **Time-to-respond histogram** — Buckets of "minutes from inquiry to
first contact." Highlights slow response times.
### Tables
1. **Top-converting sources** — Source, count, win rate, total revenue,
avg time-to-close.
2. **Recent inquiries** — Date, source, name, mooring, status (open /
converted / discarded), rep.
3. **Stuck inquiries** — Submitted >X days ago, not yet contacted.
### Filters
- Specific source (drill-down)
- Mooring (which berth pages drive conversion)
- UTM campaign (if/when we add UTM tracking — currently only `source`)
---
## Report 04 — Operational ✅ LOCKED 2026-05-27
**Purpose:** answer "how full are we, how long do tenancies last,
where are operational bottlenecks (signing, occupancy turnover)."
**Conditional behaviour:** half this report (tenancy charts + KPIs)
depends on `tenancies_module_enabled = true`. When the module is off,
those tiles render `—` with a "Tenancies module disabled" hint and
the tenancy charts/tables are omitted entirely (replaced with a
single "Enable tenancies in System Settings to populate this section"
banner).
### KPI strip (7 tiles; some auto-hide)
| # | Tile | Formula | Notes |
| --- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Total berths** | `count(berths) WHERE archivedAt IS NULL` | Physical inventory |
| 2 | **Sold %** | `count(status='sold') / total × 100%` | Period-over-period delta computed from `audit_logs` (entity_type='berth', action='status_changed'). All historical changes incl. accidental/manual ones are reflected — the audit log is the truth source |
| 3 | **Under offer %** | Live compute from `interest_berths`: any berth with an active `is_specific_interest=true` link whose interest has open outcome | Quality-first source; catches drift where `berths.status` column lags the link table |
| 4 | **Active tenancies** | `count(berth_tenancies) WHERE status='active'` | Module-OFF → `—` |
| 5 | **Avg tenancy length** | `median(endDate - startDate)` for `status='ended'` tenancies, in years (1 decimal) | Module-OFF → `—`. Need ≥3 ended tenancies for meaningful median; otherwise `—` with hint |
| 6 | **Signing turnaround (per type)** | `median(document.completedAt - document.sentAt)` per document type | Three small stats in one tile: `EOI 4.2d · Reservation 6.8d · Contract 12.4d`. Excludes voided + declined |
| 7 | **Berths in conflict** | `count(berths WHERE >1 active interest has is_specific_interest=true)` | **Hidden when 0**; appears (and reads red) when ≥1 conflict — the "two clients want the same berth" alarm |
### Charts (7)
1. **Berth utilisation timeline** (echarts heatmap)
- Grid: `area × month`; cell colour = % occupied (sold + under-offer) in that area that month
- **Range:** user-pickable, default trailing 24 months
- Reuses `audit_logs` reconstruction (same engine as KPI 2)
2. **Status mix over time** (recharts stacked area, with **toggle**)
- Two views: proportional (100%-stacked) AND absolute counts
- Toggle button on the chart switches between them
- 3 series: available / under_offer / sold
3. **Tenancy churn waterfall** _(module ON)_ (echarts waterfall)
- Per bucket: `+ new active`, ` ended`, `= net Δ`
- **Bucket: auto-pick** — monthly if avg >2 events/month, else quarterly
4. **Tenure distribution** _(module ON)_ (recharts histogram bar)
- Marina-tuned buckets: `<1y` / `15y` / `510y` / `1020y` / `20y+`
- Ended tenancies only (active ones have no end date yet)
5. **Signing turnaround box plot** (echarts)
- One box per document type (EOI / Reservation / Contract)
- Median + quartiles + whiskers + outlier dots
- Excludes voided + declined
6. **Occupancy by area** (recharts stacked horizontal bar)
- One bar per area; segments coloured sold / under_offer / available
- Scales cleanly to 10+ areas (vs donut-per-area which doesn't)
7. **Documents in pipeline** (recharts stacked bar)
- Per document type, count by current status (`pending` / `sent` / `signed` / `declined` / `voided`)
- Spots stuck batches at a glance
### Tables (4)
1. **Tenancies ending soon** _(module ON)_
- Window: **next 6 months** (default)
- Columns: client · berth · tenure type · end date · days until end · quick action (renew / end)
- Sort: `endDate` asc
2. **Berths with no current owner**
- Threshold: available for **>60 days**
- Columns: mooring · area · dimensions · price · days available · last viewed date (from public berth-page analytics if available)
3. **Stuck signing**
- **Document-type-aware thresholds:** EOI >10d / Reservation >7d / Contract >5d
- Columns: document type · client · sent date · days outstanding · next signer · resend button
4. **Highest-value vacant berths**
- Available berths sorted by `price` desc
- Columns: mooring · area · dimensions · price · days available
- Sales-focus list
### Filters
- **Cross-cutting** (auto-hidden when not relevant): date range + comparison toggle + rep + source
- **Operational-specific:**
- **Berth area** — multi-select; restricts heatmap + tables
- **Tenure type** — permanent / fixed-term (affects tenancy charts + ending-soon table)
- **Document type** — EOI / Reservation / Contract (affects signing chart + stuck-signing)
- **Status filter** — for the heatmap/status-mix views: which statuses to display
### Currency handling
- All berth prices render in port-default currency
- Underlying records can be in any currency; convert at render time
- Render with thousand-separator + currency symbol
---
## Report 05 — Custom (ad-hoc composer)
**Purpose:** answer questions the canonical reports don't cover.
### Composition surface
1. **Pick an entity** (one): Clients, Yachts, Companies, Interests,
Berths, Tenancies, Invoices, Expenses, Documents,
Website Submissions.
2. **Pick columns** — checkbox list of available columns for that
entity, with sensible defaults pre-checked. Includes computed
columns where they exist (e.g. `daysOverdue` on invoices).
3. **Add filters** — one row per filter; each row: column → operator
(=, ≠, in, contains, > <, between, is null) → value picker
appropriate to the column type. AND/OR between rows.
4. **Group by** (optional single dimension) — column from the entity.
5. **Sort** — column + direction.
6. **Aggregate** (when group-by is set) — count, sum, avg, min, max
on each numeric column.
7. **Live preview** — first 50 rows render as you build, server query
re-runs on debounced change.
8. **Save** — three buttons:
- **Run once** — generate the report and add to library, no
template saved.
- **Save as template** — name + scope (personal / port-wide).
- **Update existing template** — only visible if you opened from a
template.
### Permissions
- Column whitelist per entity per role. A rep without
`clients.view_pii` cannot pick `email` or `phone` columns. Same
enforcement on the server-side row filter.
- Filtering is always tenant-scoped via `port_id` (defense in depth).
### Output
- Same export buttons (PDF / CSV / Excel) as canonical reports.
- PDF treatment uses the standard branded shell.
---
## Templates system
Applies to all 5 categories.
### Lifecycle
1. **Open a builder** — defaults to "Untitled" config.
2. **Modify any filter / column / range** — header shows "Modified ●"
indicator.
3. **Save** — three options:
- Overwrite the loaded template (if any).
- Save as new (prompts for name + scope).
- Discard changes.
4. **Templates page** — list of all templates, per-template actions:
open, run, schedule, share, archive.
### Scope
- **Personal** — visible only to creator. Can be promoted to port-wide
later.
- **Port-wide** — visible to all reps in the port; editable only by
admins. "Owned by" name shown.
### Storage
- `report_templates` table already exists (per `schema/reports.ts`),
audit to confirm shape matches the lifecycle above.
---
## Schedules
### Schedule object
- `templateId` — the report to run
- `cron` expression OR friendly cadence (daily 9am, weekly Mondays,
monthly 1st)
- `emailEnabled` — boolean. When true, fires email; when false, only
drops into runs library.
- `recipients` — array of email addresses (only used when
`emailEnabled`)
- `format` — pdf / csv / xlsx — what to attach to the email
- `lastRunAt`, `nextRunAt`, `lastResult` (success / failure)
### Worker
- BullMQ recurring job already exists in the stack; one queue
`report-runs` does both on-demand and scheduled runs.
- Failure surface: email the schedule creator on first failure (with
short error), backoff retry once, mark `lastResult='failure'`.
---
## Open questions for the user
1. **AR aging buckets.** Do we use 30-day buckets or 14-day buckets?
30 is industry standard; 14 catches issues earlier.
2. **Currency normalisation for revenue.** USD or EUR as default? Or
the port's `branding_default_currency`?
3. **Sales rep visibility.** Should a rep see ONLY their own metrics
on Sales Performance by default (with admins seeing the full
leaderboard), or always the full team?
4. **Inquiry → interest auto-link rule.** We've got `convertedAt` on
`websiteSubmissions` and `sourceInquiryId` on `clients`. Is every
conversion captured today, or are some manual links missed (which
would skew the marketing report)?
5. **"Pulse" / heat data.** Should the Sales report surface the deal
pulse metric, or is that a separate "Deal Pulse" report?
6. **Geographic chart.** The `react-simple-maps` library is approved
(per memory). Are we OK to use it for the Marketing country chart,
or is that scope creep?
7. **Custom builder entity scope.** All 10 entities above, or start
with the 4 sales-core ones (Clients, Yachts, Interests, Berths)
and expand later?