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>
This commit is contained in:
524
docs/reports-content-spec.md
Normal file
524
docs/reports-content-spec.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# 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` / `1–5y` / `5–10y` / `10–20y` / `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?
|
||||
Reference in New Issue
Block a user