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
|
|
|
/**
|
2026-06-02 14:28:51 +02:00
|
|
|
* Custom-report entity registry — server query layer.
|
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
|
|
|
*
|
|
|
|
|
* The custom builder is the catch-all for slices the four canonical
|
|
|
|
|
* reports don't cover — pick an entity, pick columns, optionally
|
|
|
|
|
* filter by date, get a CSV. v1 ships with the four highest-value
|
|
|
|
|
* entities (clients, interests, berths, tenancies); the remaining six
|
|
|
|
|
* from the launch-readiness scope (companies, yachts, invoices,
|
|
|
|
|
* payments, deals, sends) layer in as their schemas are wired.
|
|
|
|
|
*
|
2026-06-02 14:28:51 +02:00
|
|
|
* This module is SERVER-ONLY: it pulls in `@/lib/db` + drizzle to run
|
|
|
|
|
* the underlying queries. The client-safe metadata (entity keys,
|
|
|
|
|
* column allowlists, labels/descriptions, the filter/column type
|
|
|
|
|
* contracts) lives in `registry-meta.ts` and is imported here. Client
|
|
|
|
|
* components (e.g. the column-picker UI) MUST import from
|
|
|
|
|
* `registry-meta.ts`, never this file, or they drag the DB layer into
|
|
|
|
|
* the browser bundle.
|
|
|
|
|
*
|
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
|
|
|
* Each entity defines:
|
2026-06-02 14:28:51 +02:00
|
|
|
* - `columns`: an allowlist of column keys + human labels (sourced
|
|
|
|
|
* from `registry-meta.ts`). The allowlist gates which fields a rep
|
|
|
|
|
* can pull into a CSV, so PII columns can be opt-in per role later.
|
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
|
|
|
* - `runQuery`: a Drizzle select that joins whatever the columns
|
|
|
|
|
* need, applies the port filter + optional date range, and
|
|
|
|
|
* returns raw rows.
|
|
|
|
|
*
|
|
|
|
|
* Adding a new entity:
|
2026-06-02 14:28:51 +02:00
|
|
|
* 1. Append it to ENTITY_KEYS (in registry-meta.ts).
|
|
|
|
|
* 2. Add its column array + ENTITY_META entry in registry-meta.ts.
|
|
|
|
|
* 3. Add the matching `runQuery` + ENTITY_REGISTRY entry here.
|
|
|
|
|
* 4. The UI's entity-picker reads ENTITY_META directly.
|
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
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { and, asc, desc, eq, gte, lte, sql, type SQL } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { berths } from '@/lib/db/schema/berths';
|
|
|
|
|
import { clients } from '@/lib/db/schema/clients';
|
|
|
|
|
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
|
|
|
|
import { berthTenancies as tenancies } from '@/lib/db/schema/tenancies';
|
|
|
|
|
import { STAGE_LABELS, type PipelineStage } from '@/lib/constants';
|
|
|
|
|
|
2026-06-02 14:28:51 +02:00
|
|
|
import {
|
|
|
|
|
ENTITY_KEYS,
|
|
|
|
|
ENTITY_META,
|
|
|
|
|
type ColumnDefinition,
|
|
|
|
|
type CustomEntityMeta,
|
|
|
|
|
type CustomFilter,
|
|
|
|
|
type EntityKey,
|
|
|
|
|
} from './registry-meta';
|
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
|
|
|
|
2026-06-02 14:28:51 +02:00
|
|
|
// Re-export the client-safe contracts so existing SERVER imports of
|
|
|
|
|
// this module keep working unchanged.
|
|
|
|
|
export { ENTITY_KEYS };
|
|
|
|
|
export type { ColumnDefinition, CustomFilter, EntityKey };
|
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
|
|
|
|
2026-06-02 14:28:51 +02:00
|
|
|
/**
|
|
|
|
|
* Full server-side entity definition: the client-safe metadata plus
|
|
|
|
|
* the server-only `runQuery`.
|
|
|
|
|
*/
|
|
|
|
|
export interface CustomEntityDefinition extends CustomEntityMeta {
|
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
|
|
|
/** Execute the underlying query and return raw rows keyed by column
|
|
|
|
|
* key. The runner is responsible for the joins + port scoping;
|
|
|
|
|
* callers only pass which columns they want + the filter. */
|
|
|
|
|
runQuery: (input: {
|
|
|
|
|
portId: string;
|
|
|
|
|
columns: string[];
|
|
|
|
|
filter: CustomFilter;
|
|
|
|
|
}) => Promise<Array<Record<string, unknown>>>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function applyDateRange(column: ReturnType<typeof sql<Date>>, filter: CustomFilter): SQL[] {
|
|
|
|
|
const conds: SQL[] = [];
|
|
|
|
|
if (filter.from) conds.push(gte(column as never, filter.from));
|
|
|
|
|
if (filter.to) conds.push(lte(column as never, filter.to));
|
|
|
|
|
return conds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Clients ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function runClientsQuery({
|
|
|
|
|
portId,
|
|
|
|
|
filter,
|
|
|
|
|
}: {
|
|
|
|
|
portId: string;
|
|
|
|
|
columns: string[];
|
|
|
|
|
filter: CustomFilter;
|
|
|
|
|
}): Promise<Array<Record<string, unknown>>> {
|
|
|
|
|
const conds = [eq(clients.portId, portId), ...applyDateRange(clients.createdAt as never, filter)];
|
|
|
|
|
const rows = await db
|
|
|
|
|
.select({
|
|
|
|
|
fullName: clients.fullName,
|
|
|
|
|
nationalityIso: clients.nationalityIso,
|
|
|
|
|
preferredLanguage: clients.preferredLanguage,
|
|
|
|
|
preferredContactMethod: clients.preferredContactMethod,
|
|
|
|
|
source: clients.source,
|
|
|
|
|
createdAt: clients.createdAt,
|
|
|
|
|
archivedAt: clients.archivedAt,
|
|
|
|
|
})
|
|
|
|
|
.from(clients)
|
|
|
|
|
.where(and(...conds))
|
|
|
|
|
.orderBy(asc(clients.fullName))
|
|
|
|
|
.limit(10_000);
|
|
|
|
|
return rows.map((r) => ({ ...r }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Interests ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function runInterestsQuery({
|
|
|
|
|
portId,
|
|
|
|
|
filter,
|
|
|
|
|
}: {
|
|
|
|
|
portId: string;
|
|
|
|
|
columns: string[];
|
|
|
|
|
filter: CustomFilter;
|
|
|
|
|
}): Promise<Array<Record<string, unknown>>> {
|
|
|
|
|
const conds = [
|
|
|
|
|
eq(interests.portId, portId),
|
|
|
|
|
...applyDateRange(interests.createdAt as never, filter),
|
|
|
|
|
];
|
|
|
|
|
const rows = await db
|
|
|
|
|
.select({
|
|
|
|
|
clientName: clients.fullName,
|
|
|
|
|
primaryBerth: berths.mooringNumber,
|
|
|
|
|
pipelineStage: interests.pipelineStage,
|
|
|
|
|
leadCategory: interests.leadCategory,
|
|
|
|
|
outcome: interests.outcome,
|
|
|
|
|
source: interests.source,
|
|
|
|
|
depositExpectedAmount: interests.depositExpectedAmount,
|
|
|
|
|
depositExpectedCurrency: interests.depositExpectedCurrency,
|
|
|
|
|
dateFirstContact: interests.dateFirstContact,
|
|
|
|
|
dateLastContact: interests.dateLastContact,
|
|
|
|
|
createdAt: interests.createdAt,
|
|
|
|
|
})
|
|
|
|
|
.from(interests)
|
|
|
|
|
.innerJoin(clients, eq(interests.clientId, clients.id))
|
|
|
|
|
.leftJoin(
|
|
|
|
|
interestBerths,
|
|
|
|
|
and(eq(interestBerths.interestId, interests.id), eq(interestBerths.isPrimary, true)),
|
|
|
|
|
)
|
|
|
|
|
.leftJoin(berths, eq(interestBerths.berthId, berths.id))
|
|
|
|
|
.where(and(...conds))
|
|
|
|
|
.orderBy(desc(interests.createdAt))
|
|
|
|
|
.limit(10_000);
|
|
|
|
|
return rows.map((r) => ({
|
|
|
|
|
...r,
|
|
|
|
|
// Re-label stage to the human form so the CSV is readable;
|
|
|
|
|
// analysts can still join back via the raw enum on display.
|
|
|
|
|
pipelineStage: r.pipelineStage
|
|
|
|
|
? (STAGE_LABELS[r.pipelineStage as PipelineStage] ?? r.pipelineStage)
|
|
|
|
|
: null,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Berths ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function runBerthsQuery({
|
|
|
|
|
portId,
|
|
|
|
|
filter,
|
|
|
|
|
}: {
|
|
|
|
|
portId: string;
|
|
|
|
|
columns: string[];
|
|
|
|
|
filter: CustomFilter;
|
|
|
|
|
}): Promise<Array<Record<string, unknown>>> {
|
|
|
|
|
const conds = [eq(berths.portId, portId), ...applyDateRange(berths.createdAt as never, filter)];
|
|
|
|
|
const rows = await db
|
|
|
|
|
.select({
|
|
|
|
|
mooringNumber: berths.mooringNumber,
|
|
|
|
|
area: berths.area,
|
|
|
|
|
status: berths.status,
|
|
|
|
|
length: berths.lengthM,
|
|
|
|
|
width: berths.widthM,
|
|
|
|
|
draft: berths.draftM,
|
|
|
|
|
price: berths.price,
|
|
|
|
|
priceCurrency: berths.priceCurrency,
|
|
|
|
|
createdAt: berths.createdAt,
|
|
|
|
|
})
|
|
|
|
|
.from(berths)
|
|
|
|
|
.where(and(...conds))
|
|
|
|
|
.orderBy(asc(berths.mooringNumber))
|
|
|
|
|
.limit(10_000);
|
|
|
|
|
return rows.map((r) => ({ ...r }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Tenancies ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function runTenanciesQuery({
|
|
|
|
|
portId,
|
|
|
|
|
filter,
|
|
|
|
|
}: {
|
|
|
|
|
portId: string;
|
|
|
|
|
columns: string[];
|
|
|
|
|
filter: CustomFilter;
|
|
|
|
|
}): Promise<Array<Record<string, unknown>>> {
|
|
|
|
|
const conds = [
|
|
|
|
|
eq(tenancies.portId, portId),
|
|
|
|
|
...applyDateRange(tenancies.createdAt as never, filter),
|
|
|
|
|
];
|
|
|
|
|
const rows = await db
|
|
|
|
|
.select({
|
|
|
|
|
clientName: clients.fullName,
|
|
|
|
|
mooringNumber: berths.mooringNumber,
|
|
|
|
|
tenureType: tenancies.tenureType,
|
|
|
|
|
startDate: tenancies.startDate,
|
|
|
|
|
endDate: tenancies.endDate,
|
|
|
|
|
status: tenancies.status,
|
|
|
|
|
createdAt: tenancies.createdAt,
|
|
|
|
|
})
|
|
|
|
|
.from(tenancies)
|
|
|
|
|
.leftJoin(clients, eq(tenancies.clientId, clients.id))
|
|
|
|
|
.leftJoin(berths, eq(tenancies.berthId, berths.id))
|
|
|
|
|
.where(and(...conds))
|
|
|
|
|
.orderBy(desc(tenancies.startDate))
|
|
|
|
|
.limit(10_000);
|
|
|
|
|
return rows.map((r) => ({ ...r }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Registry ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-02 14:28:51 +02:00
|
|
|
const RUN_QUERIES: Record<EntityKey, CustomEntityDefinition['runQuery']> = {
|
|
|
|
|
clients: runClientsQuery,
|
|
|
|
|
interests: runInterestsQuery,
|
|
|
|
|
berths: runBerthsQuery,
|
|
|
|
|
tenancies: runTenanciesQuery,
|
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
|
|
|
};
|
2026-06-02 14:28:51 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Full server registry: client-safe metadata (from `registry-meta.ts`)
|
|
|
|
|
* composed with the matching `runQuery` for each entity.
|
|
|
|
|
*/
|
|
|
|
|
export const ENTITY_REGISTRY: Record<EntityKey, CustomEntityDefinition> = ENTITY_KEYS.reduce(
|
|
|
|
|
(acc, key) => {
|
|
|
|
|
acc[key] = { ...ENTITY_META[key], runQuery: RUN_QUERIES[key] };
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{} as Record<EntityKey, CustomEntityDefinition>,
|
|
|
|
|
);
|