@kelpie/ui 0.7.0 → 0.9.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 (349) hide show
  1. package/dist/api/resource.d.ts +59 -6
  2. package/dist/api/resource.d.ts.map +1 -1
  3. package/dist/api/resource.js +106 -24
  4. package/dist/api/resource.js.map +1 -1
  5. package/dist/api/resources/companies.d.ts +2 -0
  6. package/dist/api/resources/companies.d.ts.map +1 -1
  7. package/dist/api/resources/companies.js +5 -1
  8. package/dist/api/resources/companies.js.map +1 -1
  9. package/dist/api/resources/consentPurposes.d.ts +13 -0
  10. package/dist/api/resources/consentPurposes.d.ts.map +1 -0
  11. package/dist/api/resources/consentPurposes.js +39 -0
  12. package/dist/api/resources/consentPurposes.js.map +1 -0
  13. package/dist/api/resources/customFields.d.ts +15 -0
  14. package/dist/api/resources/customFields.d.ts.map +1 -0
  15. package/dist/api/resources/customFields.js +41 -0
  16. package/dist/api/resources/customFields.js.map +1 -0
  17. package/dist/api/resources/decisions.d.ts +2 -0
  18. package/dist/api/resources/decisions.d.ts.map +1 -1
  19. package/dist/api/resources/decisions.js +1 -0
  20. package/dist/api/resources/decisions.js.map +1 -1
  21. package/dist/api/resources/emailDomainLinker.d.ts +16 -0
  22. package/dist/api/resources/emailDomainLinker.d.ts.map +1 -0
  23. package/dist/api/resources/emailDomainLinker.js +16 -0
  24. package/dist/api/resources/emailDomainLinker.js.map +1 -0
  25. package/dist/api/resources/enquiries.d.ts +39 -0
  26. package/dist/api/resources/enquiries.d.ts.map +1 -0
  27. package/dist/api/resources/enquiries.js +68 -0
  28. package/dist/api/resources/enquiries.js.map +1 -0
  29. package/dist/api/resources/forms.d.ts +21 -1
  30. package/dist/api/resources/forms.d.ts.map +1 -1
  31. package/dist/api/resources/forms.js +44 -17
  32. package/dist/api/resources/forms.js.map +1 -1
  33. package/dist/api/resources/handbookPages.d.ts +1 -1
  34. package/dist/api/resources/handbookPages.js +1 -1
  35. package/dist/api/resources/importJobs.d.ts +5 -0
  36. package/dist/api/resources/importJobs.d.ts.map +1 -1
  37. package/dist/api/resources/importJobs.js +3 -0
  38. package/dist/api/resources/importJobs.js.map +1 -1
  39. package/dist/api/resources/listMembers.d.ts +27 -0
  40. package/dist/api/resources/listMembers.d.ts.map +1 -0
  41. package/dist/api/resources/listMembers.js +108 -0
  42. package/dist/api/resources/listMembers.js.map +1 -0
  43. package/dist/api/resources/lists.d.ts +14 -0
  44. package/dist/api/resources/lists.d.ts.map +1 -0
  45. package/dist/api/resources/lists.js +34 -0
  46. package/dist/api/resources/lists.js.map +1 -0
  47. package/dist/api/resources/opportunities.d.ts +2 -0
  48. package/dist/api/resources/opportunities.d.ts.map +1 -1
  49. package/dist/api/resources/opportunities.js +1 -0
  50. package/dist/api/resources/opportunities.js.map +1 -1
  51. package/dist/api/resources/people.d.ts.map +1 -1
  52. package/dist/api/resources/people.js +4 -1
  53. package/dist/api/resources/people.js.map +1 -1
  54. package/dist/api/resources/roles.d.ts +2 -0
  55. package/dist/api/resources/roles.d.ts.map +1 -1
  56. package/dist/api/resources/roles.js +1 -1
  57. package/dist/api/resources/roles.js.map +1 -1
  58. package/dist/api/resources/sampleData.d.ts +17 -0
  59. package/dist/api/resources/sampleData.d.ts.map +1 -0
  60. package/dist/api/resources/sampleData.js +31 -0
  61. package/dist/api/resources/sampleData.js.map +1 -0
  62. package/dist/api/resources/webhooks.d.ts.map +1 -1
  63. package/dist/api/resources/webhooks.js +7 -22
  64. package/dist/api/resources/webhooks.js.map +1 -1
  65. package/dist/app/KelpieApp.d.ts.map +1 -1
  66. package/dist/app/KelpieApp.js +9 -1
  67. package/dist/app/KelpieApp.js.map +1 -1
  68. package/dist/components/ActivitiesPanel.d.ts.map +1 -1
  69. package/dist/components/ActivitiesPanel.js +3 -1
  70. package/dist/components/ActivitiesPanel.js.map +1 -1
  71. package/dist/components/ColumnPicker.d.ts +24 -0
  72. package/dist/components/ColumnPicker.d.ts.map +1 -0
  73. package/dist/components/ColumnPicker.js +56 -0
  74. package/dist/components/ColumnPicker.js.map +1 -0
  75. package/dist/components/CustomFieldsPanel.d.ts +23 -0
  76. package/dist/components/CustomFieldsPanel.d.ts.map +1 -0
  77. package/dist/components/CustomFieldsPanel.js +116 -0
  78. package/dist/components/CustomFieldsPanel.js.map +1 -0
  79. package/dist/components/DataTable.d.ts +16 -1
  80. package/dist/components/DataTable.d.ts.map +1 -1
  81. package/dist/components/DataTable.js +51 -10
  82. package/dist/components/DataTable.js.map +1 -1
  83. package/dist/components/DecisionsPanel.d.ts.map +1 -1
  84. package/dist/components/DecisionsPanel.js +4 -3
  85. package/dist/components/DecisionsPanel.js.map +1 -1
  86. package/dist/components/EntitySearch.d.ts.map +1 -1
  87. package/dist/components/EntitySearch.js +11 -5
  88. package/dist/components/EntitySearch.js.map +1 -1
  89. package/dist/components/FormsPanel.d.ts +18 -0
  90. package/dist/components/FormsPanel.d.ts.map +1 -0
  91. package/dist/components/FormsPanel.js +37 -0
  92. package/dist/components/FormsPanel.js.map +1 -0
  93. package/dist/components/ListsPanel.d.ts +14 -0
  94. package/dist/components/ListsPanel.d.ts.map +1 -0
  95. package/dist/components/ListsPanel.js +56 -0
  96. package/dist/components/ListsPanel.js.map +1 -0
  97. package/dist/components/NotesPanel.d.ts.map +1 -1
  98. package/dist/components/NotesPanel.js +6 -5
  99. package/dist/components/NotesPanel.js.map +1 -1
  100. package/dist/components/Paginator.d.ts +26 -0
  101. package/dist/components/Paginator.d.ts.map +1 -0
  102. package/dist/components/Paginator.js +16 -0
  103. package/dist/components/Paginator.js.map +1 -0
  104. package/dist/components/PlanAttention.d.ts +3 -2
  105. package/dist/components/PlanAttention.d.ts.map +1 -1
  106. package/dist/components/PlanAttention.js +7 -3
  107. package/dist/components/PlanAttention.js.map +1 -1
  108. package/dist/components/PlanPanel.d.ts.map +1 -1
  109. package/dist/components/PlanPanel.js +2 -1
  110. package/dist/components/PlanPanel.js.map +1 -1
  111. package/dist/components/Shell.d.ts.map +1 -1
  112. package/dist/components/Shell.js +4 -0
  113. package/dist/components/Shell.js.map +1 -1
  114. package/dist/components/useHasCustomFields.d.ts +13 -0
  115. package/dist/components/useHasCustomFields.d.ts.map +1 -0
  116. package/dist/components/useHasCustomFields.js +16 -0
  117. package/dist/components/useHasCustomFields.js.map +1 -0
  118. package/dist/index.d.ts +8 -2
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +4 -1
  121. package/dist/index.js.map +1 -1
  122. package/dist/lib/listView.d.ts +15 -0
  123. package/dist/lib/listView.d.ts.map +1 -0
  124. package/dist/lib/listView.js +66 -0
  125. package/dist/lib/listView.js.map +1 -0
  126. package/dist/lib/sort.d.ts +43 -0
  127. package/dist/lib/sort.d.ts.map +1 -0
  128. package/dist/lib/sort.js +102 -0
  129. package/dist/lib/sort.js.map +1 -0
  130. package/dist/pages/CompaniesPage.d.ts.map +1 -1
  131. package/dist/pages/CompaniesPage.js +88 -3
  132. package/dist/pages/CompaniesPage.js.map +1 -1
  133. package/dist/pages/CompanyDetail.d.ts.map +1 -1
  134. package/dist/pages/CompanyDetail.js +40 -15
  135. package/dist/pages/CompanyDetail.js.map +1 -1
  136. package/dist/pages/DealDetail.d.ts.map +1 -1
  137. package/dist/pages/DealDetail.js +20 -1
  138. package/dist/pages/DealDetail.js.map +1 -1
  139. package/dist/pages/DealsPage.d.ts.map +1 -1
  140. package/dist/pages/DealsPage.js +134 -14
  141. package/dist/pages/DealsPage.js.map +1 -1
  142. package/dist/pages/DecisionsPage.d.ts.map +1 -1
  143. package/dist/pages/DecisionsPage.js +52 -8
  144. package/dist/pages/DecisionsPage.js.map +1 -1
  145. package/dist/pages/EnquiriesPage.d.ts +2 -0
  146. package/dist/pages/EnquiriesPage.d.ts.map +1 -0
  147. package/dist/pages/EnquiriesPage.js +251 -0
  148. package/dist/pages/EnquiriesPage.js.map +1 -0
  149. package/dist/pages/EnquiryDetail.d.ts +2 -0
  150. package/dist/pages/EnquiryDetail.d.ts.map +1 -0
  151. package/dist/pages/EnquiryDetail.js +207 -0
  152. package/dist/pages/EnquiryDetail.js.map +1 -0
  153. package/dist/pages/EnquiryStageSettingsPage.d.ts +3 -0
  154. package/dist/pages/EnquiryStageSettingsPage.d.ts.map +1 -0
  155. package/dist/pages/EnquiryStageSettingsPage.js +34 -0
  156. package/dist/pages/EnquiryStageSettingsPage.js.map +1 -0
  157. package/dist/pages/FormDetail.d.ts.map +1 -1
  158. package/dist/pages/FormDetail.js +19 -1
  159. package/dist/pages/FormDetail.js.map +1 -1
  160. package/dist/pages/FormsPage.d.ts.map +1 -1
  161. package/dist/pages/FormsPage.js +53 -2
  162. package/dist/pages/FormsPage.js.map +1 -1
  163. package/dist/pages/FundraisingPage.d.ts.map +1 -1
  164. package/dist/pages/FundraisingPage.js +116 -14
  165. package/dist/pages/FundraisingPage.js.map +1 -1
  166. package/dist/pages/HiringPage.d.ts.map +1 -1
  167. package/dist/pages/HiringPage.js +27 -4
  168. package/dist/pages/HiringPage.js.map +1 -1
  169. package/dist/pages/ListDetail.d.ts +11 -0
  170. package/dist/pages/ListDetail.d.ts.map +1 -0
  171. package/dist/pages/ListDetail.js +214 -0
  172. package/dist/pages/ListDetail.js.map +1 -0
  173. package/dist/pages/ListsPage.d.ts +9 -0
  174. package/dist/pages/ListsPage.d.ts.map +1 -0
  175. package/dist/pages/ListsPage.js +125 -0
  176. package/dist/pages/ListsPage.js.map +1 -0
  177. package/dist/pages/OpportunitiesPage.d.ts.map +1 -1
  178. package/dist/pages/OpportunitiesPage.js +97 -14
  179. package/dist/pages/OpportunitiesPage.js.map +1 -1
  180. package/dist/pages/OpportunityDetail.d.ts.map +1 -1
  181. package/dist/pages/OpportunityDetail.js +64 -3
  182. package/dist/pages/OpportunityDetail.js.map +1 -1
  183. package/dist/pages/PartnershipDetail.d.ts.map +1 -1
  184. package/dist/pages/PartnershipDetail.js +19 -1
  185. package/dist/pages/PartnershipDetail.js.map +1 -1
  186. package/dist/pages/PartnershipsPage.d.ts.map +1 -1
  187. package/dist/pages/PartnershipsPage.js +111 -14
  188. package/dist/pages/PartnershipsPage.js.map +1 -1
  189. package/dist/pages/PeoplePage.d.ts.map +1 -1
  190. package/dist/pages/PeoplePage.js +119 -3
  191. package/dist/pages/PeoplePage.js.map +1 -1
  192. package/dist/pages/PersonDetail.d.ts.map +1 -1
  193. package/dist/pages/PersonDetail.js +67 -5
  194. package/dist/pages/PersonDetail.js.map +1 -1
  195. package/dist/pages/PlanningPage.d.ts.map +1 -1
  196. package/dist/pages/PlanningPage.js +88 -17
  197. package/dist/pages/PlanningPage.js.map +1 -1
  198. package/dist/pages/RaiseDetail.d.ts.map +1 -1
  199. package/dist/pages/RaiseDetail.js +13 -1
  200. package/dist/pages/RaiseDetail.js.map +1 -1
  201. package/dist/pages/RoleDetail.d.ts.map +1 -1
  202. package/dist/pages/RoleDetail.js +5 -4
  203. package/dist/pages/RoleDetail.js.map +1 -1
  204. package/dist/pages/SearchPage.d.ts.map +1 -1
  205. package/dist/pages/SearchPage.js +1 -0
  206. package/dist/pages/SearchPage.js.map +1 -1
  207. package/dist/pages/admin/DataPage.d.ts.map +1 -1
  208. package/dist/pages/admin/DataPage.js +126 -13
  209. package/dist/pages/admin/DataPage.js.map +1 -1
  210. package/dist/pages/admin/FieldsPage.d.ts +2 -0
  211. package/dist/pages/admin/FieldsPage.d.ts.map +1 -0
  212. package/dist/pages/admin/FieldsPage.js +164 -0
  213. package/dist/pages/admin/FieldsPage.js.map +1 -0
  214. package/dist/pages/admin/PrivacyPage.d.ts +9 -0
  215. package/dist/pages/admin/PrivacyPage.d.ts.map +1 -0
  216. package/dist/pages/admin/PrivacyPage.js +170 -0
  217. package/dist/pages/admin/PrivacyPage.js.map +1 -0
  218. package/dist/pages/admin/WebhookDeliveries.d.ts.map +1 -1
  219. package/dist/pages/admin/WebhookDeliveries.js +3 -2
  220. package/dist/pages/admin/WebhookDeliveries.js.map +1 -1
  221. package/dist/pages/admin/WebhooksPage.d.ts.map +1 -1
  222. package/dist/pages/admin/WebhooksPage.js +3 -2
  223. package/dist/pages/admin/WebhooksPage.js.map +1 -1
  224. package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
  225. package/dist/pages/admin/WorkspaceSettingsPage.js +45 -2
  226. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  227. package/dist/pages/dashboard/attention.d.ts.map +1 -1
  228. package/dist/pages/dashboard/attention.js +3 -0
  229. package/dist/pages/dashboard/attention.js.map +1 -1
  230. package/dist/pages/forms/AddFieldMenu.d.ts +17 -0
  231. package/dist/pages/forms/AddFieldMenu.d.ts.map +1 -0
  232. package/dist/pages/forms/AddFieldMenu.js +43 -0
  233. package/dist/pages/forms/AddFieldMenu.js.map +1 -0
  234. package/dist/pages/forms/EmbedPanel.d.ts +4 -0
  235. package/dist/pages/forms/EmbedPanel.d.ts.map +1 -1
  236. package/dist/pages/forms/EmbedPanel.js +62 -3
  237. package/dist/pages/forms/EmbedPanel.js.map +1 -1
  238. package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
  239. package/dist/pages/forms/FieldsEditor.js +179 -36
  240. package/dist/pages/forms/FieldsEditor.js.map +1 -1
  241. package/dist/pages/forms/FormActions.d.ts +6 -0
  242. package/dist/pages/forms/FormActions.d.ts.map +1 -0
  243. package/dist/pages/forms/FormActions.js +149 -0
  244. package/dist/pages/forms/FormActions.js.map +1 -0
  245. package/dist/pages/forms/FormSettings.d.ts.map +1 -1
  246. package/dist/pages/forms/FormSettings.js +15 -19
  247. package/dist/pages/forms/FormSettings.js.map +1 -1
  248. package/dist/pages/forms/SubmissionDetail.d.ts +2 -0
  249. package/dist/pages/forms/SubmissionDetail.d.ts.map +1 -0
  250. package/dist/pages/forms/SubmissionDetail.js +76 -0
  251. package/dist/pages/forms/SubmissionDetail.js.map +1 -0
  252. package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
  253. package/dist/pages/forms/SubmissionsTable.js +57 -8
  254. package/dist/pages/forms/SubmissionsTable.js.map +1 -1
  255. package/dist/pages/forms/fieldList.d.ts +11 -5
  256. package/dist/pages/forms/fieldList.d.ts.map +1 -1
  257. package/dist/pages/forms/fieldList.js +66 -6
  258. package/dist/pages/forms/fieldList.js.map +1 -1
  259. package/dist/pages/forms/template.d.ts +17 -0
  260. package/dist/pages/forms/template.d.ts.map +1 -1
  261. package/dist/pages/forms/template.js +93 -0
  262. package/dist/pages/forms/template.js.map +1 -1
  263. package/dist/pages/handbook/HandbookPage.js +1 -1
  264. package/dist/pages/hiringDirectory.js +2 -2
  265. package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
  266. package/dist/pages/onboarding/WorkspaceStepPage.js +15 -2
  267. package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
  268. package/dist/pages/positionDirectory.js +2 -2
  269. package/package.json +2 -2
  270. package/src/api/resource.ts +155 -32
  271. package/src/api/resources/companies.ts +7 -1
  272. package/src/api/resources/consentPurposes.ts +83 -0
  273. package/src/api/resources/customFields.ts +88 -0
  274. package/src/api/resources/decisions.ts +3 -0
  275. package/src/api/resources/emailDomainLinker.ts +43 -0
  276. package/src/api/resources/enquiries.ts +118 -0
  277. package/src/api/resources/forms.ts +59 -19
  278. package/src/api/resources/handbookPages.ts +1 -1
  279. package/src/api/resources/importJobs.ts +9 -0
  280. package/src/api/resources/listMembers.ts +173 -0
  281. package/src/api/resources/lists.ts +61 -0
  282. package/src/api/resources/opportunities.ts +3 -0
  283. package/src/api/resources/people.ts +4 -1
  284. package/src/api/resources/roles.ts +3 -1
  285. package/src/api/resources/sampleData.ts +57 -0
  286. package/src/api/resources/webhooks.ts +8 -24
  287. package/src/app/KelpieApp.tsx +16 -0
  288. package/src/components/ActivitiesPanel.tsx +3 -10
  289. package/src/components/ColumnPicker.tsx +124 -0
  290. package/src/components/CustomFieldsPanel.tsx +330 -0
  291. package/src/components/DataTable.tsx +89 -10
  292. package/src/components/DecisionsPanel.tsx +4 -12
  293. package/src/components/EntitySearch.tsx +12 -5
  294. package/src/components/FormsPanel.tsx +86 -0
  295. package/src/components/ListsPanel.tsx +176 -0
  296. package/src/components/NotesPanel.tsx +6 -14
  297. package/src/components/Paginator.tsx +78 -0
  298. package/src/components/PlanAttention.tsx +20 -3
  299. package/src/components/PlanPanel.tsx +2 -10
  300. package/src/components/Shell.tsx +4 -0
  301. package/src/components/useHasCustomFields.ts +18 -0
  302. package/src/index.ts +10 -1
  303. package/src/lib/listView.ts +149 -0
  304. package/src/lib/sort.ts +143 -0
  305. package/src/pages/CompaniesPage.tsx +125 -11
  306. package/src/pages/CompanyDetail.tsx +98 -63
  307. package/src/pages/DealDetail.tsx +34 -0
  308. package/src/pages/DealsPage.tsx +178 -24
  309. package/src/pages/DecisionsPage.tsx +75 -18
  310. package/src/pages/EnquiriesPage.tsx +434 -0
  311. package/src/pages/EnquiryDetail.tsx +554 -0
  312. package/src/pages/EnquiryStageSettingsPage.tsx +63 -0
  313. package/src/pages/FormDetail.tsx +23 -1
  314. package/src/pages/FormsPage.tsx +81 -17
  315. package/src/pages/FundraisingPage.tsx +152 -24
  316. package/src/pages/HiringPage.tsx +39 -11
  317. package/src/pages/ListDetail.tsx +470 -0
  318. package/src/pages/ListsPage.tsx +281 -0
  319. package/src/pages/OpportunitiesPage.tsx +130 -24
  320. package/src/pages/OpportunityDetail.tsx +168 -2
  321. package/src/pages/PartnershipDetail.tsx +35 -0
  322. package/src/pages/PartnershipsPage.tsx +149 -24
  323. package/src/pages/PeoplePage.tsx +157 -11
  324. package/src/pages/PersonDetail.tsx +157 -6
  325. package/src/pages/PlanningPage.tsx +259 -29
  326. package/src/pages/RaiseDetail.tsx +24 -0
  327. package/src/pages/RoleDetail.tsx +21 -26
  328. package/src/pages/SearchPage.tsx +1 -0
  329. package/src/pages/admin/DataPage.tsx +324 -13
  330. package/src/pages/admin/FieldsPage.tsx +455 -0
  331. package/src/pages/admin/PrivacyPage.tsx +461 -0
  332. package/src/pages/admin/WebhookDeliveries.tsx +10 -15
  333. package/src/pages/admin/WebhooksPage.tsx +14 -19
  334. package/src/pages/admin/WorkspaceSettingsPage.tsx +134 -2
  335. package/src/pages/dashboard/attention.ts +3 -0
  336. package/src/pages/forms/AddFieldMenu.tsx +121 -0
  337. package/src/pages/forms/EmbedPanel.tsx +163 -2
  338. package/src/pages/forms/FieldsEditor.tsx +539 -165
  339. package/src/pages/forms/FormActions.tsx +552 -0
  340. package/src/pages/forms/FormSettings.tsx +29 -70
  341. package/src/pages/forms/SubmissionDetail.tsx +249 -0
  342. package/src/pages/forms/SubmissionsTable.tsx +112 -26
  343. package/src/pages/forms/fieldList.ts +68 -5
  344. package/src/pages/forms/template.ts +103 -0
  345. package/src/pages/handbook/HandbookPage.tsx +1 -1
  346. package/src/pages/hiringDirectory.ts +2 -2
  347. package/src/pages/onboarding/WorkspaceStepPage.tsx +31 -3
  348. package/src/pages/positionDirectory.ts +2 -2
  349. package/src/styles.css +3 -3
