@wealthx/shadcn 1.0.2 → 1.2.0

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 (300) hide show
  1. package/.turbo/turbo-build.log +235 -138
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +82 -0
  4. package/dist/{chunk-6OJF6XRN.mjs → chunk-24FUO7TD.mjs} +4 -8
  5. package/dist/{chunk-4AJ5HWHD.mjs → chunk-2I5S2AMY.mjs} +3 -3
  6. package/dist/chunk-2SF672SZ.mjs +161 -0
  7. package/dist/{chunk-GPRJQ24C.mjs → chunk-34NWQURD.mjs} +2 -2
  8. package/dist/{chunk-MQ72DIBH.mjs → chunk-3GF7OVTP.mjs} +14 -5
  9. package/dist/chunk-3WMX6KWS.mjs +245 -0
  10. package/dist/{chunk-PMKODV6M.mjs → chunk-462HMNO4.mjs} +6 -10
  11. package/dist/chunk-4CX4SBRO.mjs +153 -0
  12. package/dist/chunk-4MN6UQHG.mjs +443 -0
  13. package/dist/chunk-5QQVZTVZ.mjs +233 -0
  14. package/dist/{chunk-BGP2N52Z.mjs → chunk-66MI7Q4B.mjs} +5 -5
  15. package/dist/chunk-6FCGKSZX.mjs +268 -0
  16. package/dist/{chunk-CGOKTPXU.mjs → chunk-6JQFUE5I.mjs} +20 -23
  17. package/dist/{chunk-Z3MK2KKZ.mjs → chunk-7DHU4VGG.mjs} +7 -3
  18. package/dist/{chunk-VZ2NR7L3.mjs → chunk-7PYJD5JI.mjs} +35 -27
  19. package/dist/{chunk-JU2RUWHF.mjs → chunk-7XJHLGUV.mjs} +1 -1
  20. package/dist/{chunk-BMFN37JH.mjs → chunk-7YAU5CY6.mjs} +1 -1
  21. package/dist/chunk-A56YQQHG.mjs +402 -0
  22. package/dist/chunk-AH52LG6N.mjs +315 -0
  23. package/dist/{chunk-SLWCCURD.mjs → chunk-CLIN5525.mjs} +8 -4
  24. package/dist/{chunk-3VQNJ235.mjs → chunk-CSDO6VBW.mjs} +7 -0
  25. package/dist/chunk-D4ILTPOG.mjs +293 -0
  26. package/dist/{chunk-HS7TFG7V.mjs → chunk-D6ID6M4V.mjs} +1 -1
  27. package/dist/chunk-DOH3EHX7.mjs +378 -0
  28. package/dist/{chunk-MJIEMGRD.mjs → chunk-EFRENWEJ.mjs} +9 -17
  29. package/dist/chunk-ERGGHC2V.mjs +185 -0
  30. package/dist/{chunk-OXQQNQZI.mjs → chunk-FEZKMUCF.mjs} +10 -1
  31. package/dist/{chunk-55CEW76V.mjs → chunk-FH6QVUVZ.mjs} +1 -1
  32. package/dist/chunk-FMAXJ2SI.mjs +71 -0
  33. package/dist/chunk-FZIXGLMV.mjs +173 -0
  34. package/dist/{chunk-DS2AMHN2.mjs → chunk-GYMYRIZP.mjs} +2 -2
  35. package/dist/{chunk-KQDD5MU3.mjs → chunk-H45TKD34.mjs} +5 -5
  36. package/dist/{chunk-BBJBJSXQ.mjs → chunk-J5UICVJS.mjs} +1 -1
  37. package/dist/{chunk-RL772EH7.mjs → chunk-JHJHG4GO.mjs} +4 -12
  38. package/dist/chunk-KMCGSZTX.mjs +177 -0
  39. package/dist/{chunk-FHNT55I5.mjs → chunk-KUDCQ4FI.mjs} +4 -4
  40. package/dist/chunk-LE6YFY6D.mjs +209 -0
  41. package/dist/{chunk-HUVTPUV2.mjs → chunk-LLVQKSU3.mjs} +23 -19
  42. package/dist/{chunk-KKHTJNMM.mjs → chunk-MARPPFOJ.mjs} +8 -4
  43. package/dist/{chunk-6AFMNC42.mjs → chunk-N2PT566P.mjs} +15 -11
  44. package/dist/chunk-NLCKVHWB.mjs +161 -0
  45. package/dist/{chunk-YN5SYTOO.mjs → chunk-NQPOYKAQ.mjs} +9 -5
  46. package/dist/{chunk-ZZV5JVNW.mjs → chunk-NSLMILBT.mjs} +3 -7
  47. package/dist/chunk-NXA3CZ7A.mjs +248 -0
  48. package/dist/chunk-OGOYQ7BG.mjs +150 -0
  49. package/dist/{chunk-3NQGYJEZ.mjs → chunk-P6AM5V7O.mjs} +10 -18
  50. package/dist/{chunk-CZ3BW5GL.mjs → chunk-P76HMUI6.mjs} +5 -11
  51. package/dist/chunk-PCPLO5HT.mjs +671 -0
  52. package/dist/chunk-PG6K5XEC.mjs +475 -0
  53. package/dist/chunk-PJHPSRYD.mjs +234 -0
  54. package/dist/{chunk-DDPA2XXS.mjs → chunk-PMB3A7V3.mjs} +2 -2
  55. package/dist/chunk-PR6V5XKM.mjs +209 -0
  56. package/dist/{chunk-46OFHMQA.mjs → chunk-Q76O3RIQ.mjs} +10 -6
  57. package/dist/chunk-QVKWW6KE.mjs +272 -0
  58. package/dist/chunk-RGU7HOEC.mjs +140 -0
  59. package/dist/{chunk-JF4PHPD5.mjs → chunk-RGVKLTLH.mjs} +4 -4
  60. package/dist/{chunk-VG6UF6UT.mjs → chunk-RP3SQYA3.mjs} +2 -2
  61. package/dist/chunk-RRBS6D63.mjs +163 -0
  62. package/dist/chunk-SMQ3DG25.mjs +285 -0
  63. package/dist/chunk-SPJ5KXW7.mjs +199 -0
  64. package/dist/chunk-SYOD63OZ.mjs +225 -0
  65. package/dist/chunk-UFYSFDER.mjs +42 -0
  66. package/dist/chunk-VACKZOMY.mjs +190 -0
  67. package/dist/chunk-VLQZANBF.mjs +42 -0
  68. package/dist/chunk-WA6O6EUR.mjs +1885 -0
  69. package/dist/{chunk-E3K6O4FZ.mjs → chunk-WAZD7NFU.mjs} +5 -2
  70. package/dist/chunk-WG6JGJXB.mjs +165 -0
  71. package/dist/{chunk-I64K754C.mjs → chunk-WNGWBVLV.mjs} +2 -2
  72. package/dist/{chunk-3U7SD3MS.mjs → chunk-WOEHFRGB.mjs} +3 -3
  73. package/dist/{chunk-DKZRJOMF.mjs → chunk-XIRTEFKH.mjs} +12 -12
  74. package/dist/chunk-Y6DWJSKZ.mjs +79 -0
  75. package/dist/chunk-YKPROFLB.mjs +161 -0
  76. package/dist/{chunk-K76E2TQU.mjs → chunk-ZRO5JO3H.mjs} +107 -67
  77. package/dist/{chunk-VYMHBV6D.mjs → chunk-ZU4NV6RG.mjs} +5 -3
  78. package/dist/components/ui/accordion.js +40 -4
  79. package/dist/components/ui/accordion.mjs +2 -2
  80. package/dist/components/ui/add-column-modal.js +789 -0
  81. package/dist/components/ui/add-column-modal.mjs +17 -0
  82. package/dist/components/ui/add-lead-modal.js +647 -0
  83. package/dist/components/ui/add-lead-modal.mjs +16 -0
  84. package/dist/components/ui/ai-assistant-drawer.js +686 -0
  85. package/dist/components/ui/ai-assistant-drawer.mjs +16 -0
  86. package/dist/components/ui/alert-dialog.js +37 -5
  87. package/dist/components/ui/alert-dialog.mjs +4 -4
  88. package/dist/components/ui/alert.js +37 -11
  89. package/dist/components/ui/alert.mjs +2 -2
  90. package/dist/components/ui/avatar.js +36 -8
  91. package/dist/components/ui/avatar.mjs +2 -2
  92. package/dist/components/ui/backoffice-alert-history-chart.js +624 -0
  93. package/dist/components/ui/backoffice-alert-history-chart.mjs +16 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +687 -0
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +16 -0
  96. package/dist/components/ui/badge.js +37 -2
  97. package/dist/components/ui/badge.mjs +2 -2
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +639 -0
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +16 -0
  100. package/dist/components/ui/button.js +35 -3
  101. package/dist/components/ui/button.mjs +2 -2
  102. package/dist/components/ui/calendar.js +43 -19
  103. package/dist/components/ui/calendar.mjs +3 -3
  104. package/dist/components/ui/card.js +40 -4
  105. package/dist/components/ui/card.mjs +2 -2
  106. package/dist/components/ui/cash-balance-line-chart.js +627 -0
  107. package/dist/components/ui/cash-balance-line-chart.mjs +16 -0
  108. package/dist/components/ui/cashflow-bar-chart.js +650 -0
  109. package/dist/components/ui/cashflow-bar-chart.mjs +16 -0
  110. package/dist/components/ui/checkbox.js +36 -5
  111. package/dist/components/ui/checkbox.mjs +2 -3
  112. package/dist/components/ui/chip.js +37 -2
  113. package/dist/components/ui/chip.mjs +3 -3
  114. package/dist/components/ui/combobox.js +280 -0
  115. package/dist/components/ui/combobox.mjs +28 -0
  116. package/dist/components/ui/data-table.js +160 -88
  117. package/dist/components/ui/data-table.mjs +10 -11
  118. package/dist/components/ui/date-picker.js +44 -20
  119. package/dist/components/ui/date-picker.mjs +6 -7
  120. package/dist/components/ui/dialog.js +44 -12
  121. package/dist/components/ui/dialog.mjs +4 -4
  122. package/dist/components/ui/drawer.js +46 -10
  123. package/dist/components/ui/drawer.mjs +3 -3
  124. package/dist/components/ui/dropdown-menu.js +40 -16
  125. package/dist/components/ui/dropdown-menu.mjs +3 -3
  126. package/dist/components/ui/empty.js +41 -5
  127. package/dist/components/ui/empty.mjs +2 -2
  128. package/dist/components/ui/expense-bar-chart.js +642 -0
  129. package/dist/components/ui/expense-bar-chart.mjs +16 -0
  130. package/dist/components/ui/field.js +53 -21
  131. package/dist/components/ui/field.mjs +4 -4
  132. package/dist/components/ui/financial-cards.js +1002 -0
  133. package/dist/components/ui/financial-cards.mjs +24 -0
  134. package/dist/components/ui/financial-drawers.js +637 -0
  135. package/dist/components/ui/financial-drawers.mjs +17 -0
  136. package/dist/components/ui/financial-primitives.js +218 -0
  137. package/dist/components/ui/financial-primitives.mjs +22 -0
  138. package/dist/components/ui/financial-sections.js +1422 -0
  139. package/dist/components/ui/financial-sections.mjs +30 -0
  140. package/dist/components/ui/form-primitives.js +682 -0
  141. package/dist/components/ui/form-primitives.mjs +19 -0
  142. package/dist/components/ui/income-bar-chart.js +641 -0
  143. package/dist/components/ui/income-bar-chart.mjs +16 -0
  144. package/dist/components/ui/input-group.js +43 -7
  145. package/dist/components/ui/input-group.mjs +5 -5
  146. package/dist/components/ui/input-otp.js +39 -3
  147. package/dist/components/ui/input-otp.mjs +2 -2
  148. package/dist/components/ui/input.js +34 -2
  149. package/dist/components/ui/input.mjs +2 -2
  150. package/dist/components/ui/kanban-column.js +1143 -0
  151. package/dist/components/ui/kanban-column.mjs +20 -0
  152. package/dist/components/ui/label.js +35 -7
  153. package/dist/components/ui/label.mjs +2 -2
  154. package/dist/components/ui/opportunity-card.js +960 -0
  155. package/dist/components/ui/opportunity-card.mjs +20 -0
  156. package/dist/components/ui/opportunity-edit-modals.js +3360 -0
  157. package/dist/components/ui/opportunity-edit-modals.mjs +37 -0
  158. package/dist/components/ui/opportunity-summary-tab.js +4365 -0
  159. package/dist/components/ui/opportunity-summary-tab.mjs +34 -0
  160. package/dist/components/ui/pagination.js +35 -3
  161. package/dist/components/ui/pagination.mjs +3 -3
  162. package/dist/components/ui/pipeline-alerts.js +103 -0
  163. package/dist/components/ui/pipeline-alerts.mjs +8 -0
  164. package/dist/components/ui/pipeline-board.js +1408 -0
  165. package/dist/components/ui/pipeline-board.mjs +24 -0
  166. package/dist/components/ui/pipeline-chart.js +216 -0
  167. package/dist/components/ui/pipeline-chart.mjs +10 -0
  168. package/dist/components/ui/pipeline-dialogs.js +1183 -0
  169. package/dist/components/ui/pipeline-dialogs.mjs +23 -0
  170. package/dist/components/ui/pipeline-primitives.js +300 -0
  171. package/dist/components/ui/pipeline-primitives.mjs +11 -0
  172. package/dist/components/ui/popover.js +45 -4
  173. package/dist/components/ui/popover.mjs +3 -3
  174. package/dist/components/ui/progress.js +33 -1
  175. package/dist/components/ui/progress.mjs +2 -2
  176. package/dist/components/ui/property-cashflow-doughnut-chart.js +523 -0
  177. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +16 -0
  178. package/dist/components/ui/property-debt-equity-doughnut-chart.js +521 -0
  179. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +16 -0
  180. package/dist/components/ui/property-mobile-estimate-line-chart.js +682 -0
  181. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +16 -0
  182. package/dist/components/ui/radio-group.js +33 -1
  183. package/dist/components/ui/radio-group.mjs +2 -2
  184. package/dist/components/ui/select.js +66 -26
  185. package/dist/components/ui/select.mjs +3 -3
  186. package/dist/components/ui/separator.js +33 -1
  187. package/dist/components/ui/separator.mjs +2 -2
  188. package/dist/components/ui/sheet.js +37 -9
  189. package/dist/components/ui/sheet.mjs +3 -3
  190. package/dist/components/ui/skeleton.js +33 -1
  191. package/dist/components/ui/skeleton.mjs +2 -2
  192. package/dist/components/ui/slider.js +86 -102
  193. package/dist/components/ui/slider.mjs +2 -2
  194. package/dist/components/ui/spinner.js +33 -1
  195. package/dist/components/ui/spinner.mjs +2 -2
  196. package/dist/components/ui/stage-timeline.js +579 -0
  197. package/dist/components/ui/stage-timeline.mjs +15 -0
  198. package/dist/components/ui/switch.js +37 -4
  199. package/dist/components/ui/switch.mjs +2 -3
  200. package/dist/components/ui/table.js +37 -5
  201. package/dist/components/ui/table.mjs +2 -2
  202. package/dist/components/ui/tabs.js +36 -12
  203. package/dist/components/ui/tabs.mjs +2 -2
  204. package/dist/components/ui/textarea.js +34 -2
  205. package/dist/components/ui/textarea.mjs +2 -2
  206. package/dist/components/ui/toggle-group.js +35 -4
  207. package/dist/components/ui/toggle-group.mjs +3 -4
  208. package/dist/components/ui/toggle.js +35 -4
  209. package/dist/components/ui/toggle.mjs +2 -3
  210. package/dist/components/ui/tooltip.js +51 -22
  211. package/dist/components/ui/tooltip.mjs +3 -3
  212. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +528 -0
  213. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +16 -0
  214. package/dist/components/ui/transactions-income-expense-bar-chart.js +516 -0
  215. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +16 -0
  216. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +528 -0
  217. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +16 -0
  218. package/dist/index.js +11613 -2868
  219. package/dist/index.mjs +377 -164
  220. package/dist/lib/theme-provider.js +10 -1
  221. package/dist/lib/theme-provider.mjs +1 -1
  222. package/dist/lib/typography.js +8 -0
  223. package/dist/lib/typography.mjs +3 -1
  224. package/dist/lib/utils.js +33 -1
  225. package/dist/lib/utils.mjs +1 -1
  226. package/dist/styles.css +1 -1
  227. package/package.json +169 -6
  228. package/src/components/index.tsx +323 -13
  229. package/src/components/ui/accordion.tsx +6 -3
  230. package/src/components/ui/add-column-modal.tsx +339 -0
  231. package/src/components/ui/add-lead-modal.tsx +290 -0
  232. package/src/components/ui/ai-assistant-drawer.tsx +408 -0
  233. package/src/components/ui/alert-dialog.tsx +80 -54
  234. package/src/components/ui/alert.tsx +28 -28
  235. package/src/components/ui/avatar.tsx +30 -29
  236. package/src/components/ui/backoffice-alert-history-chart.tsx +260 -0
  237. package/src/components/ui/backoffice-contact-history-chart.tsx +325 -0
  238. package/src/components/ui/badge.tsx +17 -15
  239. package/src/components/ui/borrowing-capacity-line-chart.tsx +357 -0
  240. package/src/components/ui/button.tsx +30 -27
  241. package/src/components/ui/calendar.tsx +53 -67
  242. package/src/components/ui/card.tsx +27 -24
  243. package/src/components/ui/cash-balance-line-chart.tsx +302 -0
  244. package/src/components/ui/cashflow-bar-chart.tsx +363 -0
  245. package/src/components/ui/chart-shared.tsx +261 -0
  246. package/src/components/ui/checkbox.tsx +30 -26
  247. package/src/components/ui/combobox.tsx +223 -0
  248. package/src/components/ui/data-table.tsx +160 -99
  249. package/src/components/ui/date-picker.tsx +0 -2
  250. package/src/components/ui/dialog.tsx +70 -60
  251. package/src/components/ui/drawer.tsx +57 -48
  252. package/src/components/ui/dropdown-menu.tsx +90 -82
  253. package/src/components/ui/empty.tsx +31 -27
  254. package/src/components/ui/expense-bar-chart.tsx +296 -0
  255. package/src/components/ui/field.tsx +70 -62
  256. package/src/components/ui/financial-cards.tsx +830 -0
  257. package/src/components/ui/financial-drawers.tsx +339 -0
  258. package/src/components/ui/financial-primitives.tsx +331 -0
  259. package/src/components/ui/financial-sections.tsx +672 -0
  260. package/src/components/ui/form-primitives.tsx +536 -0
  261. package/src/components/ui/income-bar-chart.tsx +297 -0
  262. package/src/components/ui/input-group.tsx +41 -34
  263. package/src/components/ui/input-otp.tsx +29 -24
  264. package/src/components/ui/input.tsx +8 -8
  265. package/src/components/ui/kanban-column.tsx +333 -0
  266. package/src/components/ui/label.tsx +9 -12
  267. package/src/components/ui/opportunity-card.tsx +616 -0
  268. package/src/components/ui/opportunity-edit-modals.tsx +2528 -0
  269. package/src/components/ui/opportunity-summary-tab.tsx +579 -0
  270. package/src/components/ui/pipeline-alerts.tsx +74 -0
  271. package/src/components/ui/pipeline-board.tsx +268 -0
  272. package/src/components/ui/pipeline-chart.tsx +173 -0
  273. package/src/components/ui/pipeline-dialogs.tsx +303 -0
  274. package/src/components/ui/pipeline-primitives.tsx +108 -0
  275. package/src/components/ui/popover.tsx +41 -36
  276. package/src/components/ui/property-cashflow-doughnut-chart.tsx +188 -0
  277. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +185 -0
  278. package/src/components/ui/property-mobile-estimate-line-chart.tsx +393 -0
  279. package/src/components/ui/select.tsx +65 -52
  280. package/src/components/ui/sheet.tsx +55 -52
  281. package/src/components/ui/slider.tsx +54 -77
  282. package/src/components/ui/stage-timeline.tsx +205 -0
  283. package/src/components/ui/switch.tsx +42 -29
  284. package/src/components/ui/table.tsx +28 -28
  285. package/src/components/ui/tabs.tsx +22 -28
  286. package/src/components/ui/textarea.tsx +8 -8
  287. package/src/components/ui/toggle-group.tsx +0 -2
  288. package/src/components/ui/toggle.tsx +13 -15
  289. package/src/components/ui/tooltip.tsx +30 -28
  290. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +191 -0
  291. package/src/components/ui/transactions-income-expense-bar-chart.tsx +205 -0
  292. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +191 -0
  293. package/src/lib/theme-provider.tsx +10 -0
  294. package/src/lib/typography.ts +9 -0
  295. package/src/lib/utils.ts +41 -3
  296. package/src/styles/globals.css +371 -124
  297. package/src/styles/styles-css.ts +1 -1
  298. package/tsup.config.ts +32 -0
  299. package/dist/chunk-K74JRTJR.mjs +0 -105
  300. package/dist/chunk-V7CNWJT3.mjs +0 -10
@@ -0,0 +1,339 @@
1
+ import * as React from "react";
2
+ import { Plus, Trash2 } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogFooter,
10
+ } from "@/components/ui/dialog";
11
+ import { Button, buttonVariants } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Label } from "@/components/ui/label";
14
+ import { Separator } from "@/components/ui/separator";
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from "@/components/ui/select";
22
+
23
+ /**
24
+ * AddColumnModal — WealthX DS (L4 Dialog)
25
+ *
26
+ * Create or edit a Pipeline stage (column). Supports:
27
+ * - Column title (optionally locked for system stages)
28
+ * - Time-based alert thresholds (warning days, priority days)
29
+ * - Task template list (up to 20 tasks with optional AI agent assignment)
30
+ *
31
+ * Data source: `listColumns()` → `Stage` in `loan-crm.ts`
32
+ */
33
+
34
+ const MAX_TASKS = 20;
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Types
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export interface AiAgent {
41
+ id: string;
42
+ name: string;
43
+ }
44
+
45
+ export interface ColumnTask {
46
+ /** Temp numeric ID for new tasks; persisted string ID for existing ones. */
47
+ id: string | number;
48
+ name: string;
49
+ aiAgentId?: string | null;
50
+ }
51
+
52
+ export interface AddColumnEditData {
53
+ id?: string;
54
+ name: string;
55
+ orderIndex?: number;
56
+ warningDays?: number | null;
57
+ priorityDays?: number | null;
58
+ tasks?: ColumnTask[];
59
+ /** When true the title field is read-only (system/fixed stages). */
60
+ isTitleLocked?: boolean;
61
+ }
62
+
63
+ export interface AddColumnModalProps {
64
+ open: boolean;
65
+ onOpenChange: (open: boolean) => void;
66
+ onSave: (data: AddColumnEditData) => void;
67
+ /** Pre-populate for edit mode. Omit (or pass null) for create mode. */
68
+ editData?: AddColumnEditData | null;
69
+ /** Available AI agents for task assignment. Pass [] to hide agent selects. */
70
+ aiAgents?: AiAgent[];
71
+ loading?: boolean;
72
+ className?: string;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Component
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function AddColumnModal({
80
+ open,
81
+ onOpenChange,
82
+ onSave,
83
+ editData,
84
+ aiAgents = [],
85
+ loading = false,
86
+ className,
87
+ }: AddColumnModalProps) {
88
+ const isEdit = !!editData?.id;
89
+ const nextId = React.useRef(0);
90
+
91
+ const [name, setName] = React.useState(editData?.name ?? "");
92
+ const [warningDays, setWarningDays] = React.useState(
93
+ editData?.warningDays != null ? String(editData.warningDays) : "",
94
+ );
95
+ const [priorityDays, setPriorityDays] = React.useState(
96
+ editData?.priorityDays != null ? String(editData.priorityDays) : "",
97
+ );
98
+ const [tasks, setTasks] = React.useState<ColumnTask[]>(editData?.tasks ?? []);
99
+
100
+ // Sync state when dialog opens or editData changes
101
+ React.useEffect(() => {
102
+ if (open) {
103
+ setName(editData?.name ?? "");
104
+ setWarningDays(
105
+ editData?.warningDays != null ? String(editData.warningDays) : "",
106
+ );
107
+ setPriorityDays(
108
+ editData?.priorityDays != null ? String(editData.priorityDays) : "",
109
+ );
110
+ setTasks(editData?.tasks ?? []);
111
+ }
112
+ }, [open, editData]);
113
+
114
+ // Validation
115
+ const wDays = warningDays !== "" ? parseInt(warningDays, 10) : null;
116
+ const pDays = priorityDays !== "" ? parseInt(priorityDays, 10) : null;
117
+ const daysError =
118
+ wDays !== null && pDays !== null && wDays >= pDays
119
+ ? "Warning days must be less than priority days."
120
+ : null;
121
+
122
+ const taskNames = tasks.map((t) => t.name.trim().toLowerCase());
123
+ const hasDuplicateTasks =
124
+ new Set(taskNames).size !== taskNames.filter(Boolean).length;
125
+
126
+ const isValid =
127
+ name.trim().length > 0 &&
128
+ !daysError &&
129
+ !hasDuplicateTasks &&
130
+ tasks.every((t) => t.name.trim().length > 0);
131
+
132
+ function addTask() {
133
+ if (tasks.length >= MAX_TASKS) return;
134
+ setTasks((prev) => [
135
+ ...prev,
136
+ { id: nextId.current++, name: "", aiAgentId: null },
137
+ ]);
138
+ }
139
+
140
+ function removeTask(id: string | number) {
141
+ setTasks((prev) => prev.filter((t) => t.id !== id));
142
+ }
143
+
144
+ function updateTask(
145
+ id: string | number,
146
+ patch: Partial<Pick<ColumnTask, "name" | "aiAgentId">>,
147
+ ) {
148
+ setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, ...patch } : t)));
149
+ }
150
+
151
+ function handleSave() {
152
+ onSave({
153
+ id: editData?.id,
154
+ name: name.trim(),
155
+ orderIndex: editData?.orderIndex,
156
+ warningDays: wDays,
157
+ priorityDays: pDays,
158
+ tasks,
159
+ isTitleLocked: editData?.isTitleLocked,
160
+ });
161
+ }
162
+
163
+ return (
164
+ <Dialog open={open} onOpenChange={loading ? undefined : onOpenChange}>
165
+ <DialogContent className={cn("max-w-md", className)}>
166
+ <DialogHeader>
167
+ <DialogTitle>{isEdit ? "Edit Column" : "Add New Column"}</DialogTitle>
168
+ </DialogHeader>
169
+
170
+ <div className="flex flex-col gap-4">
171
+ {/* Column title */}
172
+ <div className="flex flex-col gap-1.5">
173
+ <Label>Column title</Label>
174
+ <Input
175
+ value={name}
176
+ onChange={(e) => setName(e.target.value)}
177
+ placeholder="e.g. Pre-approval"
178
+ disabled={editData?.isTitleLocked || loading}
179
+ />
180
+ {editData?.isTitleLocked && (
181
+ <p className="text-xs text-muted-foreground">
182
+ This column title is locked and cannot be changed.
183
+ </p>
184
+ )}
185
+ </div>
186
+
187
+ {/* Time-based alerts */}
188
+ <div className="flex flex-col gap-2">
189
+ <span className="text-sm font-medium text-foreground">
190
+ Time-based alerts
191
+ </span>
192
+ <div className="grid grid-cols-2 gap-3">
193
+ <div className="flex flex-col gap-1.5">
194
+ <Label className="font-normal text-muted-foreground">
195
+ Warning after (days)
196
+ </Label>
197
+ <Input
198
+ type="number"
199
+ min={1}
200
+ value={warningDays}
201
+ onChange={(e) => setWarningDays(e.target.value)}
202
+ placeholder="e.g. 7"
203
+ disabled={loading}
204
+ />
205
+ </div>
206
+ <div className="flex flex-col gap-1.5">
207
+ <Label className="font-normal text-muted-foreground">
208
+ Priority after (days)
209
+ </Label>
210
+ <Input
211
+ type="number"
212
+ min={1}
213
+ value={priorityDays}
214
+ onChange={(e) => setPriorityDays(e.target.value)}
215
+ placeholder="e.g. 14"
216
+ disabled={loading}
217
+ />
218
+ </div>
219
+ </div>
220
+ {daysError && (
221
+ <p className="text-xs text-destructive">{daysError}</p>
222
+ )}
223
+ </div>
224
+
225
+ <Separator />
226
+
227
+ {/* Task templates */}
228
+ <div className="flex flex-col gap-2">
229
+ <div className="flex items-center justify-between">
230
+ <span className="text-sm font-medium text-foreground">
231
+ Task templates
232
+ </span>
233
+ <Button
234
+ variant="outline"
235
+ size="sm"
236
+ className="h-7 gap-1 text-xs"
237
+ onClick={addTask}
238
+ disabled={tasks.length >= MAX_TASKS || loading}
239
+ >
240
+ <Plus className="size-3" />
241
+ Add ({tasks.length}/{MAX_TASKS})
242
+ </Button>
243
+ </div>
244
+
245
+ {tasks.length === 0 && (
246
+ <p className="text-xs text-muted-foreground">
247
+ No tasks yet. Tasks will be created for each new opportunity
248
+ added to this column.
249
+ </p>
250
+ )}
251
+
252
+ <div className="flex flex-col gap-2">
253
+ {tasks.map((task) => (
254
+ <div key={task.id} className="flex flex-col gap-1.5">
255
+ <div className="flex items-center gap-2">
256
+ <Input
257
+ value={task.name}
258
+ onChange={(e) =>
259
+ updateTask(task.id, { name: e.target.value })
260
+ }
261
+ placeholder="Task name"
262
+ className="flex-1"
263
+ disabled={loading}
264
+ />
265
+ <button
266
+ onClick={() => removeTask(task.id)}
267
+ disabled={loading}
268
+ className={cn(
269
+ buttonVariants({ variant: "ghost", size: "icon" }),
270
+ "size-9 shrink-0 text-muted-foreground hover:text-destructive",
271
+ )}
272
+ aria-label="Remove task"
273
+ >
274
+ <Trash2 className="size-4" />
275
+ </button>
276
+ </div>
277
+
278
+ {aiAgents.length > 0 && (
279
+ <Select
280
+ value={task.aiAgentId ?? undefined}
281
+ onValueChange={(v) =>
282
+ updateTask(task.id, { aiAgentId: v || null })
283
+ }
284
+ >
285
+ <SelectTrigger size="sm" className="w-full">
286
+ <span
287
+ className={cn(
288
+ !task.aiAgentId && "text-muted-foreground",
289
+ )}
290
+ >
291
+ {task.aiAgentId
292
+ ? (aiAgents.find((a) => a.id === task.aiAgentId)
293
+ ?.name ?? task.aiAgentId)
294
+ : "Select agent"}
295
+ </span>
296
+ </SelectTrigger>
297
+ <SelectContent>
298
+ {aiAgents.map((agent) => (
299
+ <SelectItem key={agent.id} value={agent.id}>
300
+ {agent.name}
301
+ </SelectItem>
302
+ ))}
303
+ </SelectContent>
304
+ </Select>
305
+ )}
306
+ </div>
307
+ ))}
308
+ </div>
309
+
310
+ {hasDuplicateTasks && (
311
+ <p className="text-xs text-destructive">
312
+ Task names must be unique.
313
+ </p>
314
+ )}
315
+ </div>
316
+ </div>
317
+
318
+ <DialogFooter>
319
+ <Button
320
+ variant="outline"
321
+ onClick={() => onOpenChange(false)}
322
+ disabled={loading}
323
+ >
324
+ Cancel
325
+ </Button>
326
+ <Button onClick={handleSave} disabled={loading || !isValid}>
327
+ {loading
328
+ ? isEdit
329
+ ? "Saving…"
330
+ : "Adding…"
331
+ : isEdit
332
+ ? "Save changes"
333
+ : "Add column"}
334
+ </Button>
335
+ </DialogFooter>
336
+ </DialogContent>
337
+ </Dialog>
338
+ );
339
+ }
@@ -0,0 +1,290 @@
1
+ import * as React from "react";
2
+ import { Search, X } from "lucide-react";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogFooter,
9
+ } from "@/components/ui/dialog";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Input } from "@/components/ui/input";
12
+ import { Checkbox } from "@/components/ui/checkbox";
13
+ import { Spinner } from "@/components/ui/spinner";
14
+ import { cn } from "@/lib/utils";
15
+
16
+ /**
17
+ * AddLeadModal — WealthX DS (L4 Dialog)
18
+ *
19
+ * Search and select contacts to add as pipeline leads.
20
+ * Pagination and search are server-side — the parent controls them.
21
+ *
22
+ * Data source: `getLeadContactList()` in `loan-crm.ts`
23
+ */
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export interface LeadContact {
30
+ id: string;
31
+ name: string;
32
+ email?: string;
33
+ phone?: string;
34
+ }
35
+
36
+ export interface AddLeadModalProps {
37
+ open: boolean;
38
+ onOpenChange: (open: boolean) => void;
39
+ /** Contacts for the current page — supplied and paginated by the parent. */
40
+ contacts: LeadContact[];
41
+ /** Total contacts matching the current search (drives pagination). */
42
+ total: number;
43
+ isLoading?: boolean;
44
+ isSubmitting?: boolean;
45
+ /** Controlled search query. */
46
+ searchValue?: string;
47
+ onSearchChange?: (value: string) => void;
48
+ /** 0-indexed current page. */
49
+ page?: number;
50
+ pageSize?: number;
51
+ onPageChange?: (page: number) => void;
52
+ /** Called with the selected contact IDs when the user clicks Add. */
53
+ onAdd?: (selectedIds: string[]) => void;
54
+ className?: string;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Component
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export function AddLeadModal({
62
+ open,
63
+ onOpenChange,
64
+ contacts,
65
+ total,
66
+ isLoading = false,
67
+ isSubmitting = false,
68
+ searchValue = "",
69
+ onSearchChange,
70
+ page = 0,
71
+ pageSize = 10,
72
+ onPageChange,
73
+ onAdd,
74
+ className,
75
+ }: AddLeadModalProps) {
76
+ const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set());
77
+
78
+ // Reset selection when dialog opens
79
+ React.useEffect(() => {
80
+ if (open) setSelectedIds(new Set());
81
+ }, [open]);
82
+
83
+ const allPageSelected =
84
+ contacts.length > 0 && contacts.every((c) => selectedIds.has(c.id));
85
+ const somePageSelected = contacts.some((c) => selectedIds.has(c.id));
86
+
87
+ function toggleAll() {
88
+ setSelectedIds((prev) => {
89
+ const next = new Set(prev);
90
+ if (allPageSelected) {
91
+ contacts.forEach((c) => next.delete(c.id));
92
+ } else {
93
+ contacts.forEach((c) => next.add(c.id));
94
+ }
95
+ return next;
96
+ });
97
+ }
98
+
99
+ function toggleOne(id: string) {
100
+ setSelectedIds((prev) => {
101
+ const next = new Set(prev);
102
+ next.has(id) ? next.delete(id) : next.add(id);
103
+ return next;
104
+ });
105
+ }
106
+
107
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
108
+ const canPrev = page > 0;
109
+ const canNext = page < totalPages - 1;
110
+
111
+ return (
112
+ <Dialog open={open} onOpenChange={isSubmitting ? undefined : onOpenChange}>
113
+ <DialogContent className={cn("sm:max-w-2xl", className)}>
114
+ <DialogHeader>
115
+ <DialogTitle>Add Lead</DialogTitle>
116
+ </DialogHeader>
117
+
118
+ {/* Search */}
119
+ <div className="relative">
120
+ <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
121
+ <Input
122
+ placeholder="Search contacts…"
123
+ value={searchValue}
124
+ onChange={(e) => onSearchChange?.(e.target.value)}
125
+ className="pl-8 pr-8"
126
+ disabled={isSubmitting}
127
+ />
128
+ {searchValue && (
129
+ <button
130
+ onClick={() => onSearchChange?.("")}
131
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
132
+ aria-label="Clear search"
133
+ >
134
+ <X className="size-3.5" />
135
+ </button>
136
+ )}
137
+ </div>
138
+
139
+ {/* Contact table
140
+ border-separate + border-spacing-0 + inner <div h-12>: row height is
141
+ anchored to the fixed-height div, not td padding+content+border arithmetic,
142
+ so selected and unselected rows are always pixel-identical. */}
143
+ <div className="relative min-h-[200px] overflow-auto border border-border">
144
+ {isLoading && (
145
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/60">
146
+ <Spinner />
147
+ </div>
148
+ )}
149
+ <table className="w-full border-separate border-spacing-0 text-sm">
150
+ <thead className="sticky top-0 bg-muted/50">
151
+ <tr>
152
+ <th className="w-10 border-b border-border">
153
+ <div className="flex h-10 w-full items-center justify-center">
154
+ <span onClick={(e) => e.stopPropagation()}>
155
+ <Checkbox
156
+ checked={
157
+ somePageSelected && !allPageSelected
158
+ ? "indeterminate"
159
+ : allPageSelected
160
+ }
161
+ onCheckedChange={toggleAll}
162
+ aria-label="Select all on page"
163
+ disabled={contacts.length === 0 || isSubmitting}
164
+ />
165
+ </span>
166
+ </div>
167
+ </th>
168
+ {(["Name", "Email", "Phone"] as const).map((col) => (
169
+ <th key={col} className="border-b border-border">
170
+ <div className="flex h-10 items-center px-3 text-xs font-semibold text-foreground">
171
+ {col}
172
+ </div>
173
+ </th>
174
+ ))}
175
+ </tr>
176
+ </thead>
177
+ <tbody>
178
+ {contacts.length === 0 && !isLoading ? (
179
+ <tr>
180
+ <td
181
+ colSpan={4}
182
+ className="py-10 text-center text-xs text-muted-foreground"
183
+ >
184
+ No contacts found.
185
+ </td>
186
+ </tr>
187
+ ) : (
188
+ contacts.map((contact, idx) => {
189
+ const isSelected = selectedIds.has(contact.id);
190
+ const tdCls = cn(
191
+ idx < contacts.length - 1 && "border-b border-border",
192
+ );
193
+ return (
194
+ <tr
195
+ key={contact.id}
196
+ className={cn(
197
+ "cursor-pointer",
198
+ isSelected
199
+ ? "bg-primary/5 hover:bg-primary/10"
200
+ : "hover:bg-muted/30",
201
+ )}
202
+ onClick={() => !isSubmitting && toggleOne(contact.id)}
203
+ >
204
+ <td className={tdCls}>
205
+ <div className="flex h-12 w-full items-center justify-center">
206
+ <span onClick={(e) => e.stopPropagation()}>
207
+ <Checkbox
208
+ checked={isSelected}
209
+ onCheckedChange={() => toggleOne(contact.id)}
210
+ disabled={isSubmitting}
211
+ />
212
+ </span>
213
+ </div>
214
+ </td>
215
+ <td className={tdCls}>
216
+ <div className="flex h-12 items-center px-3 font-medium text-foreground">
217
+ {contact.name}
218
+ </div>
219
+ </td>
220
+ <td className={tdCls}>
221
+ <div className="flex h-12 items-center px-3 text-muted-foreground">
222
+ {contact.email ?? "—"}
223
+ </div>
224
+ </td>
225
+ <td className={tdCls}>
226
+ <div className="flex h-12 items-center px-3 text-muted-foreground">
227
+ {contact.phone ?? "—"}
228
+ </div>
229
+ </td>
230
+ </tr>
231
+ );
232
+ })
233
+ )}
234
+ </tbody>
235
+ </table>
236
+ </div>
237
+
238
+ {/* Pagination */}
239
+ {totalPages > 1 && (
240
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
241
+ <span>
242
+ {total} contact{total !== 1 ? "s" : ""}
243
+ </span>
244
+ <div className="flex items-center gap-2">
245
+ <Button
246
+ variant="outline"
247
+ size="sm"
248
+ className="h-7 text-xs"
249
+ onClick={() => onPageChange?.(page - 1)}
250
+ disabled={!canPrev || isLoading || isSubmitting}
251
+ >
252
+ Prev
253
+ </Button>
254
+ <span>
255
+ {page + 1} / {totalPages}
256
+ </span>
257
+ <Button
258
+ variant="outline"
259
+ size="sm"
260
+ className="h-7 text-xs"
261
+ onClick={() => onPageChange?.(page + 1)}
262
+ disabled={!canNext || isLoading || isSubmitting}
263
+ >
264
+ Next
265
+ </Button>
266
+ </div>
267
+ </div>
268
+ )}
269
+
270
+ <DialogFooter>
271
+ <Button
272
+ variant="outline"
273
+ onClick={() => onOpenChange(false)}
274
+ disabled={isSubmitting}
275
+ >
276
+ Cancel
277
+ </Button>
278
+ <Button
279
+ onClick={() => onAdd?.([...selectedIds])}
280
+ disabled={selectedIds.size === 0 || isSubmitting}
281
+ >
282
+ {isSubmitting
283
+ ? "Adding…"
284
+ : `Add${selectedIds.size > 0 ? ` (${selectedIds.size})` : ""}`}
285
+ </Button>
286
+ </DialogFooter>
287
+ </DialogContent>
288
+ </Dialog>
289
+ );
290
+ }