@wealthx/shadcn 1.5.24 → 1.5.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +144 -144
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-RYGZRDP6.mjs → chunk-4FJC64FV.mjs} +468 -410
- package/dist/{chunk-FGHM34AV.mjs → chunk-7JVKSZ4O.mjs} +1 -1
- package/dist/{chunk-6HTE24TP.mjs → chunk-EY5YPFKX.mjs} +1 -1
- package/dist/{chunk-3HFOSFOM.mjs → chunk-IG7DEIWU.mjs} +4 -4
- package/dist/{chunk-2VTOF7PW.mjs → chunk-K2KX3NX7.mjs} +1 -1
- package/dist/{chunk-NXZ2F4JA.mjs → chunk-KGBLORCQ.mjs} +1 -1
- package/dist/{chunk-2SDEURIQ.mjs → chunk-PX2B3Q3A.mjs} +7 -7
- package/dist/{chunk-66NM4AX2.mjs → chunk-QM7LU2BR.mjs} +10 -10
- package/dist/{chunk-4UCRTTVL.mjs → chunk-RNLIZRAK.mjs} +1 -1
- package/dist/{chunk-BWG7AX6X.mjs → chunk-SQ54W3JH.mjs} +1 -1
- package/dist/{chunk-RJHE3V4M.mjs → chunk-UMR3HVZF.mjs} +4 -4
- package/dist/{chunk-JGUC3KCA.mjs → chunk-VNB5E7SI.mjs} +5 -5
- package/dist/{chunk-ETT5JAXF.mjs → chunk-Z5QI7CW2.mjs} +1 -1
- package/dist/components/ui/about-you-form.mjs +4 -4
- package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
- package/dist/components/ui/{ai-conversations.js → ai-conversations/index.js} +818 -763
- package/dist/components/ui/ai-conversations/index.mjs +42 -0
- package/dist/components/ui/appointment-action-dialogs.mjs +1 -1
- package/dist/components/ui/appointment-availability-settings.mjs +5 -5
- package/dist/components/ui/appointment-book-dialog.mjs +4 -4
- package/dist/components/ui/appointment-detail-sheet.mjs +3 -3
- package/dist/components/ui/appointment-upcoming-card.mjs +1 -1
- package/dist/components/ui/assets-liabilities-side-card.mjs +6 -6
- package/dist/components/ui/backoffice-signup-steps.mjs +4 -4
- package/dist/components/ui/bank-statement-generate-dialog.mjs +6 -6
- package/dist/components/ui/contact-alert-dialog/index.mjs +2 -2
- package/dist/components/ui/create-contact-modal.mjs +3 -3
- package/dist/components/ui/date-picker.mjs +2 -2
- package/dist/components/ui/expense-detail-item.mjs +3 -3
- package/dist/components/ui/expense-work-details.mjs +9 -9
- package/dist/components/ui/file-preview-dialog.mjs +2 -2
- package/dist/components/ui/financial-drawers.mjs +2 -2
- package/dist/components/ui/form-primitives.mjs +2 -2
- package/dist/components/ui/frontend-signup-steps.mjs +5 -5
- package/dist/components/ui/income-work-details.mjs +3 -3
- package/dist/components/ui/kanban-column.mjs +4 -4
- package/dist/components/ui/opportunity-card.mjs +1 -1
- package/dist/components/ui/opportunity-edit-modals.mjs +6 -6
- package/dist/components/ui/opportunity-summary-tab.mjs +8 -8
- package/dist/components/ui/pipeline-board.mjs +6 -6
- package/dist/components/ui/pipeline-dialogs.mjs +5 -5
- package/dist/components/ui/property-report-dialog.mjs +4 -4
- package/dist/components/ui/savings-goal-modal.mjs +4 -4
- package/dist/components/ui/share-details-dialog.mjs +2 -2
- package/dist/components/ui/signup-form-primitives.mjs +1 -1
- package/dist/index.js +4913 -4858
- package/dist/index.mjs +160 -160
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/components/index.tsx +1 -0
- package/src/components/ui/ai-conversations/helpers.tsx +31 -0
- package/src/components/ui/ai-conversations/index.tsx +468 -0
- package/src/components/ui/ai-conversations/lead-panel.tsx +517 -0
- package/src/components/ui/ai-conversations/list.tsx +335 -0
- package/src/components/ui/ai-conversations/thread.tsx +919 -0
- package/src/components/ui/ai-conversations/types.ts +83 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +1 -1
- package/dist/components/ui/ai-conversations.mjs +0 -42
- package/src/components/ui/ai-conversations.tsx +0 -2270
- package/dist/{chunk-A43XIVO6.mjs → chunk-2PWTXE66.mjs} +3 -3
- package/dist/{chunk-PSBQ4I3M.mjs → chunk-53ZB2JWT.mjs} +6 -6
- package/dist/{chunk-G6RCC2SF.mjs → chunk-5ST6BK7R.mjs} +3 -3
- package/dist/{chunk-H5NI6ZIU.mjs → chunk-734FOOJC.mjs} +3 -3
- package/dist/{chunk-ONYADWSO.mjs → chunk-ABVCQWDY.mjs} +3 -3
- package/dist/{chunk-AADJ5IT6.mjs → chunk-ECC2LLZM.mjs} +3 -3
- package/dist/{chunk-DK55HZPN.mjs → chunk-KENLXFJ7.mjs} +6 -6
- package/dist/{chunk-ET4MTPIY.mjs → chunk-LDC6V6DJ.mjs} +3 -3
- package/dist/{chunk-3S4XQTAL.mjs → chunk-SO4RB3XB.mjs} +9 -9
- package/dist/{chunk-UMF6LLQK.mjs → chunk-TRM3KIHT.mjs} +3 -3
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ArrowLeft,
|
|
4
|
+
Briefcase,
|
|
5
|
+
Calendar,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
HelpCircle,
|
|
9
|
+
Link2,
|
|
10
|
+
Mail,
|
|
11
|
+
MapPin,
|
|
12
|
+
Navigation,
|
|
13
|
+
Phone,
|
|
14
|
+
PhoneCall,
|
|
15
|
+
Plus,
|
|
16
|
+
UserPlus,
|
|
17
|
+
Video,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
import { cn } from "@/lib/utils";
|
|
20
|
+
import { Badge } from "@/components/ui/badge";
|
|
21
|
+
import { Button } from "@/components/ui/button";
|
|
22
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
23
|
+
import {
|
|
24
|
+
Tooltip,
|
|
25
|
+
TooltipContent,
|
|
26
|
+
TooltipTrigger,
|
|
27
|
+
} from "@/components/ui/tooltip";
|
|
28
|
+
import type {
|
|
29
|
+
AiConvAppointmentData,
|
|
30
|
+
AiConvContact,
|
|
31
|
+
AiConvDataField,
|
|
32
|
+
AiConvMeetingType,
|
|
33
|
+
} from "./types";
|
|
34
|
+
import {
|
|
35
|
+
ContactAvatar,
|
|
36
|
+
displayContactName,
|
|
37
|
+
PANEL_HEADER_HEIGHT,
|
|
38
|
+
} from "./helpers.tsx";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// AICollectedDataSection
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface AICollectedDataSectionProps {
|
|
45
|
+
fields: AiConvDataField[];
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function AICollectedDataSection({
|
|
50
|
+
fields,
|
|
51
|
+
className,
|
|
52
|
+
}: AICollectedDataSectionProps) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("flex flex-col", className)}>
|
|
55
|
+
{fields.map((field, i) => (
|
|
56
|
+
<div
|
|
57
|
+
key={i}
|
|
58
|
+
className="flex items-center justify-between gap-2 border-b border-border/40 py-1.5 last:border-b-0"
|
|
59
|
+
>
|
|
60
|
+
<span className="shrink-0 text-sm text-muted-foreground">
|
|
61
|
+
{field.label}
|
|
62
|
+
</span>
|
|
63
|
+
<div className="flex items-center gap-1.5">
|
|
64
|
+
<span className="text-right text-sm font-medium text-foreground">
|
|
65
|
+
{field.value}
|
|
66
|
+
</span>
|
|
67
|
+
{field.confidence === "confirmed" ? (
|
|
68
|
+
<CheckCircle2 className="size-3 shrink-0 text-success-text" />
|
|
69
|
+
) : (
|
|
70
|
+
<HelpCircle className="size-3 shrink-0 text-warning-text" />
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// LeadInfoPanel — internal helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const APPOINTMENT_STATUS_LABEL: Record<
|
|
84
|
+
AiConvAppointmentData["status"],
|
|
85
|
+
string
|
|
86
|
+
> = {
|
|
87
|
+
requested: "Lead requested",
|
|
88
|
+
confirmed: "Confirmed",
|
|
89
|
+
pending: "Pending confirmation",
|
|
90
|
+
cancelled: "Cancelled",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const MEETING_ICON: Record<AiConvMeetingType, React.ElementType> = {
|
|
94
|
+
video: Video,
|
|
95
|
+
phone: Phone,
|
|
96
|
+
"in-person": MapPin,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const MEETING_LABEL: Record<AiConvMeetingType, string> = {
|
|
100
|
+
video: "Video Call",
|
|
101
|
+
phone: "Phone Call",
|
|
102
|
+
"in-person": "In Person",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const MEETING_DETAIL_ICON: Record<AiConvMeetingType, React.ElementType> = {
|
|
106
|
+
video: Link2,
|
|
107
|
+
phone: PhoneCall,
|
|
108
|
+
"in-person": Navigation,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function MeetingDetailRow({
|
|
112
|
+
meetingType,
|
|
113
|
+
detail,
|
|
114
|
+
}: {
|
|
115
|
+
meetingType: AiConvMeetingType;
|
|
116
|
+
detail: string;
|
|
117
|
+
}) {
|
|
118
|
+
const DetailIcon = MEETING_DETAIL_ICON[meetingType];
|
|
119
|
+
const isLink = detail.startsWith("http");
|
|
120
|
+
return (
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
<DetailIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
123
|
+
{isLink ? (
|
|
124
|
+
<a
|
|
125
|
+
href={detail}
|
|
126
|
+
target="_blank"
|
|
127
|
+
rel="noopener noreferrer"
|
|
128
|
+
className="text-sm text-primary underline underline-offset-2 break-all hover:text-primary/80"
|
|
129
|
+
>
|
|
130
|
+
{detail}
|
|
131
|
+
</a>
|
|
132
|
+
) : (
|
|
133
|
+
<span className="text-sm text-muted-foreground break-all">
|
|
134
|
+
{detail}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface AppointmentSectionProps {
|
|
142
|
+
appointment: AiConvAppointmentData;
|
|
143
|
+
contactId: string;
|
|
144
|
+
isAnonymous: boolean;
|
|
145
|
+
onApproveAppointment?: () => void;
|
|
146
|
+
onDeclineAppointment?: () => void;
|
|
147
|
+
onRescheduleAppointment?: (contactId: string) => void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function AppointmentSection({
|
|
151
|
+
appointment,
|
|
152
|
+
contactId,
|
|
153
|
+
isAnonymous,
|
|
154
|
+
onApproveAppointment,
|
|
155
|
+
onDeclineAppointment,
|
|
156
|
+
onRescheduleAppointment,
|
|
157
|
+
}: AppointmentSectionProps) {
|
|
158
|
+
const AppointmentIcon = MEETING_ICON[appointment.meetingType];
|
|
159
|
+
const canReschedule = !isAnonymous && !!onRescheduleAppointment;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex flex-col gap-2">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<Calendar className="size-3.5 shrink-0 text-muted-foreground" />
|
|
165
|
+
<span className="text-sm font-medium text-foreground">
|
|
166
|
+
{appointment.datetime}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex items-center gap-2">
|
|
170
|
+
<AppointmentIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
171
|
+
<span className="text-sm text-muted-foreground">
|
|
172
|
+
{MEETING_LABEL[appointment.meetingType]}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
{appointment.meetingDetail && (
|
|
176
|
+
<MeetingDetailRow
|
|
177
|
+
meetingType={appointment.meetingType}
|
|
178
|
+
detail={appointment.meetingDetail}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
<span
|
|
182
|
+
className={cn("text-sm font-medium", {
|
|
183
|
+
"text-warning-text": appointment.status === "requested",
|
|
184
|
+
"text-success-text": appointment.status === "confirmed",
|
|
185
|
+
"text-muted-foreground": appointment.status === "pending",
|
|
186
|
+
"text-destructive line-through": appointment.status === "cancelled",
|
|
187
|
+
})}
|
|
188
|
+
>
|
|
189
|
+
{APPOINTMENT_STATUS_LABEL[appointment.status]}
|
|
190
|
+
</span>
|
|
191
|
+
|
|
192
|
+
{appointment.status === "requested" && (
|
|
193
|
+
<div className="flex gap-2 pt-1">
|
|
194
|
+
<Button size="sm" className="flex-1" onClick={onApproveAppointment}>
|
|
195
|
+
Approve
|
|
196
|
+
</Button>
|
|
197
|
+
<Button
|
|
198
|
+
variant="outline"
|
|
199
|
+
size="sm"
|
|
200
|
+
className="flex-1"
|
|
201
|
+
onClick={onDeclineAppointment}
|
|
202
|
+
>
|
|
203
|
+
Decline
|
|
204
|
+
</Button>
|
|
205
|
+
{canReschedule && (
|
|
206
|
+
<Button
|
|
207
|
+
variant="ghost"
|
|
208
|
+
size="sm"
|
|
209
|
+
className="flex-1"
|
|
210
|
+
onClick={() => onRescheduleAppointment!(contactId)}
|
|
211
|
+
>
|
|
212
|
+
Reschedule
|
|
213
|
+
</Button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{(appointment.status === "confirmed" ||
|
|
219
|
+
appointment.status === "cancelled") &&
|
|
220
|
+
canReschedule && (
|
|
221
|
+
<Button
|
|
222
|
+
variant="outline"
|
|
223
|
+
size="sm"
|
|
224
|
+
className="mt-1 w-full justify-start"
|
|
225
|
+
onClick={() => onRescheduleAppointment!(contactId)}
|
|
226
|
+
>
|
|
227
|
+
Reschedule
|
|
228
|
+
</Button>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function PanelSectionHeader({ children }: { children: React.ReactNode }) {
|
|
235
|
+
return (
|
|
236
|
+
<p className="mb-2.5 text-overline text-muted-foreground">{children}</p>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function PanelSection({
|
|
241
|
+
children,
|
|
242
|
+
last = false,
|
|
243
|
+
}: {
|
|
244
|
+
children: React.ReactNode;
|
|
245
|
+
last?: boolean;
|
|
246
|
+
}) {
|
|
247
|
+
return (
|
|
248
|
+
<div className={cn("px-4 py-4", !last && "border-b border-border")}>
|
|
249
|
+
{children}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// LeadInfoPanel
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
export interface LeadInfoPanelProps {
|
|
259
|
+
contact: AiConvContact;
|
|
260
|
+
firstSeen?: string;
|
|
261
|
+
source?: string;
|
|
262
|
+
/** Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment". */
|
|
263
|
+
topic?: string;
|
|
264
|
+
aiFields?: AiConvDataField[];
|
|
265
|
+
appointment?: AiConvAppointmentData;
|
|
266
|
+
internalNotes?: string;
|
|
267
|
+
notesSaveStatus?: "idle" | "saving" | "saved";
|
|
268
|
+
isCollapsed?: boolean;
|
|
269
|
+
/** True when this lead's contact info already exists in the system. Disables Add to Contacts. */
|
|
270
|
+
isKnownContact?: boolean;
|
|
271
|
+
onAddToContacts?: () => void;
|
|
272
|
+
onCreateOpportunity?: () => void;
|
|
273
|
+
onBookAppointment?: () => void;
|
|
274
|
+
onApproveAppointment?: () => void;
|
|
275
|
+
onDeclineAppointment?: () => void;
|
|
276
|
+
onRescheduleAppointment?: (contactId: string) => void;
|
|
277
|
+
onNotesChange?: (v: string) => void;
|
|
278
|
+
onToggleCollapse?: () => void;
|
|
279
|
+
/** Mobile only — back to chat thread. */
|
|
280
|
+
onBack?: () => void;
|
|
281
|
+
className?: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function LeadInfoPanel({
|
|
285
|
+
contact,
|
|
286
|
+
firstSeen,
|
|
287
|
+
source = "Website Chatbot",
|
|
288
|
+
topic,
|
|
289
|
+
aiFields = [],
|
|
290
|
+
appointment,
|
|
291
|
+
internalNotes = "",
|
|
292
|
+
notesSaveStatus = "idle",
|
|
293
|
+
isCollapsed = false,
|
|
294
|
+
isKnownContact = false,
|
|
295
|
+
onAddToContacts,
|
|
296
|
+
onCreateOpportunity,
|
|
297
|
+
onBookAppointment,
|
|
298
|
+
onApproveAppointment,
|
|
299
|
+
onDeclineAppointment,
|
|
300
|
+
onRescheduleAppointment,
|
|
301
|
+
onNotesChange,
|
|
302
|
+
onToggleCollapse,
|
|
303
|
+
onBack,
|
|
304
|
+
className,
|
|
305
|
+
}: LeadInfoPanelProps) {
|
|
306
|
+
const isAnonymous = !contact.name.trim();
|
|
307
|
+
const addToContactsDisabled = isAnonymous || isKnownContact;
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div className={cn("flex flex-col bg-background", className)}>
|
|
311
|
+
<div
|
|
312
|
+
className={cn(
|
|
313
|
+
PANEL_HEADER_HEIGHT,
|
|
314
|
+
"flex items-center justify-between border-b border-border px-4",
|
|
315
|
+
)}
|
|
316
|
+
>
|
|
317
|
+
{onBack && (
|
|
318
|
+
<Button
|
|
319
|
+
variant="ghost"
|
|
320
|
+
size="icon"
|
|
321
|
+
className="size-8 shrink-0 md:hidden"
|
|
322
|
+
onClick={onBack}
|
|
323
|
+
aria-label="Back to conversation"
|
|
324
|
+
>
|
|
325
|
+
<ArrowLeft className="size-4" />
|
|
326
|
+
</Button>
|
|
327
|
+
)}
|
|
328
|
+
<span className="text-sm font-semibold text-foreground">Lead Info</span>
|
|
329
|
+
{onToggleCollapse && (
|
|
330
|
+
<Tooltip>
|
|
331
|
+
<TooltipTrigger
|
|
332
|
+
render={
|
|
333
|
+
<Button
|
|
334
|
+
variant="ghost"
|
|
335
|
+
size="icon"
|
|
336
|
+
className="size-7"
|
|
337
|
+
onClick={onToggleCollapse}
|
|
338
|
+
aria-label={isCollapsed ? "Expand panel" : "Collapse panel"}
|
|
339
|
+
>
|
|
340
|
+
<ChevronRight
|
|
341
|
+
className={cn(
|
|
342
|
+
"size-4 transition-transform duration-150",
|
|
343
|
+
isCollapsed && "-rotate-180",
|
|
344
|
+
)}
|
|
345
|
+
/>
|
|
346
|
+
</Button>
|
|
347
|
+
}
|
|
348
|
+
/>
|
|
349
|
+
<TooltipContent>
|
|
350
|
+
{isCollapsed ? "Expand panel" : "Collapse panel"}
|
|
351
|
+
</TooltipContent>
|
|
352
|
+
</Tooltip>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{!isCollapsed && (
|
|
357
|
+
<div className="flex-1 overflow-y-auto" tabIndex={0}>
|
|
358
|
+
<PanelSection>
|
|
359
|
+
<PanelSectionHeader>Contact</PanelSectionHeader>
|
|
360
|
+
<div className="flex items-center gap-3">
|
|
361
|
+
<ContactAvatar name={contact.name} size="lg" />
|
|
362
|
+
<div className="min-w-0 flex-1">
|
|
363
|
+
<p className="truncate text-sm font-semibold text-foreground">
|
|
364
|
+
{displayContactName(contact.name)}
|
|
365
|
+
</p>
|
|
366
|
+
<p className="text-sm text-muted-foreground">{source}</p>
|
|
367
|
+
{topic && (
|
|
368
|
+
<Badge variant="secondary" className="mt-1 text-xs">
|
|
369
|
+
{topic}
|
|
370
|
+
</Badge>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
{(contact.email || contact.phone || firstSeen) && (
|
|
375
|
+
<div className="mt-3 flex flex-col gap-1.5">
|
|
376
|
+
{contact.email && (
|
|
377
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
378
|
+
<Mail className="size-4 shrink-0" />
|
|
379
|
+
<a
|
|
380
|
+
href={`mailto:${contact.email}`}
|
|
381
|
+
className="truncate hover:underline"
|
|
382
|
+
>
|
|
383
|
+
{contact.email}
|
|
384
|
+
</a>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
{contact.phone && (
|
|
388
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
389
|
+
<PhoneCall className="size-4 shrink-0" />
|
|
390
|
+
<a
|
|
391
|
+
href={`tel:${contact.phone}`}
|
|
392
|
+
className="hover:underline"
|
|
393
|
+
>
|
|
394
|
+
{contact.phone}
|
|
395
|
+
</a>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
{firstSeen && (
|
|
399
|
+
<p className="text-sm text-muted-foreground">
|
|
400
|
+
First seen: {firstSeen}
|
|
401
|
+
</p>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</PanelSection>
|
|
406
|
+
|
|
407
|
+
<PanelSection>
|
|
408
|
+
<PanelSectionHeader>AI-Collected Data</PanelSectionHeader>
|
|
409
|
+
{aiFields.length > 0 ? (
|
|
410
|
+
<>
|
|
411
|
+
<AICollectedDataSection fields={aiFields} />
|
|
412
|
+
<div className="mt-2.5 flex items-center gap-3 text-xs text-muted-foreground">
|
|
413
|
+
<span className="flex items-center gap-1">
|
|
414
|
+
<CheckCircle2 className="size-3 text-success-text" />
|
|
415
|
+
confirmed
|
|
416
|
+
</span>
|
|
417
|
+
<span className="flex items-center gap-1">
|
|
418
|
+
<HelpCircle className="size-3 text-warning-text" />
|
|
419
|
+
estimated
|
|
420
|
+
</span>
|
|
421
|
+
</div>
|
|
422
|
+
</>
|
|
423
|
+
) : (
|
|
424
|
+
<p className="text-sm text-muted-foreground">
|
|
425
|
+
AI is still gathering information...
|
|
426
|
+
</p>
|
|
427
|
+
)}
|
|
428
|
+
</PanelSection>
|
|
429
|
+
|
|
430
|
+
<PanelSection>
|
|
431
|
+
<PanelSectionHeader>Appointment</PanelSectionHeader>
|
|
432
|
+
{appointment ? (
|
|
433
|
+
<AppointmentSection
|
|
434
|
+
appointment={appointment}
|
|
435
|
+
contactId={contact.id}
|
|
436
|
+
isAnonymous={isAnonymous}
|
|
437
|
+
onApproveAppointment={onApproveAppointment}
|
|
438
|
+
onDeclineAppointment={onDeclineAppointment}
|
|
439
|
+
onRescheduleAppointment={onRescheduleAppointment}
|
|
440
|
+
/>
|
|
441
|
+
) : (
|
|
442
|
+
<Button
|
|
443
|
+
variant="outline"
|
|
444
|
+
size="sm"
|
|
445
|
+
className="w-full justify-start"
|
|
446
|
+
disabled={isAnonymous}
|
|
447
|
+
onClick={onBookAppointment}
|
|
448
|
+
>
|
|
449
|
+
<Plus className="mr-1.5 size-3.5" />
|
|
450
|
+
Book Appointment
|
|
451
|
+
</Button>
|
|
452
|
+
)}
|
|
453
|
+
</PanelSection>
|
|
454
|
+
|
|
455
|
+
<PanelSection>
|
|
456
|
+
<PanelSectionHeader>CRM Actions</PanelSectionHeader>
|
|
457
|
+
<div className="flex flex-col gap-2">
|
|
458
|
+
<Tooltip>
|
|
459
|
+
<TooltipTrigger
|
|
460
|
+
render={
|
|
461
|
+
<Button
|
|
462
|
+
variant="outline"
|
|
463
|
+
size="sm"
|
|
464
|
+
className="w-full justify-start"
|
|
465
|
+
disabled={addToContactsDisabled}
|
|
466
|
+
onClick={onAddToContacts}
|
|
467
|
+
>
|
|
468
|
+
<UserPlus className="mr-1.5 size-3.5" />
|
|
469
|
+
Add to Contacts
|
|
470
|
+
</Button>
|
|
471
|
+
}
|
|
472
|
+
/>
|
|
473
|
+
{isKnownContact && (
|
|
474
|
+
<TooltipContent>Already in contacts</TooltipContent>
|
|
475
|
+
)}
|
|
476
|
+
</Tooltip>
|
|
477
|
+
<Button
|
|
478
|
+
variant="outline"
|
|
479
|
+
size="sm"
|
|
480
|
+
className="w-full justify-start"
|
|
481
|
+
disabled={isAnonymous}
|
|
482
|
+
onClick={onCreateOpportunity}
|
|
483
|
+
>
|
|
484
|
+
<Briefcase className="mr-1.5 size-3.5" />
|
|
485
|
+
Add to CRM
|
|
486
|
+
</Button>
|
|
487
|
+
</div>
|
|
488
|
+
</PanelSection>
|
|
489
|
+
|
|
490
|
+
<PanelSection last>
|
|
491
|
+
<div className="mb-2.5 flex items-center justify-between">
|
|
492
|
+
<p className="text-overline text-muted-foreground">
|
|
493
|
+
Internal Notes
|
|
494
|
+
</p>
|
|
495
|
+
{notesSaveStatus === "saving" && (
|
|
496
|
+
<span className="text-xs text-muted-foreground">Saving...</span>
|
|
497
|
+
)}
|
|
498
|
+
{notesSaveStatus === "saved" && (
|
|
499
|
+
<span className="flex items-center gap-1 text-xs text-success-text">
|
|
500
|
+
<CheckCircle2 className="size-3" />
|
|
501
|
+
Saved
|
|
502
|
+
</span>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
<Textarea
|
|
506
|
+
value={internalNotes}
|
|
507
|
+
onChange={(e) => onNotesChange?.(e.target.value)}
|
|
508
|
+
placeholder="Private notes — not visible to lead..."
|
|
509
|
+
rows={4}
|
|
510
|
+
className="resize-none text-sm"
|
|
511
|
+
/>
|
|
512
|
+
</PanelSection>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
);
|
|
517
|
+
}
|