@wealthx/shadcn 1.5.10 → 1.5.12

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.
Files changed (116) hide show
  1. package/.turbo/turbo-build.log +114 -114
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-AANINK2B.mjs → chunk-2KNQZG5S.mjs} +1 -1
  4. package/dist/chunk-3KLJ4XRE.mjs +375 -0
  5. package/dist/{chunk-6U4NQGVM.mjs → chunk-4X4MGYHE.mjs} +2 -2
  6. package/dist/{chunk-CEEVYRQA.mjs → chunk-67DGIPQ4.mjs} +1 -1
  7. package/dist/{chunk-7UIL5UN3.mjs → chunk-7II6QRCZ.mjs} +1 -1
  8. package/dist/{chunk-W5QJ57PU.mjs → chunk-7LN5OGC2.mjs} +1 -1
  9. package/dist/{chunk-ZXEUBBHJ.mjs → chunk-7TMPOZDE.mjs} +1 -1
  10. package/dist/{chunk-AHSCWXYJ.mjs → chunk-AJUAJC5O.mjs} +1 -1
  11. package/dist/{chunk-3CGM3QXQ.mjs → chunk-AKWN5ZQG.mjs} +2 -2
  12. package/dist/{chunk-O5CP6VP6.mjs → chunk-CPM6P63C.mjs} +56 -44
  13. package/dist/{chunk-FRT3S72S.mjs → chunk-CQ7HKBEX.mjs} +1 -1
  14. package/dist/{chunk-54TRNCID.mjs → chunk-EB626HVW.mjs} +78 -11
  15. package/dist/{chunk-E2BNCA6L.mjs → chunk-EHQL64B7.mjs} +1 -1
  16. package/dist/{chunk-2WCIORP7.mjs → chunk-EXI64H46.mjs} +1 -1
  17. package/dist/{chunk-BBXSNDS3.mjs → chunk-FQYFPHDO.mjs} +1 -1
  18. package/dist/{chunk-3VZ6CYY2.mjs → chunk-GAXNO4JB.mjs} +1 -1
  19. package/dist/{chunk-3WGFIFP6.mjs → chunk-I4P7RXAE.mjs} +1 -1
  20. package/dist/{chunk-Z2BW5T7P.mjs → chunk-IODGRCQG.mjs} +1 -1
  21. package/dist/{chunk-GS47ZSSA.mjs → chunk-J7KQON2N.mjs} +20 -5
  22. package/dist/{chunk-IQGKOT7A.mjs → chunk-K35TFQUB.mjs} +1 -1
  23. package/dist/{chunk-4DO3WM7V.mjs → chunk-K4VWSDJJ.mjs} +1 -1
  24. package/dist/{chunk-KWD6GANL.mjs → chunk-MPA2HV5U.mjs} +1 -1
  25. package/dist/{chunk-5LZZYODG.mjs → chunk-QHAMVWDG.mjs} +19 -1
  26. package/dist/{chunk-XUCDPAVI.mjs → chunk-R6U246E4.mjs} +2 -2
  27. package/dist/{chunk-VCDGLN25.mjs → chunk-S6AYZJYO.mjs} +47 -21
  28. package/dist/{chunk-WL6WVV47.mjs → chunk-X6RC5UWB.mjs} +1 -1
  29. package/dist/{chunk-4BHDDLWK.mjs → chunk-XAS6KBIG.mjs} +2 -2
  30. package/dist/{chunk-VWZS32ZQ.mjs → chunk-XYWEGBAA.mjs} +1 -1
  31. package/dist/{chunk-54MTIKNC.mjs → chunk-YV7XF32X.mjs} +49 -24
  32. package/dist/{chunk-E5EDZQ5J.mjs → chunk-ZA44WICP.mjs} +1 -1
  33. package/dist/components/ui/advisor-card.js +144 -55
  34. package/dist/components/ui/advisor-card.mjs +5 -2
  35. package/dist/components/ui/agent-evaluation-toast.js +1 -1
  36. package/dist/components/ui/agent-evaluation-toast.mjs +2 -2
  37. package/dist/components/ui/ai-assistant-drawer.js +1 -1
  38. package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
  39. package/dist/components/ui/ai-builder.js +1 -1
  40. package/dist/components/ui/ai-builder.mjs +2 -2
  41. package/dist/components/ui/ai-conversations.js +71 -4
  42. package/dist/components/ui/ai-conversations.mjs +3 -3
  43. package/dist/components/ui/appointment-action-dialogs.js +1 -1
  44. package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
  45. package/dist/components/ui/appointment-book-dialog.js +19 -4
  46. package/dist/components/ui/appointment-book-dialog.mjs +3 -3
  47. package/dist/components/ui/appointment-calendar-view.js +1 -1
  48. package/dist/components/ui/appointment-calendar-view.mjs +2 -2
  49. package/dist/components/ui/appointment-detail-sheet.js +1 -1
  50. package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
  51. package/dist/components/ui/appointment-gmail-connect.js +1 -1
  52. package/dist/components/ui/appointment-gmail-connect.mjs +2 -2
  53. package/dist/components/ui/appointment-time-slot-picker.js +1 -1
  54. package/dist/components/ui/appointment-time-slot-picker.mjs +2 -2
  55. package/dist/components/ui/appointment-upcoming-card.js +1 -1
  56. package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
  57. package/dist/components/ui/badge.js +1 -1
  58. package/dist/components/ui/badge.mjs +1 -1
  59. package/dist/components/ui/bank-statement-generate-dialog.js +61 -46
  60. package/dist/components/ui/bank-statement-generate-dialog.mjs +1 -1
  61. package/dist/components/ui/chat-widget-primitives.js +1 -1
  62. package/dist/components/ui/chat-widget-primitives.mjs +2 -2
  63. package/dist/components/ui/chat-widget.js +1 -1
  64. package/dist/components/ui/chat-widget.mjs +3 -3
  65. package/dist/components/ui/chip.js +1 -1
  66. package/dist/components/ui/chip.mjs +2 -2
  67. package/dist/components/ui/contact-alert-dialog/index.js +19 -1
  68. package/dist/components/ui/contact-alert-dialog/index.mjs +1 -1
  69. package/dist/components/ui/dashboard-transactions-table.js +1 -1
  70. package/dist/components/ui/dashboard-transactions-table.mjs +2 -2
  71. package/dist/components/ui/financial-cards.js +1 -1
  72. package/dist/components/ui/financial-cards.mjs +2 -2
  73. package/dist/components/ui/financial-sections.js +1 -1
  74. package/dist/components/ui/financial-sections.mjs +3 -3
  75. package/dist/components/ui/income-summary-component.js +1 -1
  76. package/dist/components/ui/income-summary-component.mjs +1 -1
  77. package/dist/components/ui/integration-card.js +1 -1
  78. package/dist/components/ui/integration-card.mjs +2 -2
  79. package/dist/components/ui/kanban-column.js +46 -23
  80. package/dist/components/ui/kanban-column.mjs +4 -4
  81. package/dist/components/ui/loan-applicant-information.js +1 -1
  82. package/dist/components/ui/loan-applicant-information.mjs +1 -1
  83. package/dist/components/ui/loan-application-badge.js +1 -1
  84. package/dist/components/ui/loan-application-badge.mjs +2 -2
  85. package/dist/components/ui/opportunity-card.js +46 -23
  86. package/dist/components/ui/opportunity-card.mjs +3 -3
  87. package/dist/components/ui/opportunity-summary-tab.js +1 -1
  88. package/dist/components/ui/opportunity-summary-tab.mjs +3 -3
  89. package/dist/components/ui/pipeline-board.js +46 -23
  90. package/dist/components/ui/pipeline-board.mjs +5 -5
  91. package/dist/components/ui/pipeline-primitives.js +1 -1
  92. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  93. package/dist/components/ui/property-asset-card.js +1 -1
  94. package/dist/components/ui/property-asset-card.mjs +1 -1
  95. package/dist/components/ui/resource-center.js +1 -1
  96. package/dist/components/ui/resource-center.mjs +2 -2
  97. package/dist/components/ui/share-details-dialog.js +326 -30
  98. package/dist/components/ui/share-details-dialog.mjs +4 -1
  99. package/dist/components/ui/stage-timeline.js +1 -1
  100. package/dist/components/ui/stage-timeline.mjs +3 -3
  101. package/dist/index.js +583 -232
  102. package/dist/index.mjs +45 -43
  103. package/dist/styles.css +1 -1
  104. package/package.json +1 -1
  105. package/src/components/index.tsx +4 -0
  106. package/src/components/ui/advisor-card.tsx +75 -25
  107. package/src/components/ui/ai-conversations.tsx +157 -23
  108. package/src/components/ui/appointment-book-dialog.tsx +26 -3
  109. package/src/components/ui/appointment-time-slot-picker.tsx +1 -0
  110. package/src/components/ui/badge.tsx +1 -1
  111. package/src/components/ui/bank-statement-generate-dialog.tsx +84 -61
  112. package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +19 -1
  113. package/src/components/ui/opportunity-card.tsx +56 -20
  114. package/src/components/ui/share-details-dialog.tsx +251 -0
  115. package/src/styles/styles-css.ts +1 -1
  116. package/dist/chunk-OZ2R6ERP.mjs +0 -174
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
  import { format, parseISO, subDays } from "date-fns";
