diff --git a/src/components/interests/interest-detail-header.tsx b/src/components/interests/interest-detail-header.tsx
index 9ca16b6..2f682c9 100644
--- a/src/components/interests/interest-detail-header.tsx
+++ b/src/components/interests/interest-detail-header.tsx
@@ -2,9 +2,20 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { Pencil, Archive, RotateCcw, Trophy, XCircle, RefreshCcw } from 'lucide-react';
+import {
+ Pencil,
+ Archive,
+ RotateCcw,
+ Trophy,
+ XCircle,
+ RefreshCcw,
+ Mail,
+ MessageCircle,
+ Phone,
+} from 'lucide-react';
import Link from 'next/link';
+import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { TagBadge } from '@/components/shared/tag-badge';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
@@ -36,6 +47,12 @@ interface InterestDetailHeaderProps {
id: string;
clientId: string;
clientName: string | null;
+ /** Primary contact channels resolved from the linked client. The header
+ * uses these to render Email / Call / WhatsApp buttons so the rep
+ * doesn't have to navigate to the client page just to reach out. */
+ clientPrimaryEmail?: string | null;
+ clientPrimaryPhone?: string | null;
+ clientPrimaryPhoneE164?: string | null;
berthId: string | null;
berthMooringNumber: string | null;
pipelineStage: string;
@@ -71,6 +88,16 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
const outcomeBadge = interest.outcome ? OUTCOME_BADGE[interest.outcome] : null;
const isClosed = !!interest.outcome;
+ // Contact deep-links — resolved from the linked client's primary channels.
+ // wa.me requires the digits-only E.164 number (no leading "+"); fall back to
+ // stripping non-digits from the display value when the canonical form is
+ // missing.
+ const whatsappNumber = interest.clientPrimaryPhoneE164
+ ? interest.clientPrimaryPhoneE164.replace(/^\+/, '')
+ : interest.clientPrimaryPhone
+ ? interest.clientPrimaryPhone.replace(/[^\d]/g, '')
+ : null;
+
const reopenMutation = useMutation({
mutationFn: () =>
apiFetch(`/api/v1/interests/${interest.id}/outcome`, { method: 'DELETE', body: {} }),
@@ -200,6 +227,65 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
))}
)}
+
+ {/* Contact deep-links — let the rep email / call / WhatsApp the
+ client without leaving the interest workspace. Resolved from
+ the linked client's primary contact channels (server-side
+ fetch in getInterestById). */}
+ {interest.clientPrimaryEmail || interest.clientPrimaryPhone || whatsappNumber ? (
+
+ {interest.clientPrimaryEmail ? (
+
+ ) : null}
+ {interest.clientPrimaryPhone ? (
+
+ ) : null}
+ {whatsappNumber ? (
+
+ ) : null}
+
+ ) : null}
{/* Top-right actions. Won/Lost are sales-critical and read as text
diff --git a/src/components/interests/interest-detail.tsx b/src/components/interests/interest-detail.tsx
index be2f7c6..701d574 100644
--- a/src/components/interests/interest-detail.tsx
+++ b/src/components/interests/interest-detail.tsx
@@ -16,6 +16,18 @@ interface InterestData {
portId: string;
clientId: string;
clientName: string | null;
+ /** Linked client's primary email (display value). Powers the header
+ * "Email" button and the EOI prereq checklist. */
+ clientPrimaryEmail: string | null;
+ /** Linked client's primary phone (display value). Powers the header
+ * "Call" button. */
+ clientPrimaryPhone: string | null;
+ /** Linked client's primary phone in E.164 form ("+1XXXXXXXXXX"). Used
+ * by wa.me to assemble the WhatsApp deep-link. */
+ clientPrimaryPhoneE164: string | null;
+ /** True when the linked client has any primary address row. Used by
+ * the EOI prereq checklist on the Documents tab. */
+ clientHasAddress: boolean;
berthId: string | null;
berthMooringNumber: string | null;
pipelineStage: string;
@@ -39,6 +51,8 @@ interface InterestData {
archivedAt: string | null;
createdAt: string;
updatedAt: string;
+ outcome?: string | null;
+ outcomeReason?: string | null;
tags: Array<{ id: string; name: string; color: string }>;
}
diff --git a/src/lib/services/interests.service.ts b/src/lib/services/interests.service.ts
index ddca80a..de4824f 100644
--- a/src/lib/services/interests.service.ts
+++ b/src/lib/services/interests.service.ts
@@ -282,9 +282,10 @@ export async function getInterestById(id: string, portId: string) {
.from(clients)
.where(eq(clients.id, interest.clientId));
- // EOI prerequisites: surface enough of the linked client's primary contact
- // and address so the Documents tab can show the readiness checklist
- // (Required: name, email, address — Section 2 of the EOI document).
+ // EOI prerequisites + interest-detail header contact actions: surface the
+ // linked client's primary email/phone (and the canonical E.164 form for
+ // wa.me) so the header can render Email / Call / WhatsApp buttons without
+ // a second fetch, and the Documents tab can show the EOI prereq checklist.
const [emailContact] = await db
.select({ value: clientContacts.value })
.from(clientContacts)
@@ -292,6 +293,18 @@ export async function getInterestById(id: string, portId: string) {
.orderBy(desc(clientContacts.isPrimary), desc(clientContacts.updatedAt))
.limit(1);
+ const [phoneContact] = await db
+ .select({ value: clientContacts.value, valueE164: clientContacts.valueE164 })
+ .from(clientContacts)
+ .where(
+ and(
+ eq(clientContacts.clientId, interest.clientId),
+ inArray(clientContacts.channel, ['phone', 'whatsapp']),
+ ),
+ )
+ .orderBy(desc(clientContacts.isPrimary), desc(clientContacts.updatedAt))
+ .limit(1);
+
const [addressRow] = await db
.select({ id: clientAddresses.id })
.from(clientAddresses)
@@ -319,6 +332,8 @@ export async function getInterestById(id: string, portId: string) {
...interest,
clientName: clientRow?.fullName ?? null,
clientPrimaryEmail: emailContact?.value ?? null,
+ clientPrimaryPhone: phoneContact?.value ?? null,
+ clientPrimaryPhoneE164: phoneContact?.valueE164 ?? null,
clientHasAddress: !!addressRow,
berthMooringNumber,
tags: tagRows,