@@ -2,14 +2,41 @@ import type { Person } from '@kelpie/schemas'
2
2
  import { useState } from 'react'
3
3
  import { useNavigate } from 'react-router'
4
4
 
5
+ import { useTimezone } from '../api/resources/account.ts'
5
6
  import { useCreatePerson, usePeople } from '../api/resources/people.ts'
6
7
  import { Chip } from '../components/Chip.tsx'
8
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
7
9
  import { DataTable } from '../components/DataTable.tsx'
8
10
  import type { Column } from '../components/DataTable.tsx'
9
11
  import { FilterBar, PageHeader } from '../components/PageHeader.tsx'
12
+ import { Paginator } from '../components/Paginator.tsx'
10
13
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
14
+ import { formatDate } from '../lib/dates.ts'
15
+ import { useListView } from '../lib/listView.ts'
16
+ import { serverSortOnly } from '../lib/sort.ts'
11
17
  import { usePeopleDirectory } from './positionDirectory.ts'
12
18
 
19
+ /**
20
+ * Column catalog for People. Every field the wire schema carries appears here,
21
+ * plus the two joined-in columns (position title, company names) computed from
22
+ * the position directory. `sortKey` names the server field; `getSortValue`
23
+ * enables the DataTable's page-local sort for columns the server does not
24
+ * index. Everything not in `DEFAULT_VISIBLE_KEYS` starts hidden and comes back
25
+ * through the Columns picker.
26
+ */
27
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = ['name', 'email', 'phones', 'company', 'updatedAt']
28
+
29
+ /** The sort fields `/v1/people` accepts. Anything else is a client-only sort. */
30
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
31
+
32
+ /** Written out because "Firstname" is what deriving these from the key would give. */
33
+ const NAME_PART_HEADERS = {
34
+ salutation: 'Salutation',
35
+ firstName: 'First name',
36
+ lastName: 'Last name',
37
+ suffix: 'Suffix',
38
+ } as const
39
+
13
40
  /**
14
41
  * The People list.
15
42
  *
@@ -22,8 +49,12 @@ export function PeoplePage(): React.JSX.Element {
22
49
  const [term, setTerm] = useState('')
23
50
  const [sort, setSort] = useState<string | undefined>(undefined)
24
51
  const navigate = useNavigate()
52
+ const timezone = useTimezone()
25
53
  const hasFilter = term.trim().length > 0
26
- const people = usePeople({ term: hasFilter ? term.trim() : undefined, sort })
54
+ const people = usePeople({
55
+ term: hasFilter ? term.trim() : undefined,
56
+ sort: serverSortOnly(sort, SERVER_SORT_KEYS),
57
+ })
27
58
  const directory = usePeopleDirectory(people.records.map((person) => person.id))
28
59
  const createPerson = useCreatePerson()
29
60
 
@@ -42,23 +73,115 @@ export function PeoplePage(): React.JSX.Element {
42
73
  sortKey: 'name',
43
74
  render: (person) => <span className="font-medium text-ink">{person.name}</span>,
44
75
  },
76
+ // Hidden by default, and sorted page-locally rather than by the server: the
77
+ // parts are nullable, and `?sort=` is keyset paged, which cannot seek past a
78
+ // null. The catalog carries them so a workspace that imported them can put
79
+ // them back through the Columns picker.
80
+ ...(['salutation', 'firstName', 'lastName', 'suffix'] as const).map(
81
+ (key): Column<Person> => ({
82
+ key,
83
+ header: NAME_PART_HEADERS[key],
84
+ getSortValue: (person) => person[key],
85
+ render: (person) => person[key] ?? '—',
86
+ }),
87
+ ),
45
88
  {
46
89
  key: 'position',
47
90
  header: 'Position',
91
+ getSortValue: (person) => directory.titleFor(person.id) ?? null,
48
92
  render: (person) => directory.titleFor(person.id) ?? '—',
49
93
  },
50
94
  {
51
95
  key: 'company',
52
96
  header: 'Company',
97
+ getSortValue: (person) => directory.companyNamesFor(person.id).join(', ') || null,
53
98
  render: (person) => {
54
99
  const names = directory.companyNamesFor(person.id)
55
100
 
56
101
  return names.length > 0 ? names.join(', ') : '—'
57
102
  },
58
103
  },
104
+ {
105
+ key: 'email',
106
+ header: 'Email',
107
+ getSortValue: (person) => person.email,
108
+ render: (person) =>
109
+ person.email === null ? (
110
+ '—'
111
+ ) : (
112
+ <span className="font-mono text-[12px] text-ink-muted">{person.email}</span>
113
+ ),
114
+ },
115
+ {
116
+ key: 'phones',
117
+ header: 'Phones',
118
+ getSortValue: (person) => person.phones.join(', ') || null,
119
+ render: (person) =>
120
+ person.phones.length === 0 ? '—' : (
121
+ <span className="font-mono text-[12px] text-ink-muted">{person.phones.join(', ')}</span>
122
+ ),
123
+ },
124
+ {
125
+ key: 'socials',
126
+ header: 'Socials',
127
+ getSortValue: (person) => person.socialProfiles.map((entry) => entry.network).join(', ') || null,
128
+ render: (person) =>
129
+ person.socialProfiles.length === 0 ? '—' : (
130
+ <span className="flex flex-wrap gap-1">
131
+ {person.socialProfiles.map((entry) => (
132
+ <Chip key={entry.network}>
133
+ <span className="text-[10px]">{entry.network}</span>
134
+ </Chip>
135
+ ))}
136
+ </span>
137
+ ),
138
+ },
139
+ {
140
+ key: 'location',
141
+ header: 'Location',
142
+ getSortValue: (person) => person.location,
143
+ render: (person) => person.location ?? '—',
144
+ },
145
+ {
146
+ key: 'timezone',
147
+ header: 'Timezone',
148
+ getSortValue: (person) => person.timezone,
149
+ render: (person) =>
150
+ person.timezone === null ? '—' : (
151
+ <span className="font-mono text-[12px] text-ink-muted">{person.timezone}</span>
152
+ ),
153
+ },
154
+ {
155
+ key: 'preferredChannel',
156
+ header: 'Preferred channel',
157
+ getSortValue: (person) => person.preferredChannel,
158
+ render: (person) => <span className="capitalize">{person.preferredChannel}</span>,
159
+ },
160
+ {
161
+ key: 'influence',
162
+ header: 'Influence',
163
+ getSortValue: (person) => person.influence,
164
+ render: (person) => <span className="capitalize">{person.influence.replace('_', ' ')}</span>,
165
+ },
166
+ {
167
+ key: 'relationship',
168
+ header: 'Relationship',
169
+ getSortValue: (person) => person.relationship,
170
+ render: (person) => <span className="capitalize">{person.relationship}</span>,
171
+ },
172
+ {
173
+ key: 'summary',
174
+ header: 'Summary',
175
+ getSortValue: (person) => person.summary || null,
176
+ render: (person) =>
177
+ person.summary.length === 0 ? '—' : (
178
+ <span className="text-ink-muted">{person.summary}</span>
179
+ ),
180
+ },
59
181
  {
60
182
  key: 'tags',
61
183
  header: 'Tags',
184
+ getSortValue: (person) => person.tags.join(', ') || null,
62
185
  render: (person) =>
63
186
  person.tags.length > 0 ? (
64
187
  <span className="flex flex-wrap gap-1">
@@ -72,8 +195,31 @@ export function PeoplePage(): React.JSX.Element {
72
195
  '—'
73
196
  ),
74
197
  },
198
+ {
199
+ key: 'lastContactedAt',
200
+ header: 'Last contacted',
201
+ getSortValue: (person) => person.lastContactedAt,
202
+ render: (person) =>
203
+ person.lastContactedAt === null ? '—' : formatDate(person.lastContactedAt, timezone),
204
+ },
205
+ {
206
+ key: 'createdAt',
207
+ header: 'Created',
208
+ sortKey: 'created_at',
209
+ render: (person) => formatDate(person.createdAt, timezone),
210
+ },
211
+ {
212
+ key: 'updatedAt',
213
+ header: 'Updated',
214
+ sortKey: 'updated_at',
215
+ render: (person) => formatDate(person.updatedAt, timezone),
216
+ },
75
217
  ]
76
218
 
219
+ const supportedKeys = columns.map((column) => column.key)
220
+ const listView = useListView('people', supportedKeys, DEFAULT_VISIBLE_KEYS)
221
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
222
+
77
223
  return (
78
224
  <div className="animate-fade-in">
79
225
  <PageHeader
@@ -82,6 +228,13 @@ export function PeoplePage(): React.JSX.Element {
82
228
  void addPerson()
83
229
  }}
84
230
  addLabel="Add person"
231
+ actions={
232
+ <ColumnPicker
233
+ options={pickerOptions}
234
+ visibleKeys={listView.visibleKeys}
235
+ onChange={listView.setVisibleKeys}
236
+ />
237
+ }
85
238
  />
86
239
  <FilterBar
87
240
  value={term}
@@ -101,6 +254,7 @@ export function PeoplePage(): React.JSX.Element {
101
254
  <LoadingPanel label="Loading people…" />
102
255
  ) : (
103
256
  <>
257
+ <Paginator list={people} placement="top" />
104
258
  <DataTable
105
259
  columns={columns}
106
260
  rows={people.records}
@@ -122,22 +276,14 @@ export function PeoplePage(): React.JSX.Element {
122
276
  }
123
277
  sort={sort}
124
278
  onSortChange={setSort}
279
+ visibleColumnKeys={listView.visibleKeys}
125
280
  />
126
281
  {!directory.isComplete && !directory.isLoading && (
127
282
  <p className="mt-2 text-[11px] text-ink-faint">
128
283
  These people hold more positions than one page returns, so some rows may read “—”.
129
284
  </p>
130
285
  )}
131
- {people.hasMore && (
132
- <button
133
- type="button"
134
- onClick={people.loadMore}
135
- disabled={people.isLoadingMore}
136
- className="mt-3 rounded-md border border-border px-3 py-1.5 text-[12px] font-medium text-ink transition hover:border-border-strong hover:bg-surface-sunken disabled:opacity-50"
137
- >
138
- {people.isLoadingMore ? 'Loading…' : 'Load more'}
139
- </button>
140
- )}
286
+ <Paginator list={people} />
141
287
  </>
142
288
  )}
143
289
  </div>
@@ -1,5 +1,11 @@
1
- import { IN_PROCESS, PREFERRED_CHANNELS } from '@kelpie/schemas'
2
- import type { Candidate, Person, PersonInput, PreferredChannel } from '@kelpie/schemas'
1
+ import { CONSENT_PURPOSE_STATUS_LABELS, IN_PROCESS, PREFERRED_CHANNELS } from '@kelpie/schemas'
2
+ import type {
3
+ Candidate,
4
+ ConsentStatus,
5
+ Person,
6
+ PersonInput,
7
+ PreferredChannel,
8
+ } from '@kelpie/schemas'
3
9
  import { useState } from 'react'
4
10
  import type { FormEvent } from 'react'
5
11
  import { Link, useNavigate, useParams } from 'react-router'
@@ -9,6 +15,8 @@ import type { PatchResult } from '../api/resource.ts'
9
15
  import { useCandidates } from '../api/resources/candidates.ts'
10
16
  import { useCompanies } from '../api/resources/companies.ts'
11
17
  import { useDeals } from '../api/resources/deals.ts'
18
+ import { useEnquiries } from '../api/resources/enquiries.ts'
19
+ import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
12
20
  import { usePartnerships } from '../api/resources/partnerships.ts'
13
21
  import { useDeletePerson, usePerson, useUpdatePerson } from '../api/resources/people.ts'
14
22
  import {
@@ -21,12 +29,16 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
21
29
  import { AgentTasks } from '../components/AgentTasks.tsx'
22
30
  import { Chip } from '../components/Chip.tsx'
23
31
  import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
32
+ import { FormsPanel } from '../components/FormsPanel.tsx'
24
33
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
25
34
  import { EntitySearch } from '../components/EntitySearch.tsx'
35
+ import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
36
+ import { useHasCustomFields } from '../components/useHasCustomFields.ts'
26
37
  import { InlineEdit } from '../components/InlineEdit.tsx'
27
38
  import { NotesPanel } from '../components/NotesPanel.tsx'
28
39
  import { RelatedPlanAttention } from '../components/PlanAttention.tsx'
29
40
  import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
41
+ import { ListsPanel } from '../components/ListsPanel.tsx'
30
42
  import { RecordTabs } from '../components/RecordTabs.tsx'
31
43
  import type { RecordTabDescriptor } from '../components/RecordTabs.tsx'
32
44
  import { SectionHeader } from '../components/SectionHeader.tsx'
@@ -69,10 +81,12 @@ export function PersonDetail(): React.JSX.Element {
69
81
  const { record, isLoading, isNotFound, error } = usePerson(id)
70
82
  const deletePerson = useDeletePerson()
71
83
  const moduleTabs = inSlotOrder(useRecordTabs('person'))
84
+ const hasCustomFields = useHasCustomFields('person')
72
85
  const [activeTab, setActiveTab] = useState('overview')
73
86
  const candidacies = useCandidates({ personIds: id === undefined ? [] : [id] }, {
74
87
  enabled: id !== undefined,
75
88
  })
89
+ const formSubmissions = useFormSubmissionsForRecord('person', id)
76
90
 
77
91
  if (isNotFound) {
78
92
  return <NotFoundPanel label="Person" backTo="/people" />
@@ -88,12 +102,17 @@ export function PersonDetail(): React.JSX.Element {
88
102
 
89
103
  const tabs: readonly RecordTabDescriptor<string>[] = [
90
104
  { id: 'overview', label: 'Overview' },
105
+ ...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
91
106
  { id: 'activity', label: 'Activity' },
92
107
  ...(candidacies.records.length === 0
93
108
  ? []
94
109
  : [{ id: 'hiring', label: 'Hiring', count: candidacies.records.length }]),
95
110
  { id: 'notes', label: 'Notes' },
96
111
  { id: 'decisions', label: 'Decisions' },
112
+ { id: 'lists', label: 'Lists' },
113
+ ...(formSubmissions.records.length === 0
114
+ ? []
115
+ : [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
97
116
  ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
98
117
  ]
99
118
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
@@ -130,18 +149,23 @@ export function PersonDetail(): React.JSX.Element {
130
149
 
131
150
  <RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Person sections">
132
151
  {active === 'overview' && <PersonOverview person={record} />}
152
+ {active === 'fields' && <PersonFields person={record} />}
133
153
  {active === 'activity' && <ActivitiesPanel targetType="person" targetId={record.id} />}
134
154
  {active === 'hiring' && (
135
155
  <PersonHiring candidacies={candidacies.records} personName={record.name} />
136
156
  )}
137
157
  {active === 'notes' && <NotesPanel targetType="person" targetId={record.id} />}
138
158
  {active === 'decisions' && <DecisionsPanel targetType="person" targetId={record.id} />}
159
+ {active === 'lists' && <ListsPanel targetType="person" targetId={record.id} />}
160
+ {active === 'forms' && <FormsPanel targetType="person" targetId={record.id} />}
139
161
  {moduleTab?.render({ objectType: 'person', recordId: record.id })}
140
162
  </RecordTabs>
141
163
  </div>
142
164
 
143
165
  <aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
166
+ <PersonNameParts person={record} />
144
167
  <PersonSidebar person={record} />
168
+ <PersonConsents person={record} />
145
169
  <PersonPositions person={record} />
146
170
  </aside>
147
171
  </div>
@@ -197,6 +221,7 @@ function PersonOverview({ person }: { readonly person: Person }): React.JSX.Elem
197
221
  const { patch, error } = usePersonPatch(person)
198
222
  const deals = useDeals({ personIds: [person.id] })
199
223
  const partnerships = usePartnerships({ personIds: [person.id] })
224
+ const enquiries = useEnquiries({ personIds: [person.id] })
200
225
 
201
226
  return (
202
227
  <div className="space-y-8">
@@ -210,7 +235,8 @@ function PersonOverview({ person }: { readonly person: Person }): React.JSX.Elem
210
235
  <RelatedPlanAttention
211
236
  deals={deals.records}
212
237
  partnerships={partnerships.records}
213
- isLoading={deals.isLoading || partnerships.isLoading}
238
+ enquiries={enquiries.records}
239
+ isLoading={deals.isLoading || partnerships.isLoading || enquiries.isLoading}
214
240
  />
215
241
  <LatestActivity targetType="person" targetId={person.id} />
216
242
  </div>
@@ -297,6 +323,46 @@ function PersonHiring({
297
323
  )
298
324
  }
299
325
 
326
+ /**
327
+ * The parts of the name, beside the name itself rather than instead of it.
328
+ *
329
+ * Editing one of these does not rename the record: the heading is `name`, and
330
+ * `name` is only ever what somebody put there. Both halves are on this page at
331
+ * once so that is visible rather than surprising — a first name corrected here
332
+ * leaves the heading alone, and the heading is one click away when it should
333
+ * change too.
334
+ */
335
+ function PersonNameParts({ person }: { readonly person: Person }): React.JSX.Element {
336
+ const { patch, error } = usePersonPatch(person)
337
+ const fields = [
338
+ { label: 'Salutation', value: person.salutation, key: 'salutation' },
339
+ { label: 'First name', value: person.firstName, key: 'firstName' },
340
+ { label: 'Last name', value: person.lastName, key: 'lastName' },
341
+ { label: 'Suffix', value: person.suffix, key: 'suffix' },
342
+ ] as const
343
+
344
+ return (
345
+ <section className="rounded-md border border-border p-3">
346
+ {error !== null && (
347
+ <div className="mb-2">
348
+ <ErrorPanel error={error} />
349
+ </div>
350
+ )}
351
+ {fields.map((field) => (
352
+ <SidebarField key={field.key} label={field.label}>
353
+ <InlineEdit
354
+ value={field.value ?? ''}
355
+ onChange={(next) => {
356
+ patch({ [field.key]: next.length > 0 ? next : null })
357
+ }}
358
+ displayClassName="not-italic normal-case text-[12px]"
359
+ />
360
+ </SidebarField>
361
+ ))}
362
+ </section>
363
+ )
364
+ }
365
+
300
366
  function PersonSidebar({ person }: { readonly person: Person }): React.JSX.Element {
301
367
  const { patch, error } = usePersonPatch(person)
302
368
 
@@ -376,6 +442,22 @@ function PersonSidebar({ person }: { readonly person: Person }): React.JSX.Eleme
376
442
  )