3
- import { cn } from "@/lib/utils";
4
3
  import { Button } from "@/components/ui/button";
5
4
  import { Checkbox } from "@/components/ui/checkbox";
6
5
  import { DatePicker } from "@/components/ui/date-picker";
@@ -111,6 +110,15 @@ export interface BankStatementGenerateDialogProps {
111
110
  className?: string;
112
111
  }
113
112
 
113
+ // ---------------------------------------------------------------------------
114
+ // Constants
115
+ // ---------------------------------------------------------------------------
116
+
117
+ const APPLICANT_TYPE_LABELS: Record<"primary" | "secondary", string> = {
118
+ primary: "Main applicant",
119
+ secondary: "Co-applicant",
120
+ };
121
+
114
122
  // ---------------------------------------------------------------------------
115
123
  // Helpers
116
124
  // ---------------------------------------------------------------------------
@@ -144,8 +152,11 @@ export function BankStatementGenerateDialog({
144
152
  }: BankStatementGenerateDialogProps) {
145
153
  const [statementName, setStatementName] = useState("Bank Statement 1");
146
154
  const [rangePreset, setRangePreset] = useState<BankStatementRangePreset>(90);
147
- const [fromDate, setFromDate] = useState<string>("");
148
- const [toDate, setToDate] = useState<string>("");
155
+ // Initialise immediately so the Period column never shows "" on first open.
156
+ const [fromDate, setFromDate] = useState<string>(
157
+ () => presetToDateRange(90).from,
158
+ );
159
+ const [toDate, setToDate] = useState<string>(() => presetToDateRange(90).to);
149
160
  const [applicantType, setApplicantType] = useState<
150
161
  "primary" | "secondary" | ""
151
162
  >("");
@@ -185,6 +196,12 @@ export function BankStatementGenerateDialog({
185
196
  }
186
197
  };
187
198
 
199
+ const handlePresetChange = (val: string[]) => {
200
+ if (val.length === 0) return;
201
+ const raw = val[0];
202
+ applyPreset(raw === "custom" ? "custom" : (Number(raw) as 90 | 180 | 365));
203
+ };
204
+
188
205
  // ── Account selection ────────────────────────────────────────────────────
189
206
 
190
207
  const areAllSelected =
@@ -240,7 +257,8 @@ export function BankStatementGenerateDialog({
240
257
  return (
241
258
  <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
242
259
  <DialogContent
243
- className={cn("max-w-[700px]", className)}
260
+ size="3xl"
261
+ className={className}
244
262
  data-slot="bank-statement-generate-dialog"
245
263
  >
246
264
  <DialogHeader>
@@ -267,15 +285,7 @@ export function BankStatementGenerateDialog({
267
285
  type="single"
268
286
  variant="outline"
269
287
  value={[String(rangePreset)]}
270
- onValueChange={(val) => {
271
- if (val.length === 0) return;
272
- const raw = val[0];
273
- if (raw === "custom") {
274
- applyPreset("custom");
275
- } else {
276
- applyPreset(Number(raw) as 90 | 180 | 365);
277
- }
278
- }}
288
+ onValueChange={handlePresetChange}
279
289
  className="w-full"
280
290
  >
281
291
  <ToggleGroupItem value="90" className="flex-1">
@@ -317,14 +327,19 @@ export function BankStatementGenerateDialog({
317
327
  onValueChange={(v) =>
318
328
  handleApplicantTypeChange(v as "primary" | "secondary")
319
329
  }
330
+ items={APPLICANT_TYPE_LABELS}
320
331
  >
321
332
  <SelectTrigger className="w-full">
322
333
  <SelectValue placeholder="Applicant Type" />
323
334
  </SelectTrigger>
324
335
  <SelectContent>
325
- <SelectItem value="primary">Main applicant</SelectItem>
336
+ <SelectItem value="primary">
337
+ {APPLICANT_TYPE_LABELS.primary}
338
+ </SelectItem>
326
339
  {hasCoApplicant && (
327
- <SelectItem value="secondary">Co-applicant</SelectItem>
340
+ <SelectItem value="secondary">
341
+ {APPLICANT_TYPE_LABELS.secondary}
342
+ </SelectItem>
328
343
  )}
329
344
  </SelectContent>
330
345
  </Select>
@@ -363,53 +378,61 @@ export function BankStatementGenerateDialog({
363
378
  </TableRow>
364
379
  </TableHeader>
365
380
  <TableBody>
366
- {bankAccounts.map((account) => (
367
- <TableRow key={account.id}>
368
- <TableCell>
369
- <Checkbox
370
- checked={selectedAccountIds.includes(account.id)}
371
- onCheckedChange={() =>
372
- handleToggleAccount(account.id)
373
- }
374
- aria-label={`Select ${account.name}`}
375
- />
376
- </TableCell>
377
- <TableCell>
378
- <div className="flex items-center gap-2">
379
- {account.institutionLogo && (
380
- <img
381
- src={account.institutionLogo}
382
- alt={account.institutionName ?? ""}
383
- className="size-8 rounded object-cover"
384
- />
385
- )}
386
- <span className="text-body-medium font-semibold">
387
- {account.name || account.institutionName || "—"}
381
+ {bankAccounts.map((account) => {
382
+ const isSelected = selectedAccountIds.includes(
383
+ account.id,
384
+ );
385
+ return (
386
+ <TableRow
387
+ key={account.id}
388
+ data-state={isSelected ? "selected" : undefined}
389
+ >
390
+ <TableCell>
391
+ <Checkbox
392
+ checked={isSelected}
393
+ onCheckedChange={() =>
394
+ handleToggleAccount(account.id)
395
+ }
396
+ aria-label={`Select ${account.name}`}
397
+ />
398
+ </TableCell>
399
+ <TableCell>
400
+ <div className="flex items-center gap-2">
401
+ {account.institutionLogo && (
402
+ <img
403
+ src={account.institutionLogo}
404
+ alt={account.institutionName ?? ""}
405
+ className="size-8 object-cover"
406
+ />
407
+ )}
408
+ <span className="text-body-medium font-semibold">
409
+ {account.name || account.institutionName || "—"}
410
+ </span>
411
+ </div>
412
+ </TableCell>
413
+ <TableCell>
414
+ <span className="text-body-medium">
415
+ {account.accountNo ?? "—"}
416
+ </span>
417
+ </TableCell>
418
+ <TableCell>
419
+ <span className="text-body-medium">
420
+ {periodLabel}
421
+ </span>
422
+ </TableCell>
423
+ <TableCell>
424
+ <span className="text-body-medium">
425
+ {account.lastUpdated
426
+ ? format(
427
+ parseISO(account.lastUpdated),
428
+ "dd MMM yyyy",
429
+ )
430
+ : "—"}
388
431
  </span>
389
- </div>
390
- </TableCell>
391
- <TableCell>
392
- <span className="text-body-medium">
393
- {account.accountNo ?? "—"}
394
- </span>
395
- </TableCell>
396
- <TableCell>
397
- <span className="text-body-medium">
398
- {periodLabel}
399
- </span>
400
- </TableCell>
401
- <TableCell>
402
- <span className="text-body-medium">
403
- {account.lastUpdated
404
- ? format(
405
- parseISO(account.lastUpdated),
406
- "dd MMM yyyy",
407
- )
408
- : "—"}
409
- </span>
410
- </TableCell>
411
- </TableRow>
412
- ))}
432
+ </TableCell>
433
+ </TableRow>
434
+ );
435
+ })}
413
436
  </TableBody>
414
437
  </Table>
415
438
  )}
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { AlertCircleIcon, EyeIcon, InfoIcon } from "lucide-react";
2
3
  import {
3
4
  Query,
4
5
  Utils as QbUtils,
@@ -17,6 +18,7 @@ import { Checkbox } from "@/components/ui/checkbox";
17
18
  import { Label } from "@/components/ui/label";
18
19
  import { Field, FieldLabel } from "@/components/ui/field";
19
20
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
21
+ import { cn } from "@/lib/utils";
20
22
  import type {
21
23
  ContactAlertDialogProps,
22
24
  ContactAlertQueryBuilderProps,
@@ -161,7 +163,23 @@ export function ContactAlertDialog({
161
163
  {(
162
164
  ["NEED_ACTION", "WATCH", "HEALTHY"] as ContactAlertSeverity[]
163
165
  ).map((s) => (
164
- <ToggleGroupItem key={s} value={s}>
166
+ <ToggleGroupItem
167
+ key={s}
168
+ value={s}
169
+ className={cn(
170
+ s === "NEED_ACTION" &&
171
+ "data-pressed:bg-destructive/10 data-pressed:inset-ring-destructive data-pressed:text-destructive-text data-pressed:hover:bg-destructive/10",
172
+ s === "WATCH" &&
173
+ "data-pressed:bg-warning/10 data-pressed:inset-ring-warning data-pressed:text-warning-text data-pressed:hover:bg-warning/10",
174
+ s === "HEALTHY" &&
175
+ "data-pressed:bg-info/10 data-pressed:inset-ring-info data-pressed:text-info-text data-pressed:hover:bg-info/10",
176
+ )}
177
+ >
178
+ {s === "NEED_ACTION" && (
179
+ <AlertCircleIcon className="size-3.5" />
180
+ )}
181
+ {s === "WATCH" && <EyeIcon className="size-3.5" />}
182
+ {s === "HEALTHY" && <InfoIcon className="size-3.5" />}
165
183
  {SEVERITY_LABELS[s]}
166
184
  </ToggleGroupItem>
167
185
  ))}
@@ -6,6 +6,8 @@ import {
6
6
  Users,
7
7
  Calendar,
8
8
  Check,
9
+ Copy,
10
+ Link2,
9
11
  MoreVertical,
10
12
  Clock,
11
13
  ArrowRight,
@@ -782,6 +784,54 @@ export function OpportunityCard({
782
784
  // Primary action: send them the loan application link.
783
785
  // ---------------------------------------------------------------------------
784
786
 
787
+ function normalizeUrl(url: string): string {
788
+ return `https://${url.replace(/^https?:\/\//, "")}`;
789
+ }
790
+
791
+ /** Shortened + copyable loan application URL row. */
792
+ function LoanApplicationLink({ url }: { url: string }) {
793
+ const [copied, setCopied] = useState(false);
794
+ const href = normalizeUrl(url);
795
+
796
+ function handleCopy() {
797
+ navigator.clipboard.writeText(href).then(() => {
798
+ setCopied(true);
799
+ setTimeout(() => setCopied(false), 2000);
800
+ });
801
+ }
802
+
803
+ return (
804
+ <div className="flex flex-col gap-1">
805
+ <p className="text-caption text-muted-foreground">
806
+ Or the link below to fill out the loan application directly.
807
+ </p>
808
+ <div className="flex items-center gap-1.5">
809
+ <Link2 className="size-4 shrink-0 text-muted-foreground" />
810
+ <a
811
+ href={href}
812
+ target="_blank"
813
+ rel="noreferrer"
814
+ className="min-w-0 flex-1 truncate text-body-small text-primary underline-offset-2 hover:underline"
815
+ >
816
+ {url}
817
+ </a>
818
+ <button
819
+ type="button"
820
+ onClick={handleCopy}
821
+ className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground"
822
+ aria-label="Copy link"
823
+ >
824
+ {copied ? (
825
+ <Check className="size-4 text-success-text" />
826
+ ) : (
827
+ <Copy className="size-4" />
828
+ )}
829
+ </button>
830
+ </div>
831
+ </div>
832
+ );
833
+ }
834
+
785
835
  export interface LeadCardProps {
786
836
  id: string;
787
837
  customerName: string;
@@ -816,18 +866,18 @@ export function LeadCard({
816
866
  {/* ── Customer info + delete menu ── */}
817
867
  <div className="flex items-start justify-between gap-2">
818
868
  <div className="flex min-w-0 flex-1 flex-col gap-1">
819
- <span className="text-sm font-semibold text-foreground">
869
+ <span className="text-label-medium text-foreground">
820
870
  {customerName}
821
871
  </span>
822
872
  {customerPhone && (
823
- <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
824
- <Phone className="size-3 shrink-0" aria-hidden="true" />
873
+ <span className="flex items-center gap-1.5 text-caption text-muted-foreground">
874
+ <Phone className="size-3.5 shrink-0" aria-hidden="true" />
825
875
  {customerPhone}
826
876
  </span>
827
877
  )}
828
878
  {customerEmail && (
829
- <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
830
- <Mail className="size-3 shrink-0" aria-hidden="true" />
879
+ <span className="flex items-center gap-1.5 text-caption text-muted-foreground">
880
+ <Mail className="size-3.5 shrink-0" aria-hidden="true" />
831
881
  <span className="truncate">{customerEmail}</span>
832
882
  </span>
833
883
  )}
@@ -871,21 +921,7 @@ export function LeadCard({
871
921
  Send Loan Application Request
872
922
  </Button>
873
923
  {loanApplicationUrl && (
874
- <p className="text-xs text-muted-foreground">
875
- Or the link below to fill out the loan application directly.
876
- <br />
877
- <a
878
- href={`https://${loanApplicationUrl.replace(
879
- /^https?:\/\//,
880
- "",
881
- )}`}
882
- target="_blank"
883
- rel="noreferrer"
884
- className="text-primary underline-offset-2 hover:underline"
885
- >
886
- {loanApplicationUrl}
887
- </a>
888
- </p>
924
+ <LoanApplicationLink url={loanApplicationUrl} />
889
925
  )}
890
926
  </div>
891
927
  )}
@@ -12,6 +12,8 @@ import { Input } from "@/components/ui/input";
12
12
  import { Label } from "@/components/ui/label";
13
13
  import { Spinner } from "@/components/ui/spinner";
14
14
  import { Textarea } from "@/components/ui/textarea";
15
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
16
+ import { cn } from "@/lib/utils";
15
17
 
16
18
  // ---------------------------------------------------------------------------
17
19
  // Shared helper
@@ -236,3 +238,252 @@ export function EmailTemplateDialog({
236
238
  </Dialog>
237
239
  );
238
240
  }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // ShareContactDialog
244
+ // ---------------------------------------------------------------------------
245
+
246
+ export interface InternalAdvisor {
247
+ id: string;
248
+ name: string;
249
+ role: string;
250
+ practice: string;
251
+ }
252
+
253
+ export interface ShareContactDialogProps {
254
+ open: boolean;
255
+ onOpenChange: (open: boolean) => void;
256
+ contactName: string;
257
+ internalAdvisors: InternalAdvisor[];
258
+ onShareInternal: (advisorId: string, notes: string) => void;
259
+ onShareExternal: (payload: {
260
+ firstName: string;
261
+ lastName: string;
262
+ email: string;
263
+ notes: string;
264
+ }) => void;
265
+ isSending?: boolean;
266
+ className?: string;
267
+ }
268
+
269
+ export function ShareContactDialog({
270
+ open,
271
+ onOpenChange,
272
+ contactName,
273
+ internalAdvisors,
274
+ onShareInternal,
275
+ onShareExternal,
276
+ isSending = false,
277
+ className,
278
+ }: ShareContactDialogProps) {
279
+ const [tab, setTab] = React.useState("internal");
280
+ const [search, setSearch] = React.useState("");
281
+ const [selectedId, setSelectedId] = React.useState<string | null>(null);
282
+ const [internalNotes, setInternalNotes] = React.useState("");
283
+ const [firstName, setFirstName] = React.useState("");
284
+ const [lastName, setLastName] = React.useState("");
285
+ const [email, setEmail] = React.useState("");
286
+ const [externalNotes, setExternalNotes] = React.useState("");
287
+
288
+ function reset() {
289
+ setTab("internal");
290
+ setSearch("");
291
+ setSelectedId(null);
292
+ setInternalNotes("");
293
+ setFirstName("");
294
+ setLastName("");
295
+ setEmail("");
296
+ setExternalNotes("");
297
+ }
298
+
299
+ function handleOpenChange(v: boolean) {
300
+ if (!v) reset();
301
+ onOpenChange(v);
302
+ }
303
+
304
+ const filtered = internalAdvisors.filter((a) => {
305
+ const q = search.toLowerCase();
306
+ return a.name.toLowerCase().includes(q) || a.role.toLowerCase().includes(q);
307
+ });
308
+
309
+ return (
310
+ <Dialog open={open} onOpenChange={isSending ? undefined : handleOpenChange}>
311
+ <DialogContent size="md" className={className}>
312
+ <DialogHeader>
313
+ <DialogTitle>Share Contact's Information</DialogTitle>
314
+ </DialogHeader>
315
+
316
+ <p className="text-sm text-muted-foreground">
317
+ Share{" "}
318
+ <span className="font-medium text-foreground">{contactName}</span>
319
+ 's information with an advisor.
320
+ </p>
321
+
322
+ <Tabs value={tab} onValueChange={setTab}>
323
+ <TabsList className="w-full">
324
+ <TabsTrigger value="internal" className="flex-1">
325
+ Internal Advisor
326
+ </TabsTrigger>
327
+ <TabsTrigger value="external" className="flex-1">
328
+ External / Referral
329
+ </TabsTrigger>
330
+ </TabsList>
331
+
332
+ <TabsContent value="internal" className="mt-4 flex flex-col gap-4">
333
+ <Field label="Search Advisor">
334
+ <Input
335
+ placeholder="Search by name or role…"
336
+ value={search}
337
+ onChange={(e) => setSearch(e.target.value)}
338
+ />
339
+ </Field>
340
+
341
+ <div className="max-h-[200px] overflow-y-auto border border-border divide-y divide-border">
342
+ {filtered.length === 0 ? (
343
+ <p className="px-3 py-4 text-center text-sm text-muted-foreground">
344
+ No advisors found.
345
+ </p>
346
+ ) : (
347
+ filtered.map((a) => {
348
+ const isSelected = selectedId === a.id;
349
+ return (
350
+ <button
351
+ key={a.id}
352
+ type="button"
353
+ onClick={() => setSelectedId(a.id)}
354
+ className={cn(
355
+ "flex w-full items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-muted/40",
356
+ isSelected && "bg-primary/5",
357
+ )}
358
+ >
359
+ <div
360
+ className={cn(
361
+ "flex size-4 shrink-0 items-center justify-center rounded-full border-2",
362
+ isSelected
363
+ ? "border-primary"
364
+ : "border-border bg-background",
365
+ )}
366
+ >
367
+ {isSelected && (
368
+ <div className="size-2 rounded-full bg-primary" />
369
+ )}
370
+ </div>
371
+ <div>
372
+ <p className="text-sm font-medium">{a.name}</p>
373
+ <p className="text-xs text-muted-foreground">
374
+ {a.role} · {a.practice}
375
+ </p>
376
+ </div>
377
+ </button>
378
+ );
379
+ })
380
+ )}
381
+ </div>
382
+
383
+ <Field id="sc-int-notes" label="Notes">
384
+ <Textarea
385
+ id="sc-int-notes"
386
+ placeholder="Optional message for the advisor…"
387
+ rows={3}
388
+ value={internalNotes}
389
+ onChange={(e) => setInternalNotes(e.target.value)}
390
+ disabled={isSending}
391
+ />
392
+ </Field>
393
+ </TabsContent>
394
+
395
+ <TabsContent value="external" className="mt-4 flex flex-col gap-4">
396
+ <div className="grid grid-cols-2 gap-4">
397
+ <Field id="sc-first" label="First Name">
398
+ <Input
399
+ id="sc-first"
400
+ placeholder="First name"
401
+ value={firstName}
402
+ onChange={(e) => setFirstName(e.target.value)}
403
+ disabled={isSending}
404
+ />
405
+ </Field>
406
+ <Field id="sc-last" label="Last Name">
407
+ <Input
408
+ id="sc-last"
409
+ placeholder="Last name"
410
+ value={lastName}
411
+ onChange={(e) => setLastName(e.target.value)}
412
+ disabled={isSending}
413
+ />
414
+ </Field>
415
+ </div>
416
+
417
+ <Field id="sc-email" label="Email" required>
418
+ <Input
419
+ id="sc-email"
420
+ type="email"
421
+ placeholder="advisor@company.com.au"
422
+ value={email}
423
+ onChange={(e) => setEmail(e.target.value)}
424
+ disabled={isSending}
425
+ />
426
+ </Field>
427
+
428
+ <Field id="sc-ext-notes" label="Notes">
429
+ <Textarea
430
+ id="sc-ext-notes"
431
+ placeholder="Optional message…"
432
+ rows={3}
433
+ value={externalNotes}
434
+ onChange={(e) => setExternalNotes(e.target.value)}
435
+ disabled={isSending}
436
+ />
437
+ </Field>
438
+ </TabsContent>
439
+ </Tabs>
440
+
441
+ <DialogFooter>
442
+ <Button
443
+ variant="outline"
444
+ onClick={() => handleOpenChange(false)}
445
+ disabled={isSending}
446
+ >
447
+ Cancel
448
+ </Button>
449
+ {tab === "internal" ? (
450
+ <Button
451
+ onClick={() => onShareInternal(selectedId!, internalNotes)}
452
+ disabled={!selectedId || isSending}
453
+ >
454
+ {isSending ? (
455
+ <>
456
+ <Spinner className="size-3.5" />
457
+ Sharing…
458
+ </>
459
+ ) : (
460
+ "Share Contact"
461
+ )}
462
+ </Button>
463
+ ) : (
464
+ <Button
465
+ onClick={() =>
466
+ onShareExternal({
467
+ firstName,
468
+ lastName,
469
+ email,
470
+ notes: externalNotes,
471
+ })
472
+ }
473
+ disabled={!email || isSending}
474
+ >
475
+ {isSending ? (
476
+ <>
477
+ <Spinner className="size-3.5" />
478
+ Sending…
479
+ </>
480
+ ) : (
481
+ "Send Invite"
482
+ )}
483
+ </Button>
484
+ )}
485
+ </DialogFooter>
486
+ </DialogContent>
487
+ </Dialog>
488
+ );
489
+ }