fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes; single grouped commit so the index doesn't fragment into 30 one-liners. User & auth: - `user-settings`: name now updates the avatar + topbar menu after save (was reading stale session). - `me/password-reset`: 3 bugs (token validation, error response shape, redirect chain). - Admin user permission-overrides route honours the same envelope as the rest of the admin surface. Dashboard: - Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card` (replaced by the customisable widget grid). - Strip `revenue_breakdown` from analytics route + use-analytics + service + integration test so nothing renders an empty card. - Activity log timeline overshoot fix (`interest-timeline` + `entity-activity-feed`). - Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile. - `dev-mode-banner`: derive dismissed state synchronously instead of via an effect (set-state-in-effect lint rule). Forms & lists (assorted polish): - client / company / yacht / interest / reminder forms — validation + empty-state copy + tab transitions. - companies/yachts list tweaks; berth recommender panel; qualification checklist; supplemental info request button. Infra & misc: - Queue workers (ai / email / notifications) — log shape + per-job timeout consistency. - Auth / brochures / users schema small adjustments; seeds reflect permissions matrix changes. - Scan shell + scanner manifest + AI admin page small fixes. - `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react` (recommended config from echarts-for-react inside Next). Docs: - `docs/superpowers/audits/alpha-uat-master.md` — single rolling cross-cutting UAT findings doc (per CLAUDE.md convention). - `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log normalization (§J). - 2026-05-18 audit log updated with this batch. - `CLAUDE.md` — small manual UAT scaffold notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,6 @@ import { and, between, eq, isNull, sql } from 'drizzle-orm';
|
||||
import { db } from '@/lib/db';
|
||||
import { analyticsSnapshots } from '@/lib/db/schema/insights';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
import { invoices } from '@/lib/db/schema/financial';
|
||||
import { PIPELINE_STAGES } from '@/lib/constants';
|
||||
import {
|
||||
ALL_RANGES,
|
||||
@@ -26,11 +25,7 @@ import {
|
||||
export { ALL_RANGES, isCustomRange, rangeToBounds };
|
||||
export type { DateRange, PresetDateRange, CustomDateRange };
|
||||
|
||||
export type MetricBase =
|
||||
| 'pipeline_funnel'
|
||||
| 'occupancy_timeline'
|
||||
| 'revenue_breakdown'
|
||||
| 'lead_source_attribution';
|
||||
export type MetricBase = 'pipeline_funnel' | 'occupancy_timeline' | 'lead_source_attribution';
|
||||
|
||||
/**
|
||||
* Snapshot key. Only preset ranges are cached - custom ranges have an
|
||||
@@ -41,7 +36,6 @@ export type MetricId = `${MetricBase}.${PresetDateRange}`;
|
||||
export const ALL_METRICS: readonly MetricBase[] = [
|
||||
'pipeline_funnel',
|
||||
'occupancy_timeline',
|
||||
'revenue_breakdown',
|
||||
'lead_source_attribution',
|
||||
] as const;
|
||||
|
||||
@@ -61,19 +55,11 @@ export interface OccupancyTimelineData {
|
||||
points: Array<{ date: string; occupied: number; total: number; occupancyPct: number }>;
|
||||
}
|
||||
|
||||
export interface RevenueBreakdownData {
|
||||
bars: Array<{ status: string; amount: number; currency: string }>;
|
||||
}
|
||||
|
||||
export interface LeadSourceAttributionData {
|
||||
slices: Array<{ source: string; count: number }>;
|
||||
}
|
||||
|
||||
export type SnapshotData =
|
||||
| PipelineFunnelData
|
||||
| OccupancyTimelineData
|
||||
| RevenueBreakdownData
|
||||
| LeadSourceAttributionData;
|
||||
export type SnapshotData = PipelineFunnelData | OccupancyTimelineData | LeadSourceAttributionData;
|
||||
|
||||
// ─── Cache layer ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -219,36 +205,6 @@ export async function computeOccupancyTimeline(
|
||||
return { points };
|
||||
}
|
||||
|
||||
export async function computeRevenueBreakdown(
|
||||
portId: string,
|
||||
range: DateRange,
|
||||
): Promise<RevenueBreakdownData> {
|
||||
const { from, to } = rangeToBounds(range);
|
||||
const rows = await db
|
||||
.select({
|
||||
status: invoices.status,
|
||||
currency: invoices.currency,
|
||||
amount: sql<string>`coalesce(sum(${invoices.total}), 0)::text`,
|
||||
})
|
||||
.from(invoices)
|
||||
.where(
|
||||
and(
|
||||
eq(invoices.portId, portId),
|
||||
isNull(invoices.archivedAt),
|
||||
between(invoices.createdAt, from, to),
|
||||
),
|
||||
)
|
||||
.groupBy(invoices.status, invoices.currency);
|
||||
|
||||
return {
|
||||
bars: rows.map((r) => ({
|
||||
status: r.status,
|
||||
currency: r.currency,
|
||||
amount: Number(r.amount),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function computeLeadSourceAttribution(
|
||||
portId: string,
|
||||
range: DateRange,
|
||||
@@ -307,19 +263,6 @@ export async function getOccupancyTimeline(
|
||||
return fresh;
|
||||
}
|
||||
|
||||
export async function getRevenueBreakdown(
|
||||
portId: string,
|
||||
range: DateRange,
|
||||
): Promise<RevenueBreakdownData> {
|
||||
if (isCustomRange(range)) return computeRevenueBreakdown(portId, range);
|
||||
const metricId = `revenue_breakdown.${range}` as const;
|
||||
const cached = await readSnapshot<RevenueBreakdownData>(portId, metricId);
|
||||
if (cached) return cached;
|
||||
const fresh = await computeRevenueBreakdown(portId, range);
|
||||
await writeSnapshot(portId, metricId, fresh);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
export async function getLeadSourceAttribution(
|
||||
portId: string,
|
||||
range: DateRange,
|
||||
@@ -337,16 +280,14 @@ export async function getLeadSourceAttribution(
|
||||
|
||||
export async function refreshSnapshotsForPort(portId: string): Promise<void> {
|
||||
for (const range of ALL_RANGES) {
|
||||
const [funnel, occupancy, revenue, leadSource] = await Promise.all([
|
||||
const [funnel, occupancy, leadSource] = await Promise.all([
|
||||
computePipelineFunnel(portId, range),
|
||||
computeOccupancyTimeline(portId, range),
|
||||
computeRevenueBreakdown(portId, range),
|
||||
computeLeadSourceAttribution(portId, range),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeSnapshot(portId, `pipeline_funnel.${range}`, funnel),
|
||||
writeSnapshot(portId, `occupancy_timeline.${range}`, occupancy),
|
||||
writeSnapshot(portId, `revenue_breakdown.${range}`, revenue),
|
||||
writeSnapshot(portId, `lead_source_attribution.${range}`, leadSource),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user