377
443
  }
378
444
 
445
+ function PersonFields({ person }: { readonly person: Person }): React.JSX.Element {
446
+ const { patch, error } = usePersonPatch(person)
447
+ return (
448
+ <div className="space-y-4">
449
+ {error !== null && <ErrorPanel error={error} />}
450
+ <CustomFieldsPanel
451
+ objectType="person"
452
+ values={person.customFields}
453
+ onPatch={(customFields) => {
454
+ patch({ customFields })
455
+ }}
456
+ />
457
+ </div>
458
+ )
459
+ }
460
+
379
461
  /**
380
462
  * The companies this person holds a title at.
381
463
  *
@@ -408,7 +490,7 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
408
490
  function submit(event: FormEvent): void {
409
491
  event.preventDefault()
410
492
 
411
- if (companyId.length === 0 || title.trim().length === 0) {
493
+ if (companyId.length === 0) {
412
494
  return
413
495
  }
414
496
 
@@ -500,9 +582,8 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
500
582
  onChange={(event) => {
501
583
  setTitle(event.target.value)
502
584
  }}
503
- placeholder="Position title"
585
+ placeholder="Position title (optional)"
504
586
  className="w-full rounded-md border border-border bg-surface-raised px-2 py-1.5 text-[12px] outline-none focus:border-accent"
505
- required
506
587
  />
507
588
  <div className="flex justify-end gap-2">
508
589
  <button
@@ -524,3 +605,73 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
524
605
  </section>
525
606
  )
526
607
  }
608
+
609
+ /**
610
+ * The person's effective consent for every workspace purpose, plus the
611
+ * global `do_not_contact` toggle.
612
+ *
613
+ * An `inherited: true` entry has no explicit `person_consents` row and reads
614
+ * the purpose's default; picking a status writes an override, and the
615
+ * "Inherit" choice clears the override again. Do-not-contact is the global
616
+ * Article 21 objection, independent of per-purpose consent.
617
+ */
618
+ function PersonConsents({ person }: { readonly person: Person }): React.JSX.Element {
619
+ const { patch, error } = usePersonPatch(person)
620
+
621
+ function setConsent(purposeSlug: string, status: ConsentStatus | null): void {
622
+ patch({ consents: [{ purposeSlug, status }] })
623
+ }
624
+
625
+ return (
626
+ <section className="rounded-md border border-border p-3">
627
+ <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
628
+ Privacy and marketing
629
+ </h3>
630
+ {error !== null && (
631
+ <div className="mb-2">
632
+ <ErrorPanel error={error} />
633
+ </div>
634
+ )}
635
+ <label className="flex items-center gap-2 py-1 text-[12px] text-ink">
636
+ <input
637
+ type="checkbox"
638
+ checked={person.doNotContact}
639
+ onChange={(event) => {
640
+ patch({ doNotContact: event.target.checked })
641
+ }}
642
+ />
643
+ Do not contact
644
+ </label>
645
+ {person.consents.length === 0 ? (
646
+ <p className="mt-2 text-[11px] text-ink-muted">
647
+ No consent purposes yet. Define them on /admin/privacy.
648
+ </p>
649
+ ) : (
650
+ person.consents.map((consent) => (
651
+ <SidebarField key={consent.purposeSlug} label={consent.purposeLabel}>
652
+ <div className="flex items-center gap-2">
653
+ <select
654
+ value={consent.inherited ? '' : consent.status}
655
+ onChange={(event) => {
656
+ const value = event.target.value
657
+ setConsent(
658
+ consent.purposeSlug,
659
+ value === '' ? null : (value as ConsentStatus),
660
+ )
661
+ }}
662
+ className="rounded-md border border-border bg-transparent px-2 py-1 text-[12px] outline-none focus:border-accent"
663
+ >
664
+ <option value="">Inherit ({CONSENT_PURPOSE_STATUS_LABELS[consent.status]})</option>
665
+ <option value="granted">Granted</option>
666
+ <option value="withdrawn">Withdrawn</option>
667
+ </select>
668
+ {!consent.inherited && consent.source !== null && (
669
+ <span className="text-[10px] text-ink-faint">via {consent.source}</span>
670
+ )}
671
+ </div>
672
+ </SidebarField>
673
+ ))
674
+ )}
675
+ </section>
676
+ )
677
+ }