realtimex-crm 0.1.2

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 (468) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +104 -0
  3. package/dist/assets/DealList-DqDrFeDV.js +59 -0
  4. package/dist/assets/DealList-DqDrFeDV.js.map +1 -0
  5. package/dist/assets/index-BiQoGq1P.css +1 -0
  6. package/dist/assets/index-CDIy4x-0.js +152 -0
  7. package/dist/assets/index-CDIy4x-0.js.map +1 -0
  8. package/dist/auth-callback.html +140 -0
  9. package/dist/favicon.ico +0 -0
  10. package/dist/img/adding-users.png +0 -0
  11. package/dist/img/empty.svg +42 -0
  12. package/dist/index.html +1 -0
  13. package/dist/logo192.png +0 -0
  14. package/dist/logo512.png +0 -0
  15. package/dist/logos/0.png +0 -0
  16. package/dist/logos/1.png +0 -0
  17. package/dist/logos/10.png +0 -0
  18. package/dist/logos/11.png +0 -0
  19. package/dist/logos/12.png +0 -0
  20. package/dist/logos/13.png +0 -0
  21. package/dist/logos/14.png +0 -0
  22. package/dist/logos/15.png +0 -0
  23. package/dist/logos/16.png +0 -0
  24. package/dist/logos/17.png +0 -0
  25. package/dist/logos/18.png +0 -0
  26. package/dist/logos/19.png +0 -0
  27. package/dist/logos/2.png +0 -0
  28. package/dist/logos/20.png +0 -0
  29. package/dist/logos/21.png +0 -0
  30. package/dist/logos/22.png +0 -0
  31. package/dist/logos/23.png +0 -0
  32. package/dist/logos/24.png +0 -0
  33. package/dist/logos/25.png +0 -0
  34. package/dist/logos/26.png +0 -0
  35. package/dist/logos/27.png +0 -0
  36. package/dist/logos/28.png +0 -0
  37. package/dist/logos/29.png +0 -0
  38. package/dist/logos/3.png +0 -0
  39. package/dist/logos/30.png +0 -0
  40. package/dist/logos/31.png +0 -0
  41. package/dist/logos/32.png +0 -0
  42. package/dist/logos/33.png +0 -0
  43. package/dist/logos/34.png +0 -0
  44. package/dist/logos/35.png +0 -0
  45. package/dist/logos/36.png +0 -0
  46. package/dist/logos/37.png +0 -0
  47. package/dist/logos/38.png +0 -0
  48. package/dist/logos/39.png +0 -0
  49. package/dist/logos/4.png +0 -0
  50. package/dist/logos/40.png +0 -0
  51. package/dist/logos/41.png +0 -0
  52. package/dist/logos/42.png +0 -0
  53. package/dist/logos/43.png +0 -0
  54. package/dist/logos/44.png +0 -0
  55. package/dist/logos/45.png +0 -0
  56. package/dist/logos/46.png +0 -0
  57. package/dist/logos/47.png +0 -0
  58. package/dist/logos/48.png +0 -0
  59. package/dist/logos/49.png +0 -0
  60. package/dist/logos/5.png +0 -0
  61. package/dist/logos/50.png +0 -0
  62. package/dist/logos/51.png +0 -0
  63. package/dist/logos/52.png +0 -0
  64. package/dist/logos/53.png +0 -0
  65. package/dist/logos/54.png +0 -0
  66. package/dist/logos/55.png +0 -0
  67. package/dist/logos/6.png +0 -0
  68. package/dist/logos/7.png +0 -0
  69. package/dist/logos/8.png +0 -0
  70. package/dist/logos/9.png +0 -0
  71. package/dist/logos/Readme.md +1 -0
  72. package/dist/logos/logo_atomic_crm.svg +14 -0
  73. package/dist/logos/logo_atomic_crm_dark.svg +14 -0
  74. package/dist/logos/logo_atomic_crm_light.svg +14 -0
  75. package/dist/manifest.json +25 -0
  76. package/dist/robots.txt +3 -0
  77. package/dist/stats.html +4949 -0
  78. package/package.json +152 -0
  79. package/public/auth-callback.html +140 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/img/adding-users.png +0 -0
  82. package/public/img/empty.svg +42 -0
  83. package/public/logo192.png +0 -0
  84. package/public/logo512.png +0 -0
  85. package/public/logos/0.png +0 -0
  86. package/public/logos/1.png +0 -0
  87. package/public/logos/10.png +0 -0
  88. package/public/logos/11.png +0 -0
  89. package/public/logos/12.png +0 -0
  90. package/public/logos/13.png +0 -0
  91. package/public/logos/14.png +0 -0
  92. package/public/logos/15.png +0 -0
  93. package/public/logos/16.png +0 -0
  94. package/public/logos/17.png +0 -0
  95. package/public/logos/18.png +0 -0
  96. package/public/logos/19.png +0 -0
  97. package/public/logos/2.png +0 -0
  98. package/public/logos/20.png +0 -0
  99. package/public/logos/21.png +0 -0
  100. package/public/logos/22.png +0 -0
  101. package/public/logos/23.png +0 -0
  102. package/public/logos/24.png +0 -0
  103. package/public/logos/25.png +0 -0
  104. package/public/logos/26.png +0 -0
  105. package/public/logos/27.png +0 -0
  106. package/public/logos/28.png +0 -0
  107. package/public/logos/29.png +0 -0
  108. package/public/logos/3.png +0 -0
  109. package/public/logos/30.png +0 -0
  110. package/public/logos/31.png +0 -0
  111. package/public/logos/32.png +0 -0
  112. package/public/logos/33.png +0 -0
  113. package/public/logos/34.png +0 -0
  114. package/public/logos/35.png +0 -0
  115. package/public/logos/36.png +0 -0
  116. package/public/logos/37.png +0 -0
  117. package/public/logos/38.png +0 -0
  118. package/public/logos/39.png +0 -0
  119. package/public/logos/4.png +0 -0
  120. package/public/logos/40.png +0 -0
  121. package/public/logos/41.png +0 -0
  122. package/public/logos/42.png +0 -0
  123. package/public/logos/43.png +0 -0
  124. package/public/logos/44.png +0 -0
  125. package/public/logos/45.png +0 -0
  126. package/public/logos/46.png +0 -0
  127. package/public/logos/47.png +0 -0
  128. package/public/logos/48.png +0 -0
  129. package/public/logos/49.png +0 -0
  130. package/public/logos/5.png +0 -0
  131. package/public/logos/50.png +0 -0
  132. package/public/logos/51.png +0 -0
  133. package/public/logos/52.png +0 -0
  134. package/public/logos/53.png +0 -0
  135. package/public/logos/54.png +0 -0
  136. package/public/logos/55.png +0 -0
  137. package/public/logos/6.png +0 -0
  138. package/public/logos/7.png +0 -0
  139. package/public/logos/8.png +0 -0
  140. package/public/logos/9.png +0 -0
  141. package/public/logos/Readme.md +1 -0
  142. package/public/logos/logo_atomic_crm.svg +14 -0
  143. package/public/logos/logo_atomic_crm_dark.svg +14 -0
  144. package/public/logos/logo_atomic_crm_light.svg +14 -0
  145. package/public/manifest.json +25 -0
  146. package/public/robots.txt +3 -0
  147. package/src/App.css +42 -0
  148. package/src/App.tsx +58 -0
  149. package/src/assets/react.svg +1 -0
  150. package/src/components/admin/Readme.md +40 -0
  151. package/src/components/admin/admin.tsx +132 -0
  152. package/src/components/admin/app-sidebar.tsx +166 -0
  153. package/src/components/admin/array-field.tsx +59 -0
  154. package/src/components/admin/array-input.tsx +201 -0
  155. package/src/components/admin/authentication.tsx +86 -0
  156. package/src/components/admin/autocomplete-array-input.tsx +254 -0
  157. package/src/components/admin/autocomplete-input.tsx +296 -0
  158. package/src/components/admin/badge-field.tsx +65 -0
  159. package/src/components/admin/boolean-input.tsx +116 -0
  160. package/src/components/admin/breadcrumb.tsx +135 -0
  161. package/src/components/admin/bulk-actions-toolbar.tsx +83 -0
  162. package/src/components/admin/bulk-delete-button.tsx +70 -0
  163. package/src/components/admin/bulk-export-button.tsx +76 -0
  164. package/src/components/admin/cancel-button.tsx +46 -0
  165. package/src/components/admin/columns-button.tsx +345 -0
  166. package/src/components/admin/confirm.tsx +166 -0
  167. package/src/components/admin/count.tsx +94 -0
  168. package/src/components/admin/create-button.tsx +58 -0
  169. package/src/components/admin/create.tsx +132 -0
  170. package/src/components/admin/data-table.tsx +520 -0
  171. package/src/components/admin/date-field.tsx +136 -0
  172. package/src/components/admin/date-input.tsx +317 -0
  173. package/src/components/admin/date-time-input.tsx +331 -0
  174. package/src/components/admin/delete-button.tsx +113 -0
  175. package/src/components/admin/edit-button.tsx +64 -0
  176. package/src/components/admin/edit-guesser.tsx +157 -0
  177. package/src/components/admin/edit.tsx +152 -0
  178. package/src/components/admin/email-field.tsx +74 -0
  179. package/src/components/admin/error.tsx +111 -0
  180. package/src/components/admin/export-button.tsx +126 -0
  181. package/src/components/admin/field-toggle.tsx +164 -0
  182. package/src/components/admin/file-field.tsx +123 -0
  183. package/src/components/admin/file-input.tsx +361 -0
  184. package/src/components/admin/filter-form.tsx +510 -0
  185. package/src/components/admin/form.tsx +312 -0
  186. package/src/components/admin/icon-button-with-tooltip.tsx +85 -0
  187. package/src/components/admin/index.ts +73 -0
  188. package/src/components/admin/input-helper-text.tsx +29 -0
  189. package/src/components/admin/layout.tsx +69 -0
  190. package/src/components/admin/list-guesser.tsx +239 -0
  191. package/src/components/admin/list-pagination.tsx +247 -0
  192. package/src/components/admin/list.tsx +178 -0
  193. package/src/components/admin/loading.tsx +40 -0
  194. package/src/components/admin/locales-menu-button.tsx +60 -0
  195. package/src/components/admin/login-page.tsx +104 -0
  196. package/src/components/admin/notification.tsx +114 -0
  197. package/src/components/admin/number-field.tsx +84 -0
  198. package/src/components/admin/number-input.tsx +124 -0
  199. package/src/components/admin/radio-button-group-input.tsx +184 -0
  200. package/src/components/admin/ready.tsx +55 -0
  201. package/src/components/admin/record-field.tsx +132 -0
  202. package/src/components/admin/reference-array-field.tsx +152 -0
  203. package/src/components/admin/reference-array-input.tsx +68 -0
  204. package/src/components/admin/reference-field.tsx +153 -0
  205. package/src/components/admin/reference-input.tsx +46 -0
  206. package/src/components/admin/reference-many-count.tsx +92 -0
  207. package/src/components/admin/reference-many-field.tsx +132 -0
  208. package/src/components/admin/refresh-button.tsx +31 -0
  209. package/src/components/admin/saved-queries.tsx +174 -0
  210. package/src/components/admin/search-input.tsx +57 -0
  211. package/src/components/admin/select-field.tsx +111 -0
  212. package/src/components/admin/select-input.tsx +323 -0
  213. package/src/components/admin/show-button.tsx +57 -0
  214. package/src/components/admin/show-guesser.tsx +215 -0
  215. package/src/components/admin/show.tsx +184 -0
  216. package/src/components/admin/simple-form-iterator.tsx +582 -0
  217. package/src/components/admin/simple-form.tsx +95 -0
  218. package/src/components/admin/simple-show-layout.tsx +8 -0
  219. package/src/components/admin/single-field-list.tsx +67 -0
  220. package/src/components/admin/sort-button.tsx +152 -0
  221. package/src/components/admin/spinner.tsx +46 -0
  222. package/src/components/admin/text-field.tsx +60 -0
  223. package/src/components/admin/text-input.tsx +77 -0
  224. package/src/components/admin/theme-mode-toggle.tsx +48 -0
  225. package/src/components/admin/theme-provider.tsx +74 -0
  226. package/src/components/admin/toggle-filter-button.tsx +77 -0
  227. package/src/components/admin/url-field.tsx +83 -0
  228. package/src/components/admin/user-menu.tsx +84 -0
  229. package/src/components/atomic-crm/activity/ActivityLog.tsx +54 -0
  230. package/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx +50 -0
  231. package/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx +42 -0
  232. package/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx +71 -0
  233. package/src/components/atomic-crm/activity/ActivityLogContext.tsx +11 -0
  234. package/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx +41 -0
  235. package/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx +84 -0
  236. package/src/components/atomic-crm/activity/ActivityLogIterator.tsx +80 -0
  237. package/src/components/atomic-crm/activity/ActivityLogNote.tsx +36 -0
  238. package/src/components/atomic-crm/companies/AutocompleteCompanyInput.tsx +43 -0
  239. package/src/components/atomic-crm/companies/CompanyAside.tsx +207 -0
  240. package/src/components/atomic-crm/companies/CompanyAvatar.tsx +29 -0
  241. package/src/components/atomic-crm/companies/CompanyCard.tsx +88 -0
  242. package/src/components/atomic-crm/companies/CompanyCreate.tsx +41 -0
  243. package/src/components/atomic-crm/companies/CompanyEdit.tsx +33 -0
  244. package/src/components/atomic-crm/companies/CompanyEmpty.tsx +26 -0
  245. package/src/components/atomic-crm/companies/CompanyInputs.tsx +160 -0
  246. package/src/components/atomic-crm/companies/CompanyList.tsx +54 -0
  247. package/src/components/atomic-crm/companies/CompanyListFilter.tsx +55 -0
  248. package/src/components/atomic-crm/companies/CompanyShow.tsx +241 -0
  249. package/src/components/atomic-crm/companies/GridList.tsx +46 -0
  250. package/src/components/atomic-crm/companies/index.ts +11 -0
  251. package/src/components/atomic-crm/companies/sizes.ts +7 -0
  252. package/src/components/atomic-crm/consts.ts +5 -0
  253. package/src/components/atomic-crm/contacts/Avatar.tsx +40 -0
  254. package/src/components/atomic-crm/contacts/ContactAside.tsx +187 -0
  255. package/src/components/atomic-crm/contacts/ContactCreate.tsx +34 -0
  256. package/src/components/atomic-crm/contacts/ContactEdit.tsx +32 -0
  257. package/src/components/atomic-crm/contacts/ContactEmpty.tsx +28 -0
  258. package/src/components/atomic-crm/contacts/ContactImportButton.tsx +213 -0
  259. package/src/components/atomic-crm/contacts/ContactInputs.tsx +209 -0
  260. package/src/components/atomic-crm/contacts/ContactList.tsx +116 -0
  261. package/src/components/atomic-crm/contacts/ContactListContent.tsx +107 -0
  262. package/src/components/atomic-crm/contacts/ContactListFilter.tsx +126 -0
  263. package/src/components/atomic-crm/contacts/ContactMergeButton.tsx +263 -0
  264. package/src/components/atomic-crm/contacts/ContactShow.tsx +76 -0
  265. package/src/components/atomic-crm/contacts/ExportVCardButton.tsx +79 -0
  266. package/src/components/atomic-crm/contacts/TagsList.tsx +33 -0
  267. package/src/components/atomic-crm/contacts/TagsListEdit.tsx +155 -0
  268. package/src/components/atomic-crm/contacts/contacts_export.csv +3 -0
  269. package/src/components/atomic-crm/contacts/exportToVCard.ts +104 -0
  270. package/src/components/atomic-crm/contacts/index.tsx +14 -0
  271. package/src/components/atomic-crm/contacts/useContactImport.tsx +206 -0
  272. package/src/components/atomic-crm/dashboard/Dashboard.tsx +66 -0
  273. package/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx +22 -0
  274. package/src/components/atomic-crm/dashboard/DashboardStepper.tsx +72 -0
  275. package/src/components/atomic-crm/dashboard/DealsChart.tsx +202 -0
  276. package/src/components/atomic-crm/dashboard/DealsPipeline.tsx +90 -0
  277. package/src/components/atomic-crm/dashboard/HotContacts.tsx +92 -0
  278. package/src/components/atomic-crm/dashboard/LatestNotes.tsx +116 -0
  279. package/src/components/atomic-crm/dashboard/TasksList.tsx +69 -0
  280. package/src/components/atomic-crm/dashboard/TasksListEmpty.tsx +22 -0
  281. package/src/components/atomic-crm/dashboard/TasksListFilter.tsx +72 -0
  282. package/src/components/atomic-crm/dashboard/Welcome.tsx +41 -0
  283. package/src/components/atomic-crm/deals/ContactList.tsx +31 -0
  284. package/src/components/atomic-crm/deals/DealArchivedList.tsx +105 -0
  285. package/src/components/atomic-crm/deals/DealCard.tsx +78 -0
  286. package/src/components/atomic-crm/deals/DealColumn.tsx +52 -0
  287. package/src/components/atomic-crm/deals/DealCreate.tsx +95 -0
  288. package/src/components/atomic-crm/deals/DealEdit.tsx +81 -0
  289. package/src/components/atomic-crm/deals/DealEmpty.tsx +63 -0
  290. package/src/components/atomic-crm/deals/DealInputs.tsx +103 -0
  291. package/src/components/atomic-crm/deals/DealList.tsx +95 -0
  292. package/src/components/atomic-crm/deals/DealListContent.tsx +245 -0
  293. package/src/components/atomic-crm/deals/DealShow.tsx +260 -0
  294. package/src/components/atomic-crm/deals/OnlyMineInput.tsx +30 -0
  295. package/src/components/atomic-crm/deals/deal.ts +5 -0
  296. package/src/components/atomic-crm/deals/dealUtils.ts +26 -0
  297. package/src/components/atomic-crm/deals/index.ts +6 -0
  298. package/src/components/atomic-crm/deals/stages.ts +28 -0
  299. package/src/components/atomic-crm/filters/FilterCategory.tsx +20 -0
  300. package/src/components/atomic-crm/layout/FormToolbar.tsx +12 -0
  301. package/src/components/atomic-crm/layout/Header.tsx +134 -0
  302. package/src/components/atomic-crm/layout/Layout.tsx +21 -0
  303. package/src/components/atomic-crm/layout/TopToolbar.tsx +24 -0
  304. package/src/components/atomic-crm/login/LoginSkeleton.tsx +18 -0
  305. package/src/components/atomic-crm/login/SignupPage.tsx +150 -0
  306. package/src/components/atomic-crm/login/StartPage.tsx +27 -0
  307. package/src/components/atomic-crm/misc/AsideSection.tsx +21 -0
  308. package/src/components/atomic-crm/misc/ContactOption.tsx +26 -0
  309. package/src/components/atomic-crm/misc/ImageEditorField.tsx +206 -0
  310. package/src/components/atomic-crm/misc/RelativeDate.tsx +5 -0
  311. package/src/components/atomic-crm/misc/Status.tsx +28 -0
  312. package/src/components/atomic-crm/misc/fetchWithTimeout.ts +19 -0
  313. package/src/components/atomic-crm/misc/isLinkedInUrl.ts +15 -0
  314. package/src/components/atomic-crm/misc/unsupportedDomains.const.ts +105 -0
  315. package/src/components/atomic-crm/misc/useAppBarHeight.ts +9 -0
  316. package/src/components/atomic-crm/misc/usePapaParse.tsx +144 -0
  317. package/src/components/atomic-crm/notes/Note.tsx +187 -0
  318. package/src/components/atomic-crm/notes/NoteAttachments.tsx +56 -0
  319. package/src/components/atomic-crm/notes/NoteCreate.tsx +112 -0
  320. package/src/components/atomic-crm/notes/NoteInputs.tsx +92 -0
  321. package/src/components/atomic-crm/notes/NotesIterator.tsx +37 -0
  322. package/src/components/atomic-crm/notes/StatusSelector.tsx +39 -0
  323. package/src/components/atomic-crm/notes/index.ts +3 -0
  324. package/src/components/atomic-crm/notes/utils.ts +13 -0
  325. package/src/components/atomic-crm/providers/commons/activity.ts +174 -0
  326. package/src/components/atomic-crm/providers/commons/canAccess.ts +26 -0
  327. package/src/components/atomic-crm/providers/commons/getCompanyAvatar.spec.ts +20 -0
  328. package/src/components/atomic-crm/providers/commons/getCompanyAvatar.ts +21 -0
  329. package/src/components/atomic-crm/providers/commons/getContactAvatar.spec.ts +80 -0
  330. package/src/components/atomic-crm/providers/commons/getContactAvatar.ts +70 -0
  331. package/src/components/atomic-crm/providers/commons/mergeContacts.ts +185 -0
  332. package/src/components/atomic-crm/providers/fakerest/authProvider.ts +74 -0
  333. package/src/components/atomic-crm/providers/fakerest/dataGenerator/companies.ts +53 -0
  334. package/src/components/atomic-crm/providers/fakerest/dataGenerator/contactNotes.ts +25 -0
  335. package/src/components/atomic-crm/providers/fakerest/dataGenerator/contacts.ts +103 -0
  336. package/src/components/atomic-crm/providers/fakerest/dataGenerator/dealNotes.ts +19 -0
  337. package/src/components/atomic-crm/providers/fakerest/dataGenerator/deals.ts +53 -0
  338. package/src/components/atomic-crm/providers/fakerest/dataGenerator/finalize.ts +10 -0
  339. package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +25 -0
  340. package/src/components/atomic-crm/providers/fakerest/dataGenerator/sales.ts +37 -0
  341. package/src/components/atomic-crm/providers/fakerest/dataGenerator/tags.ts +14 -0
  342. package/src/components/atomic-crm/providers/fakerest/dataGenerator/tasks.ts +55 -0
  343. package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +21 -0
  344. package/src/components/atomic-crm/providers/fakerest/dataGenerator/utils.ts +28 -0
  345. package/src/components/atomic-crm/providers/fakerest/dataProvider.ts +518 -0
  346. package/src/components/atomic-crm/providers/fakerest/index.ts +2 -0
  347. package/src/components/atomic-crm/providers/fakerest/internal/listParser.ts +48 -0
  348. package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.spec.ts +721 -0
  349. package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.ts +49 -0
  350. package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.spec.ts +35 -0
  351. package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.ts +17 -0
  352. package/src/components/atomic-crm/providers/fakerest/internal/transformFilter.ts +57 -0
  353. package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.spec.ts +32 -0
  354. package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.ts +17 -0
  355. package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.spec.ts +23 -0
  356. package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.ts +17 -0
  357. package/src/components/atomic-crm/providers/supabase/authProvider.ts +121 -0
  358. package/src/components/atomic-crm/providers/supabase/dataProvider.ts +407 -0
  359. package/src/components/atomic-crm/providers/supabase/index.ts +2 -0
  360. package/src/components/atomic-crm/providers/supabase/supabase.ts +34 -0
  361. package/src/components/atomic-crm/providers/types.ts +1 -0
  362. package/src/components/atomic-crm/root/CRM.tsx +167 -0
  363. package/src/components/atomic-crm/root/ConfigurationContext.tsx +80 -0
  364. package/src/components/atomic-crm/root/defaultConfiguration.ts +64 -0
  365. package/src/components/atomic-crm/root/i18nProvider.tsx +25 -0
  366. package/src/components/atomic-crm/sales/SaleName.tsx +13 -0
  367. package/src/components/atomic-crm/sales/SalesCreate.tsx +51 -0
  368. package/src/components/atomic-crm/sales/SalesEdit.tsx +82 -0
  369. package/src/components/atomic-crm/sales/SalesInputs.tsx +31 -0
  370. package/src/components/atomic-crm/sales/SalesList.tsx +62 -0
  371. package/src/components/atomic-crm/sales/index.ts +12 -0
  372. package/src/components/atomic-crm/settings/DatabaseSettings.tsx +169 -0
  373. package/src/components/atomic-crm/settings/SettingsPage.tsx +259 -0
  374. package/src/components/atomic-crm/setup/SupabaseSetupWizard.tsx +215 -0
  375. package/src/components/atomic-crm/simple-list/ListNoResults.tsx +53 -0
  376. package/src/components/atomic-crm/simple-list/ListPlaceholder.tsx +9 -0
  377. package/src/components/atomic-crm/simple-list/SimpleList.tsx +245 -0
  378. package/src/components/atomic-crm/simple-list/SimpleListItem.tsx +138 -0
  379. package/src/components/atomic-crm/simple-list/SimpleListLoading.tsx +60 -0
  380. package/src/components/atomic-crm/tags/RoundButton.tsx +10 -0
  381. package/src/components/atomic-crm/tags/TagChip.tsx +45 -0
  382. package/src/components/atomic-crm/tags/TagCreateModal.tsx +39 -0
  383. package/src/components/atomic-crm/tags/TagDialog.tsx +118 -0
  384. package/src/components/atomic-crm/tags/TagEditModal.tsx +42 -0
  385. package/src/components/atomic-crm/tags/colors.ts +12 -0
  386. package/src/components/atomic-crm/tasks/AddTask.tsx +191 -0
  387. package/src/components/atomic-crm/tasks/Task.tsx +184 -0
  388. package/src/components/atomic-crm/tasks/TaskEdit.tsx +96 -0
  389. package/src/components/atomic-crm/tasks/TasksIterator.tsx +30 -0
  390. package/src/components/atomic-crm/types.ts +226 -0
  391. package/src/components/supabase/forgot-password-page.tsx +86 -0
  392. package/src/components/supabase/layout.tsx +27 -0
  393. package/src/components/supabase/set-password-page.tsx +119 -0
  394. package/src/components/ui/README.md +34 -0
  395. package/src/components/ui/accordion.tsx +64 -0
  396. package/src/components/ui/alert.tsx +66 -0
  397. package/src/components/ui/avatar.tsx +99 -0
  398. package/src/components/ui/badge.tsx +46 -0
  399. package/src/components/ui/breadcrumb.tsx +109 -0
  400. package/src/components/ui/button.tsx +59 -0
  401. package/src/components/ui/card.tsx +92 -0
  402. package/src/components/ui/checkbox.tsx +30 -0
  403. package/src/components/ui/command.tsx +175 -0
  404. package/src/components/ui/dialog.tsx +133 -0
  405. package/src/components/ui/drawer.tsx +133 -0
  406. package/src/components/ui/dropdown-menu.tsx +255 -0
  407. package/src/components/ui/input.tsx +21 -0
  408. package/src/components/ui/label.tsx +24 -0
  409. package/src/components/ui/navigation-menu.tsx +168 -0
  410. package/src/components/ui/pagination.tsx +127 -0
  411. package/src/components/ui/popover.tsx +46 -0
  412. package/src/components/ui/progress.tsx +29 -0
  413. package/src/components/ui/radio-group.tsx +43 -0
  414. package/src/components/ui/select.tsx +183 -0
  415. package/src/components/ui/separator.tsx +26 -0
  416. package/src/components/ui/sheet.tsx +137 -0
  417. package/src/components/ui/sidebar.tsx +724 -0
  418. package/src/components/ui/skeleton.tsx +13 -0
  419. package/src/components/ui/sonner.tsx +38 -0
  420. package/src/components/ui/spinner.tsx +51 -0
  421. package/src/components/ui/switch.tsx +29 -0
  422. package/src/components/ui/table.tsx +114 -0
  423. package/src/components/ui/tabs.tsx +64 -0
  424. package/src/components/ui/textarea.tsx +18 -0
  425. package/src/components/ui/tooltip.tsx +61 -0
  426. package/src/hooks/saved-queries.tsx +67 -0
  427. package/src/hooks/simple-form-iterator-context.tsx +70 -0
  428. package/src/hooks/use-mobile.ts +21 -0
  429. package/src/hooks/useBulkExport.tsx +61 -0
  430. package/src/hooks/useSupportCreateSuggestion.tsx +188 -0
  431. package/src/hooks/user-menu-context.tsx +24 -0
  432. package/src/index.css +170 -0
  433. package/src/lib/field.type.ts +22 -0
  434. package/src/lib/genericMemo.ts +18 -0
  435. package/src/lib/i18nProvider.ts +9 -0
  436. package/src/lib/sanitizeInputRestProps.ts +46 -0
  437. package/src/lib/supabase-config.ts +123 -0
  438. package/src/lib/utils.ts +6 -0
  439. package/src/main.tsx +10 -0
  440. package/src/setupTests.js +5 -0
  441. package/src/vite-env.d.ts +1 -0
  442. package/supabase/config.toml +157 -0
  443. package/supabase/functions/.env.development +7 -0
  444. package/supabase/functions/_shared/db.ts +187 -0
  445. package/supabase/functions/_shared/supabaseAdmin.ts +13 -0
  446. package/supabase/functions/_shared/utils.ts +13 -0
  447. package/supabase/functions/mergeContacts/index.ts +215 -0
  448. package/supabase/functions/postmark/addNoteToContact.ts +129 -0
  449. package/supabase/functions/postmark/extractMailContactData.ts +41 -0
  450. package/supabase/functions/postmark/getExpectedAuthorization.ts +4 -0
  451. package/supabase/functions/postmark/getNoteContent.ts +6 -0
  452. package/supabase/functions/postmark/index.ts +210 -0
  453. package/supabase/functions/updatePassword/index.ts +50 -0
  454. package/supabase/functions/users/index.ts +206 -0
  455. package/supabase/migrations/20240730075029_init_db.sql +600 -0
  456. package/supabase/migrations/20240730075425_init_triggers.sql +57 -0
  457. package/supabase/migrations/20240806124555_task_sales_id.sql +1 -0
  458. package/supabase/migrations/20240807082449_remove-aquisition.sql +20 -0
  459. package/supabase/migrations/20240808141826_init_state_configure.sql +9 -0
  460. package/supabase/migrations/20240813084010_tags_policy.sql +18 -0
  461. package/supabase/migrations/20241104153231_sales_policies.sql +7 -0
  462. package/supabase/migrations/20250109152531_email_jsonb.sql +43 -0
  463. package/supabase/migrations/20250113132531_phone_jsonb.sql +67 -0
  464. package/supabase/migrations/20251204172855_merge_contacts_function.sql +153 -0
  465. package/supabase/migrations/20251204201317_drop_merge_contacts_function.sql +2 -0
  466. package/supabase/seed.sql +0 -0
  467. package/supabase/templates/invite.html +70 -0
  468. package/supabase/templates/recovery.html +75 -0
