Files
pn-new-crm/src/app/api/v1/interests/[id]/timeline/route.ts

214 lines
7.7 KiB
TypeScript
Raw Normal View History

import { NextResponse } from 'next/server';
import { and, eq, desc, inArray } from 'drizzle-orm';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse, NotFoundError } from '@/lib/errors';
import { db } from '@/lib/db';
import { interests } from '@/lib/db/schema/interests';
import { auditLogs } from '@/lib/db/schema/system';
import { documents, documentEvents } from '@/lib/db/schema/documents';
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
import { user } from '@/lib/db/schema/users';
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
import { stageLabel } from '@/lib/constants';
const OUTCOME_LABELS: Record<string, string> = {
won: 'Won',
lost_other_marina: 'Lost — went to another marina',
lost_unqualified: 'Lost — unqualified',
lost_no_response: 'Lost — no response',
cancelled: 'Cancelled',
};
const DOC_EVENT_LABELS: Record<string, string> = {
sent: 'sent for signing',
completed: 'fully signed',
signed: 'signed by recipient',
rejected: 'rejected',
expired: 'expired',
cancelled: 'cancelled',
reminder_sent: 'reminder sent',
};
interface TimelineEvent {
id: string;
type: 'audit' | 'document_event';
action: string;
description: string;
userId: string | null;
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
/** Resolved display name for `userId`. `'system'` for auto-events; null when
* the user has been deleted or the event has no actor. Falls back to
* email-localpart if the user has no display name. */
userName: string | null;
createdAt: Date;
metadata: Record<string, unknown>;
}
// GET /api/v1/interests/[id]/timeline
export const GET = withAuth(
withPermission('interests', 'view', async (req, ctx, params) => {
try {
const interestId = params.id!;
const interest = await db.query.interests.findFirst({
where: and(eq(interests.id, interestId), eq(interests.portId, ctx.portId)),
});
if (!interest) throw new NotFoundError('Interest');
// Fetch audit logs for this interest
const auditRows = await db
.select()
.from(auditLogs)
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
.where(and(eq(auditLogs.entityType, 'interest'), eq(auditLogs.entityId, interestId)))
.orderBy(desc(auditLogs.createdAt))
.limit(50);
// Fetch document events for documents linked to this interest
const interestDocs = await db
.select({ id: documents.id, title: documents.title })
.from(documents)
.where(eq(documents.interestId, interestId));
const docIds = interestDocs.map((d) => d.id);
const docEventRows =
docIds.length > 0
? await db
.select({
id: documentEvents.id,
documentId: documentEvents.documentId,
eventType: documentEvents.eventType,
eventData: documentEvents.eventData,
createdAt: documentEvents.createdAt,
})
.from(documentEvents)
.where(inArray(documentEvents.documentId, docIds))
.orderBy(desc(documentEvents.createdAt))
.limit(50)
: [];
const docTitles = Object.fromEntries(interestDocs.map((d) => [d.id, d.title]));
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
// Resolve display names for any `userId` that is a real user row (the
// sentinel value 'system' is used for auto-events and isn't joined).
const realUserIds = Array.from(
new Set(auditRows.map((r) => r.userId).filter((u): u is string => !!u && u !== 'system')),
);
const userRows =
realUserIds.length > 0
? await db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.where(inArray(user.id, realUserIds))
: [];
const userNameById = new Map<string, string>(
userRows.map((u) => [u.id, u.name?.trim() || u.email.split('@')[0] || 'User']),
);
const resolveUserName = (userId: string | null): string | null => {
if (!userId) return null;
if (userId === 'system') return 'system';
return userNameById.get(userId) ?? null;
};
// Union and sort
const auditEvents: TimelineEvent[] = auditRows.map((row) => ({
id: row.id,
type: 'audit',
action: row.action,
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
description: buildAuditDescription(
row.action,
row.newValue as Record<string, unknown> | null,
(row.metadata as Record<string, unknown>) ?? {},
row.userId,
),
userId: row.userId,
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
userName: resolveUserName(row.userId),
createdAt: row.createdAt,
metadata: (row.metadata as Record<string, unknown>) ?? {},
}));
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
const docEvents: TimelineEvent[] = docEventRows.map((row) => {
const title = docTitles[row.documentId] ?? row.documentId;
const action = DOC_EVENT_LABELS[row.eventType] ?? row.eventType;
return {
id: row.id,
type: 'document_event',
action: row.eventType,
description: `Document "${title}" ${action}`,
userId: null,
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
userName: null,
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
createdAt: row.createdAt,
metadata: (row.eventData as Record<string, unknown>) ?? {},
};
});
const allEvents = [...auditEvents, ...docEvents];
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
// Fallback: when no audit-log entries exist for this interest (typical
// for seed/imported data inserted directly into the table without going
// through the service), synthesize a "Created at <stage>" event so the
// tab isn't empty when the interest is clearly past `open`.
const hasCreateAudit = allEvents.some((e) => e.action === 'create');
if (!hasCreateAudit) {
const stage = stageLabel(interest.pipelineStage);
const created = interest.createdAt ?? new Date();
allEvents.push({
id: `synth-${interest.id}-create`,
type: 'audit',
action: 'create',
description:
interest.pipelineStage === 'open' ? 'Interest created' : `Interest created at ${stage}`,
userId: null,
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton - Documents hub signer status now renders via a label map (`Pending`, `Signed`, `Declined`, …) instead of the raw lowercase enum value. - Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy` via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and swaps the "Payment Method" free-text input for a `Select` of labelled options (`Bank transfer`, `Credit card`, …) so we never store `bank_transfer` from a hand-typed field again. - Interest tabs `MilestoneSection` status badge uses a `humanizeStatus` helper so values like `waiting_for_signatures` show as `Waiting For Signatures` (correctly title-cased) instead of being a lower-snake-case fragment inside an ALL-CAPS pill. - `OUTCOME_BADGE` in the interest header now has a fall-through that renders any unknown outcome as a closed-state badge, preventing a closed interest from looking open just because its enum was added upstream without a matching label entry. - Interest timeline route joins the `user` table and returns `userName` alongside `userId`; the client renders the resolved name instead of a 36-char UUID. Falls back to `'a teammate'` if the user row was deleted. - Invoice "New / Step 3 — Review" replaces the truncated UUID display with a server-resolved client/company name via a small `useQuery`, so users can confirm they picked the right billing entity before submitting. - New `loading.tsx` for client detail renders a header / tab strip / card skeleton during the server-component / initial-query window that previously flashed empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
userName: null,
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
createdAt: created,
metadata: { synthetic: true },
});
}
allEvents.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return NextResponse.json({ data: allEvents.slice(0, 50) });
} catch (error) {
return errorResponse(error);
}
}),
);
function buildAuditDescription(
action: string,
newValue: Record<string, unknown> | null,
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
metadata: Record<string, unknown>,
userId: string | null,
): string {
if (action === 'create') return 'Interest created';
if (action === 'archive') return 'Interest archived';
if (action === 'restore') return 'Interest restored';
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
const type = metadata.type;
if (type === 'outcome_set') {
const outcomeKey = (newValue?.outcome as string | undefined) ?? '';
const label = OUTCOME_LABELS[outcomeKey] ?? outcomeKey ?? 'Closed';
const reason = (newValue?.reason as string | undefined) ?? '';
return reason ? `Marked as ${label}${reason}` : `Marked as ${label}`;
}
if (type === 'outcome_cleared') {
const stage = (newValue?.pipelineStage as string | undefined) ?? '';
return stage ? `Reopened to ${stageLabel(stage)}` : 'Reopened';
}
if (type === 'stage_change' && newValue?.pipelineStage) {
const stage = stageLabel(newValue.pipelineStage as string);
const reason = (newValue.reason as string | undefined) ?? '';
const auto = userId === 'system';
if (auto) {
return reason ? `${stage} (auto-advanced — ${reason})` : `Stage advanced to ${stage}`;
}
return reason ? `Stage changed to ${stage}${reason}` : `Stage changed to ${stage}`;
}
if (action === 'update' && newValue?.pipelineStage) {
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
return `Stage changed to ${stageLabel(newValue.pipelineStage as string)}`;
}
if (action === 'update') return 'Interest updated';
return action;
}