feat(uat-b1): ship Wave A-E of Bucket 1 audit findings
Wave A (Interest+EOI form quick wins): - Auto-select yacht after inline-create from interest form - EOI generate dialog: "View EOI" action toast - Interest form berth picker: formatBerthRange compact label - Remove "Generate EOI" button from Documents tab (clean removal) - Interest auto-assign: only sales_agent/sales_manager auto-claim ownership on create (explicit role check via user_port_roles join) - LinkedBerthRowItem dims: drop "D" suffix + "L × W" format - ExternalEoiUploadDialog: prefillSignatories prop threaded from active EOI signers - EOI signature progress on Overview milestone card footer Wave B (a11y + i18n sweeps): - aria-live on supplemental-info error state - text-[10px] -> text-xs in client-pipeline-summary - Currency formatter: locale default removed (Intl uses runtime) - en-US/en-GB hardcoded toLocaleString swept across 13 components Wave C (Primary berth always in EOI bundle): - Service guard strengthened on update path - Migration 0083 backfills historical primary rows Wave D (Onboarding super_admin discoverability): - /api/v1/admin/onboarding/status endpoint + shared service - Topbar OnboardingBanner (super_admin, session-dismissible) - OnboardingTile dashboard widget (rail group, self-hides at 100%) - Celebration toast + invalidate of shared status on last tick Wave E (Branded post-completion email idempotency): - Verified handleDocumentCompleted already owns the email fan-out - Added regression test for the polling path + idempotency Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { Anchor, CheckCircle2, Circle, FileSignature, Send, Wallet } from 'lucide-react';
|
||||
|
||||
@@ -31,6 +31,7 @@ import { RemindersInline } from '@/components/reminders/reminders-inline';
|
||||
import { BerthRecommenderPanel } from '@/components/interests/berth-recommender-panel';
|
||||
import { LinkedBerthsList } from '@/components/interests/linked-berths-list';
|
||||
import { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog';
|
||||
import { SigningProgress } from '@/components/documents/signing-progress';
|
||||
|
||||
// Shared parser for the interest's stringly-typed numeric columns (Drizzle
|
||||
// returns Postgres numeric as string). Used by both the Overview milestone
|
||||
@@ -627,6 +628,76 @@ function FutureMilestones({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact per-signer progress widget for the Overview tab's EOI milestone
|
||||
* card. Mounts inside the milestone footer when the EOI is sent but not
|
||||
* yet fully signed, so reps see who's signed at a glance without leaving
|
||||
* Overview. Heavy lifting (resend, void, etc.) stays on the EOI tab — a
|
||||
* "View EOI" link below the widget routes the rep there.
|
||||
*/
|
||||
function EoiMilestoneActiveProgress({
|
||||
interestId,
|
||||
portSlug,
|
||||
}: {
|
||||
interestId: string;
|
||||
portSlug: string;
|
||||
}) {
|
||||
const { data: docsRes } = useQuery<{
|
||||
data: Array<{ id: string; status: string }>;
|
||||
}>({
|
||||
queryKey: ['documents', { interestId, documentType: 'eoi' }],
|
||||
queryFn: () =>
|
||||
apiFetch<{ data: Array<{ id: string; status: string }> }>(
|
||||
`/api/v1/documents?interestId=${interestId}&documentType=eoi`,
|
||||
),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const activeDoc = (docsRes?.data ?? []).find(
|
||||
(d) => d.status === 'sent' || d.status === 'partially_signed' || d.status === 'signed',
|
||||
);
|
||||
const { data: signersRes } = useQuery<{
|
||||
data: Array<{
|
||||
id: string;
|
||||
signerName: string;
|
||||
signerEmail: string;
|
||||
signerRole: string;
|
||||
signingOrder: number;
|
||||
status: string;
|
||||
signedAt?: string | null;
|
||||
invitedAt?: string | null;
|
||||
openedAt?: string | null;
|
||||
lastReminderSentAt?: string | null;
|
||||
signingUrl?: string | null;
|
||||
}>;
|
||||
}>({
|
||||
queryKey: ['documents', activeDoc?.id, 'signers'],
|
||||
queryFn: () =>
|
||||
apiFetch<{ data: Array<never> }>(`/api/v1/documents/${activeDoc!.id}/signers`) as never,
|
||||
enabled: !!activeDoc,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
if (!activeDoc) return null;
|
||||
const signers = signersRes?.data ?? [];
|
||||
if (signers.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<SigningProgress documentId={activeDoc.id} signers={signers} />
|
||||
<div className="flex justify-end">
|
||||
<Button asChild type="button" size="sm" variant="ghost" className="h-7 text-xs">
|
||||
<Link
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
href={`/${portSlug}/interests/${interestId}?tab=eoi` as any}
|
||||
>
|
||||
View EOI →
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewTab({
|
||||
interestId,
|
||||
interest,
|
||||
@@ -863,6 +934,8 @@ function OverviewTab({
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : eoiPhase === 'current' && interest.dateEoiSent && !interest.dateEoiSigned ? (
|
||||
<EoiMilestoneActiveProgress interestId={interestId} portSlug={portSlug} />
|
||||
) : null,
|
||||
pastSummary: interest.dateEoiSigned ? (
|
||||
`Signed ${formatDate(interest.dateEoiSigned)}`
|
||||
|
||||
Reference in New Issue
Block a user