@@ -0,0 +1,28 @@
1
+ import { CreateButton } from "@/components/admin/create-button";
2
+
3
+ import useAppBarHeight from "../misc/useAppBarHeight";
4
+ import { ContactImportButton } from "./ContactImportButton";
5
+
6
+ export const ContactEmpty = () => {
7
+ const appbarHeight = useAppBarHeight();
8
+ return (
9
+ <div
10
+ className="flex flex-col justify-center items-center gap-3"
11
+ style={{
12
+ height: `calc(100dvh - ${appbarHeight}px)`,
13
+ }}
14
+ >
15
+ <img src="./img/empty.svg" alt="No contacts found" />
16
+ <div className="flex flex-col gap-0 items-center">
17
+ <h6 className="text-lg font-bold">No contacts found</h6>
18
+ <p className="text-sm text-muted-foreground text-center mb-4">
19
+ It seems your contact list is empty.
20
+ </p>
21
+ </div>
22
+ <div className="flex flex-row gap-2">
23
+ <CreateButton label="New Contact" />
24
+ <ContactImportButton />
25
+ </div>
26
+ </div>
27
+ );
28
+ };
@@ -0,0 +1,213 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { MouseEvent } from "react";
3
+ import { Upload, Loader2 } from "lucide-react";
4
+ import { Form, useRefresh } from "ra-core";
5
+ import { Link } from "react-router";
6
+ import { Alert, AlertDescription } from "@/components/ui/alert";
7
+ import { Button } from "@/components/ui/button";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@/components/ui/dialog";
14
+ import { FormToolbar } from "@/components/admin/simple-form";
15
+ import { FileInput } from "@/components/admin/file-input";
16
+ import { FileField } from "@/components/admin/file-field";
17
+
18
+ import { usePapaParse } from "../misc/usePapaParse";
19
+ import type { ContactImportSchema } from "./useContactImport";
20
+ import { useContactImport } from "./useContactImport";
21
+ import * as sampleCsv from "./contacts_export.csv?raw";
22
+
23
+ export const ContactImportButton = () => {
24
+ const [modalOpen, setModalOpen] = useState(false);
25
+
26
+ const handleOpenModal = () => {
27
+ setModalOpen(true);
28
+ };
29
+
30
+ const handleCloseModal = () => {
31
+ setModalOpen(false);
32
+ };
33
+
34
+ return (
35
+ <>
36
+ <Button
37
+ variant="outline"
38
+ onClick={handleOpenModal}
39
+ className="flex items-center gap-2 cursor-pointer"
40
+ >
41
+ <Upload /> Import
42
+ </Button>
43
+ <ContactImportDialog open={modalOpen} onClose={handleCloseModal} />
44
+ </>
45
+ );
46
+ };
47
+
48
+ const SAMPLE_URL = `data:text/csv;name=crm_contacts_sample.csv;charset=utf-8,${encodeURIComponent(
49
+ sampleCsv.default,
50
+ )}`;
51
+
52
+ type ContactImportModalProps = {
53
+ open: boolean;
54
+ onClose(): void;
55
+ };
56
+
57
+ export function ContactImportDialog({
58
+ open,
59
+ onClose,
60
+ }: ContactImportModalProps) {
61
+ const refresh = useRefresh();
62
+ const processBatch = useContactImport();
63
+ const { importer, parseCsv, reset } = usePapaParse<ContactImportSchema>({
64
+ batchSize: 10,
65
+ processBatch,
66
+ });
67
+
68
+ const [file, setFile] = useState<File | null>(null);
69
+
70
+ useEffect(() => {
71
+ if (importer.state === "complete") {
72
+ refresh();
73
+ }
74
+ }, [importer.state, refresh]);
75
+
76
+ const handleFileChange = (file: File | null) => {
77
+ setFile(file);
78
+ };
79
+
80
+ const startImport = () => {
81
+ if (!file) return;
82
+ parseCsv(file);
83
+ };
84
+
85
+ const handleClose = () => {
86
+ reset();
87
+ onClose();
88
+ };
89
+
90
+ const handleReset = (e: MouseEvent<HTMLButtonElement>) => {
91
+ e.preventDefault();
92
+ reset();
93
+ };
94
+
95
+ return (
96
+ <Dialog open={open} onOpenChange={handleClose}>
97
+ <DialogContent className="max-w-2xl">
98
+ <Form className="flex flex-col gap-4">
99
+ <DialogHeader>
100
+ <DialogTitle>Import</DialogTitle>
101
+ </DialogHeader>
102
+
103
+ <div className="flex flex-col space-y-2">
104
+ {importer.state === "running" && (
105
+ <div className="flex flex-col gap-2">
106
+ <Alert>
107
+ <AlertDescription className="flex flex-row gap-4">
108
+ <Loader2 className="h-5 w-5 animate-spin" />
109
+ The import is running, please do not close this tab.
110
+ </AlertDescription>
111
+ </Alert>
112
+
113
+ <div className="text-sm">
114
+ Imported{" "}
115
+ <strong>
116
+ {importer.importCount} / {importer.rowCount}
117
+ </strong>{" "}
118
+ contacts, with <strong>{importer.errorCount}</strong> errors.
119
+ {importer.remainingTime !== null && (
120
+ <>
121
+ {" "}
122
+ Estimated remaining time:{" "}
123
+ <strong>
124
+ {millisecondsToTime(importer.remainingTime)}
125
+ </strong>
126
+ .{" "}
127
+ <button
128
+ onClick={handleReset}
129
+ className="text-red-600 underline hover:text-red-800"
130
+ >
131
+ Stop import
132
+ </button>
133
+ </>
134
+ )}
135
+ </div>
136
+ </div>
137
+ )}
138
+
139
+ {importer.state === "error" && (
140
+ <Alert variant="destructive">
141
+ <AlertDescription>
142
+ Failed to import this file, please make sure your provided a
143
+ valid CSV file.
144
+ </AlertDescription>
145
+ </Alert>
146
+ )}
147
+
148
+ {importer.state === "complete" && (
149
+ <Alert>
150
+ <AlertDescription>
151
+ Contacts import complete. Imported {importer.importCount}{" "}
152
+ contacts, with {importer.errorCount} errors
153
+ </AlertDescription>
154
+ </Alert>
155
+ )}
156
+
157
+ {importer.state === "idle" && (
158
+ <>
159
+ <Alert>
160
+ <AlertDescription className="flex flex-col gap-4">
161
+ Here is a sample CSV file you can use as a template
162
+ <Button asChild variant="outline" size="sm">
163
+ <Link
164
+ to={SAMPLE_URL}
165
+ download={"crm_contacts_sample.csv"}
166
+ >
167
+ Download CSV sample
168
+ </Link>
169
+ </Button>{" "}
170
+ </AlertDescription>
171
+ </Alert>
172
+
173
+ <FileInput
174
+ source="csv"
175
+ label="CSV File"
176
+ accept={{ "text/csv": [".csv"] }}
177
+ onChange={handleFileChange}
178
+ >
179
+ <FileField source="src" title="title" target="_blank" />
180
+ </FileInput>
181
+ </>
182
+ )}
183
+ </div>
184
+ </Form>
185
+
186
+ <div className="flex justify-start pt-6">
187
+ <FormToolbar>
188
+ {importer.state === "idle" ? (
189
+ <Button onClick={startImport} disabled={!file}>
190
+ Import
191
+ </Button>
192
+ ) : (
193
+ <Button
194
+ variant="outline"
195
+ onClick={handleClose}
196
+ disabled={importer.state === "running"}
197
+ >
198
+ Close
199
+ </Button>
200
+ )}
201
+ </FormToolbar>
202
+ </div>
203
+ </DialogContent>
204
+ </Dialog>
205
+ );
206
+ }
207
+
208
+ function millisecondsToTime(ms: number) {
209
+ const seconds = Math.floor((ms / 1000) % 60);
210
+ const minutes = Math.floor((ms / (60 * 1000)) % 60);
211
+
212
+ return `${minutes}m ${seconds}s`;
213
+ }
@@ -0,0 +1,209 @@
1
+ import { email, required } from "ra-core";
2
+ import type { FocusEvent, ClipboardEventHandler } from "react";
3
+ import { useFormContext } from "react-hook-form";
4
+ import { Separator } from "@/components/ui/separator";
5
+ import { useIsMobile } from "@/hooks/use-mobile";
6
+ import { BooleanInput } from "@/components/admin/boolean-input";
7
+ import { ReferenceInput } from "@/components/admin/reference-input";
8
+ import { TextInput } from "@/components/admin/text-input";
9
+ import { RadioButtonGroupInput } from "@/components/admin/radio-button-group-input";
10
+ import { SelectInput } from "@/components/admin/select-input";
11
+ import { ArrayInput } from "@/components/admin/array-input";
12
+ import { SimpleFormIterator } from "@/components/admin/simple-form-iterator";
13
+
14
+ import { isLinkedinUrl } from "../misc/isLinkedInUrl";
15
+ import { useConfigurationContext } from "../root/ConfigurationContext";
16
+ import type { Sale } from "../types";
17
+ import { Avatar } from "./Avatar";
18
+ import { AutocompleteCompanyInput } from "../companies/AutocompleteCompanyInput.tsx";
19
+
20
+ export const ContactInputs = () => {
21
+ const isMobile = useIsMobile();
22
+
23
+ return (
24
+ <div className="flex flex-col gap-2 p-1">
25
+ <Avatar />
26
+ <div className={`flex gap-6 ${isMobile ? "flex-col" : "flex-row"}`}>
27
+ <div className="flex flex-col gap-10 flex-1">
28
+ <ContactIdentityInputs />
29
+ <ContactPositionInputs />
30
+ </div>
31
+ <Separator
32
+ orientation={isMobile ? "horizontal" : "vertical"}
33
+ className="flex-shrink-0"
34
+ />
35
+ <div className="flex flex-col gap-10 flex-1">
36
+ <ContactPersonalInformationInputs />
37
+ <ContactMiscInputs />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ const ContactIdentityInputs = () => {
45
+ const { contactGender } = useConfigurationContext();
46
+ return (
47
+ <div className="flex flex-col gap-4">
48
+ <h6 className="text-lg font-semibold">Identity</h6>
49
+ <RadioButtonGroupInput
50
+ label={false}
51
+ row
52
+ source="gender"
53
+ choices={contactGender}
54
+ helperText={false}
55
+ optionText="label"
56
+ optionValue="value"
57
+ defaultValue={contactGender[0].value}
58
+ />
59
+ <TextInput source="first_name" validate={required()} helperText={false} />
60
+ <TextInput source="last_name" validate={required()} helperText={false} />
61
+ </div>
62
+ );
63
+ };
64
+
65
+ const ContactPositionInputs = () => {
66
+ return (
67
+ <div className="flex flex-col gap-4">
68
+ <h6 className="text-lg font-semibold">Position</h6>
69
+ <TextInput source="title" helperText={false} />
70
+ <ReferenceInput source="company_id" reference="companies" perPage={10}>
71
+ <AutocompleteCompanyInput />
72
+ </ReferenceInput>
73
+ </div>
74
+ );
75
+ };
76
+
77
+ const ContactPersonalInformationInputs = () => {
78
+ const { getValues, setValue } = useFormContext();
79
+
80
+ // set first and last name based on email
81
+ const handleEmailChange = (email: string) => {
82
+ const { first_name, last_name } = getValues();
83
+ if (first_name || last_name || !email) return;
84
+ const [first, last] = email.split("@")[0].split(".");
85
+ setValue("first_name", first.charAt(0).toUpperCase() + first.slice(1));
86
+ setValue(
87
+ "last_name",
88
+ last ? last.charAt(0).toUpperCase() + last.slice(1) : "",
89
+ );
90
+ };
91
+
92
+ const handleEmailPaste: ClipboardEventHandler<
93
+ HTMLTextAreaElement | HTMLInputElement
94
+ > = (e) => {
95
+ const email = e.clipboardData?.getData("text/plain");
96
+ handleEmailChange(email);
97
+ };
98
+
99
+ const handleEmailBlur = (
100
+ e: FocusEvent<HTMLTextAreaElement | HTMLInputElement>,
101
+ ) => {
102
+ const email = e.target.value;
103
+ handleEmailChange(email);
104
+ };
105
+
106
+ return (
107
+ <div className="flex flex-col gap-4">
108
+ <h6 className="text-lg font-semibold">Personal info</h6>
109
+ <ArrayInput
110
+ source="email_jsonb"
111
+ label="Email addresses"
112
+ helperText={false}
113
+ >
114
+ <SimpleFormIterator
115
+ inline
116
+ disableReordering
117
+ disableClear
118
+ className="[&>ul>li]:border-b-0 [&>ul>li]:pb-0"
119
+ >
120
+ <TextInput
121
+ source="email"
122
+ className="w-full"
123
+ helperText={false}
124
+ label={false}
125
+ placeholder="Email"
126
+ validate={email()}
127
+ onPaste={handleEmailPaste}
128
+ onBlur={handleEmailBlur}
129
+ />
130
+ <SelectInput
131
+ source="type"
132
+ helperText={false}
133
+ label={false}
134
+ optionText="id"
135
+ choices={personalInfoTypes}
136
+ defaultValue="Work"
137
+ className="w-24 min-w-24"
138
+ />
139
+ </SimpleFormIterator>
140
+ </ArrayInput>
141
+ <ArrayInput source="phone_jsonb" label="Phone numbers" helperText={false}>
142
+ <SimpleFormIterator
143
+ inline
144
+ disableReordering
145
+ disableClear
146
+ className="[&>ul>li]:border-b-0 [&>ul>li]:pb-0"
147
+ >
148
+ <TextInput
149
+ source="number"
150
+ className="w-full"
151
+ helperText={false}
152
+ label={false}
153
+ placeholder="Phone number"
154
+ />
155
+ <SelectInput
156
+ source="type"
157
+ helperText={false}
158
+ label={false}
159
+ optionText="id"
160
+ choices={personalInfoTypes}
161
+ defaultValue="Work"
162
+ className="w-24 min-w-24"
163
+ />
164
+ </SimpleFormIterator>
165
+ </ArrayInput>
166
+ <TextInput
167
+ source="linkedin_url"
168
+ label="Linkedin URL"
169
+ helperText={false}
170
+ validate={isLinkedinUrl}
171
+ />
172
+ </div>
173
+ );
174
+ };
175
+
176
+ const personalInfoTypes = [{ id: "Work" }, { id: "Home" }, { id: "Other" }];
177
+
178
+ const ContactMiscInputs = () => {
179
+ return (
180
+ <div className="flex flex-col gap-4">
181
+ <h6 className="text-lg font-semibold">Misc</h6>
182
+ <TextInput
183
+ source="background"
184
+ label="Background info (bio, how you met, etc)"
185
+ multiline
186
+ helperText={false}
187
+ />
188
+ <BooleanInput source="has_newsletter" helperText={false} />
189
+ <ReferenceInput
190
+ reference="sales"
191
+ source="sales_id"
192
+ sort={{ field: "last_name", order: "ASC" }}
193
+ filter={{
194
+ "disabled@neq": true,
195
+ }}
196
+ >
197
+ <SelectInput
198
+ helperText={false}
199
+ label="Account manager"
200
+ optionText={saleOptionRenderer}
201
+ validate={required()}
202
+ />
203
+ </ReferenceInput>
204
+ </div>
205
+ );
206
+ };
207
+
208
+ const saleOptionRenderer = (choice: Sale) =>
209
+ `${choice.first_name} ${choice.last_name}`;
@@ -0,0 +1,116 @@
1
+ import jsonExport from "jsonexport/dist";
2
+ import {
3
+ downloadCSV,
4
+ useGetIdentity,
5
+ useListContext,
6
+ type Exporter,
7
+ } from "ra-core";
8
+ import { BulkActionsToolbar } from "@/components/admin/bulk-actions-toolbar";
9
+ import { CreateButton } from "@/components/admin/create-button";
10
+ import { ExportButton } from "@/components/admin/export-button";
11
+ import { List } from "@/components/admin/list";
12
+ import { SortButton } from "@/components/admin/sort-button";
13
+ import { Card } from "@/components/ui/card";
14
+
15
+ import type { Company, Contact, Sale, Tag } from "../types";
16
+ import { ContactEmpty } from "./ContactEmpty";
17
+ import { ContactImportButton } from "./ContactImportButton";
18
+ import { ContactListContent } from "./ContactListContent";
19
+ import { ContactListFilter } from "./ContactListFilter";
20
+ import { TopToolbar } from "../layout/TopToolbar";
21
+
22
+ export const ContactList = () => {
23
+ const { identity } = useGetIdentity();
24
+
25
+ if (!identity) return null;
26
+
27
+ return (
28
+ <List
29
+ title={false}
30
+ actions={<ContactListActions />}
31
+ perPage={25}
32
+ sort={{ field: "last_seen", order: "DESC" }}
33
+ exporter={exporter}
34
+ >
35
+ <ContactListLayout />
36
+ </List>
37
+ );
38
+ };
39
+
40
+ const ContactListLayout = () => {
41
+ const { data, isPending, filterValues } = useListContext();
42
+ const { identity } = useGetIdentity();
43
+
44
+ const hasFilters = filterValues && Object.keys(filterValues).length > 0;
45
+
46
+ if (!identity || isPending) return null;
47
+
48
+ if (!data?.length && !hasFilters) return <ContactEmpty />;
49
+
50
+ return (
51
+ <div className="flex flex-row gap-8">
52
+ <ContactListFilter />
53
+ <div className="w-full flex flex-col gap-4">
54
+ <Card className="py-0">
55
+ <ContactListContent />
56
+ </Card>
57
+ </div>
58
+ <BulkActionsToolbar />
59
+ </div>
60
+ );
61
+ };
62
+
63
+ const ContactListActions = () => (
64
+ <TopToolbar>
65
+ <SortButton fields={["first_name", "last_name", "last_seen"]} />
66
+ <ContactImportButton />
67
+ <ExportButton exporter={exporter} />
68
+ <CreateButton />
69
+ </TopToolbar>
70
+ );
71
+
72
+ const exporter: Exporter<Contact> = async (records, fetchRelatedRecords) => {
73
+ const companies = await fetchRelatedRecords<Company>(
74
+ records,
75
+ "company_id",
76
+ "companies",
77
+ );
78
+ const sales = await fetchRelatedRecords<Sale>(records, "sales_id", "sales");
79
+ const tags = await fetchRelatedRecords<Tag>(records, "tags", "tags");
80
+
81
+ const contacts = records.map((contact) => {
82
+ const exportedContact = {
83
+ ...contact,
84
+ company:
85
+ contact.company_id != null
86
+ ? companies[contact.company_id].name
87
+ : undefined,
88
+ sales: `${sales[contact.sales_id].first_name} ${
89
+ sales[contact.sales_id].last_name
90
+ }`,
91
+ tags: contact.tags.map((tagId) => tags[tagId].name).join(", "),
92
+ email_work: contact.email_jsonb?.find((email) => email.type === "Work")
93
+ ?.email,
94
+ email_home: contact.email_jsonb?.find((email) => email.type === "Home")
95
+ ?.email,
96
+ email_other: contact.email_jsonb?.find((email) => email.type === "Other")
97
+ ?.email,
98
+ email_jsonb: JSON.stringify(contact.email_jsonb),
99
+ email_fts: undefined,
100
+ phone_work: contact.phone_jsonb?.find((phone) => phone.type === "Work")
101
+ ?.number,
102
+ phone_home: contact.phone_jsonb?.find((phone) => phone.type === "Home")
103
+ ?.number,
104
+ phone_other: contact.phone_jsonb?.find((phone) => phone.type === "Other")
105
+ ?.number,
106
+ phone_jsonb: JSON.stringify(contact.phone_jsonb),
107
+ phone_fts: undefined,
108
+ };
109
+ delete exportedContact.email_fts;
110
+ delete exportedContact.phone_fts;
111
+ return exportedContact;
112
+ });
113
+ return jsonExport(contacts, {}, (_err: any, csv: string) => {
114
+ downloadCSV(csv, "contacts");
115
+ });
116
+ };
@@ -0,0 +1,107 @@
1
+ import { formatRelative } from "date-fns";
2
+ import { RecordContextProvider, useListContext } from "ra-core";
3
+ import { type MouseEvent, useCallback } from "react";
4
+ import { Link } from "react-router";
5
+ import { ReferenceField } from "@/components/admin/reference-field";
6
+ import { TextField } from "@/components/admin/text-field";
7
+ import { Checkbox } from "@/components/ui/checkbox";
8
+ import { Skeleton } from "@/components/ui/skeleton";
9
+ import { useIsMobile } from "@/hooks/use-mobile";
10
+
11
+ import { Status } from "../misc/Status";
12
+ import type { Contact } from "../types";
13
+ import { Avatar } from "./Avatar";
14
+ import { TagsList } from "./TagsList";
15
+
16
+ export const ContactListContent = () => {
17
+ const {
18
+ data: contacts,
19
+ error,
20
+ isPending,
21
+ onToggleItem,
22
+ selectedIds,
23
+ } = useListContext<Contact>();
24
+ const isSmall = useIsMobile();
25
+
26
+ // StopPropagation does not work for some reason on Checkbox, this handler is a workaround
27
+ const handleLinkClick = useCallback(function handleLinkClick(
28
+ e: MouseEvent<HTMLAnchorElement>,
29
+ ) {
30
+ if (e.target instanceof HTMLButtonElement) {
31
+ e.preventDefault();
32
+ }
33
+ }, []);
34
+
35
+ if (isPending) {
36
+ return <Skeleton className="w-full h-9" />;
37
+ }
38
+
39
+ if (error) {
40
+ return null;
41
+ }
42
+ const now = Date.now();
43
+
44
+ return (
45
+ <div className="divide-y">
46
+ {contacts.map((contact) => (
47
+ <RecordContextProvider key={contact.id} value={contact}>
48
+ <Link
49
+ to={`/contacts/${contact.id}/show`}
50
+ className="flex flex-row gap-4 items-center px-4 py-2 hover:bg-muted transition-colors first:rounded-t-xl last:rounded-b-xl"
51
+ onClick={handleLinkClick}
52
+ >
53
+ <Checkbox
54
+ className="cursor-pointer"
55
+ checked={selectedIds.includes(contact.id)}
56
+ onCheckedChange={() => onToggleItem(contact.id)}
57
+ />
58
+ <Avatar />
59
+ <div className="flex-1 min-w-0">
60
+ <div className="font-medium">
61
+ {`${contact.first_name} ${contact.last_name ?? ""}`}
62
+ </div>
63
+ <div className="text-sm text-muted-foreground">
64
+ {contact.title}
65
+ {contact.title && contact.company_id != null && " at "}
66
+ {contact.company_id != null && (
67
+ <ReferenceField
68
+ source="company_id"
69
+ reference="companies"
70
+ link={false}
71
+ >
72
+ <TextField source="name" />
73
+ </ReferenceField>
74
+ )}
75
+ {contact.nb_tasks
76
+ ? ` - ${contact.nb_tasks} task${
77
+ contact.nb_tasks > 1 ? "s" : ""
78
+ }`
79
+ : ""}
80
+ &nbsp;&nbsp;
81
+ <TagsList />
82
+ </div>
83
+ </div>
84
+ {contact.last_seen && (
85
+ <div className="text-right ml-4">
86
+ <div
87
+ className="text-sm text-muted-foreground"
88
+ title={contact.last_seen}
89
+ >
90
+ {!isSmall && "last activity "}
91
+ {formatRelative(contact.last_seen, now)}{" "}
92
+ <Status status={contact.status} />
93
+ </div>
94
+ </div>
95
+ )}
96
+ </Link>
97
+ </RecordContextProvider>
98
+ ))}
99
+
100
+ {contacts.length === 0 && (
101
+ <div className="p-4">
102
+ <div className="text-muted-foreground">No contacts found</div>
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ };