@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
@@ -1,16 +1,19 @@
1
1
  import type { Opportunity, OpportunityInput, PipelineStage } from '@kelpie/schemas'
2
2
  import { useState } from 'react'
3
+ import type { FormEvent } from 'react'
3
4
  import { Link, useNavigate, useParams } from 'react-router'
4
5
 
5
6
  import { usePatch } from '../api/resource.ts'
6
7
  import type { PatchResult } from '../api/resource.ts'
7
8
  import { useCompanies, useCompany } from '../api/resources/companies.ts'
9
+ import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
8
10
  import { useMembers } from '../api/resources/members.ts'
9
11
  import {
10
12
  useDeleteOpportunity,
11
13
  useOpportunity,
12
14
  useUpdateOpportunity,
13
15
  } from '../api/resources/opportunities.ts'
16
+ import { usePeople } from '../api/resources/people.ts'
14
17
  import { usePipelineStages } from '../api/resources/pipelineStages.ts'
15
18
  import { useRecordPlanItems } from '../api/resources/planItems.ts'
16
19
  import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.tsx'
@@ -20,13 +23,18 @@ import type { ChipTone } from '../components/Chip.tsx'
20
23
  import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
21
24
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
22
25
  import { EntitySearch } from '../components/EntitySearch.tsx'
26
+ import { FormsPanel } from '../components/FormsPanel.tsx'
27
+ import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
28
+ import { useHasCustomFields } from '../components/useHasCustomFields.ts'
23
29
  import { InlineEdit } from '../components/InlineEdit.tsx'
30
+ import { ListsPanel } from '../components/ListsPanel.tsx'
24
31
  import { NotesPanel } from '../components/NotesPanel.tsx'
25
32
  import { PlanAttention } from '../components/PlanAttention.tsx'
26
33
  import { PlanPanel } from '../components/PlanPanel.tsx'
27
34
  import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
28
35
  import { RecordTabs } from '../components/RecordTabs.tsx'
29
36
  import type { RecordTabDescriptor } from '../components/RecordTabs.tsx'
37
+ import { SectionHeader } from '../components/SectionHeader.tsx'
30
38
  import { SidebarField } from '../components/SidebarField.tsx'
31
39
  import { SummaryBlock } from '../components/SummaryBlock.tsx'
32
40
  import { formatDay } from '../lib/dates.ts'
@@ -38,8 +46,10 @@ import { toTags } from './fields.ts'
38
46
  * One opportunity.
39
47
  *
40
48
  * The same shape as `DealDetail` minus what an opportunity does not have: no
41
- * value, no contacts, and a company that may be absent. A UI module can add its
42
- * own tab through the `opportunity` record-tab slot.
49
+ * value, and a company that may be absent. People attach through `person_links`
50
+ * the same way they do on a deal, and they are shown in the same contacts
51
+ * section in the aside. A UI module can add its own tab through the
52
+ * `opportunity` record-tab slot.
43
53
  */
44
54
 
45
55
  const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
@@ -53,7 +63,9 @@ export function OpportunityDetail(): React.JSX.Element {
53
63
  const { record, isLoading, isNotFound, error } = useOpportunity(id)
54
64
  const deleteOpportunity = useDeleteOpportunity()
55
65
  const moduleTabs = inSlotOrder(useRecordTabs('opportunity'))
66
+ const hasCustomFields = useHasCustomFields('opportunity')
56
67
  const [activeTab, setActiveTab] = useState('overview')
68
+ const formSubmissions = useFormSubmissionsForRecord('opportunity', id)
57
69
 
58
70
  if (isNotFound) {
59
71
  return <NotFoundPanel label="Opportunity" backTo="/opportunities" />
@@ -69,10 +81,15 @@ export function OpportunityDetail(): React.JSX.Element {
69
81
 
70
82
  const tabs: readonly RecordTabDescriptor<string>[] = [
71
83
  { id: 'overview', label: 'Overview' },
84
+ ...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
72
85
  { id: 'plan', label: 'Plan' },
73
86
  { id: 'activity', label: 'Activity' },
74
87
  { id: 'notes', label: 'Notes' },
75
88
  { id: 'decisions', label: 'Decisions' },
89
+ { id: 'lists', label: 'Lists' },
90
+ ...(formSubmissions.records.length === 0
91
+ ? []
92
+ : [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
76
93
  ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
77
94
  ]
78
95
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
@@ -114,6 +131,7 @@ export function OpportunityDetail(): React.JSX.Element {
114
131
  ariaLabel="Opportunity sections"
115
132
  >
116
133
  {active === 'overview' && <OpportunityOverview opportunity={record} />}
134
+ {active === 'fields' && <OpportunityFields opportunity={record} />}
117
135
  {active === 'plan' && <PlanPanel targetType="opportunity" targetId={record.id} />}
118
136
  {active === 'activity' && (
119
137
  <ActivitiesPanel targetType="opportunity" targetId={record.id} />
@@ -122,12 +140,15 @@ export function OpportunityDetail(): React.JSX.Element {
122
140
  {active === 'decisions' && (
123
141
  <DecisionsPanel targetType="opportunity" targetId={record.id} />
124
142
  )}
143
+ {active === 'lists' && <ListsPanel targetType="opportunity" targetId={record.id} />}
144
+ {active === 'forms' && <FormsPanel targetType="opportunity" targetId={record.id} />}
125
145
  {moduleTab?.render({ objectType: 'opportunity', recordId: record.id })}
126
146
  </RecordTabs>
127
147
  </div>
128
148
 
129
149
  <aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
130
150
  <OpportunitySidebar opportunity={record} />
151
+ <OpportunityContacts opportunity={record} />
131
152
  </aside>
132
153
  </div>
133
154
  </div>
@@ -335,3 +356,148 @@ function OpportunitySidebar({
335
356
  </section>
336
357
  )
337
358
  }
359
+
360
+ function OpportunityFields({
361
+ opportunity,
362
+ }: {
363
+ readonly opportunity: Opportunity
364
+ }): React.JSX.Element {
365
+ const { patch, error } = useOpportunityPatch(opportunity)
366
+ return (
367
+ <div className="space-y-4">
368
+ {error !== null && <ErrorPanel error={error} />}
369
+ <CustomFieldsPanel
370
+ objectType="opportunity"
371
+ values={opportunity.customFields}
372
+ onPatch={(customFields) => {
373
+ patch({ customFields })
374
+ }}
375
+ />
376
+ </div>
377
+ )
378
+ }
379
+
380
+ /** The people on the opportunity, linked and unlinked by replacing `person_ids`. */
381
+ function OpportunityContacts({
382
+ opportunity,
383
+ }: {
384
+ readonly opportunity: Opportunity
385
+ }): React.JSX.Element {
386
+ const { patch, error } = useOpportunityPatch(opportunity)
387
+
388
+ const [adding, setAdding] = useState(false)
389
+ const [personId, setPersonId] = useState('')
390
+ const [search, setSearch] = useState('')
391
+
392
+ // No `?id=` filter exists on people, so names come from the directory's first
393
+ // page. Past it, a contact renders by id rather than silently vanishing.
394
+ const directory = usePeople({ limit: 200 })
395
+ const searchable = usePeople({ term: search.trim().length > 0 ? search.trim() : undefined })
396
+ const nameById = new Map(directory.records.map((person) => [person.id, person.name]))
397
+ const linked = new Set(opportunity.personIds)
398
+
399
+ function reset(): void {
400
+ setAdding(false)
401
+ setPersonId('')
402
+ setSearch('')
403
+ }
404
+
405
+ function submit(event: FormEvent): void {
406
+ event.preventDefault()
407
+
408
+ if (personId.length === 0 || linked.has(personId)) {
409
+ return
410
+ }
411
+
412
+ patch({ personIds: [...opportunity.personIds, personId] })
413
+ reset()
414
+ }
415
+
416
+ return (
417
+ <section className="rounded-md border border-border">
418
+ <div className="border-b border-border px-3.5 py-2.5">
419
+ <SectionHeader
420
+ title="Contacts"
421
+ onAdd={() => {
422
+ setAdding((current) => !current)
423
+ }}
424
+ addLabel="Add contact"
425
+ compact
426
+ />
427
+ </div>
428
+
429
+ {error !== null && (
430
+ <div className="px-3.5 py-2">
431
+ <ErrorPanel error={error} />
432
+ </div>
433
+ )}
434
+
435
+ <ul className="divide-y divide-border">
436
+ {opportunity.personIds.length === 0 && !adding && (
437
+ <li className="px-3.5 py-4 text-[12px] text-ink-faint">No contacts yet.</li>
438
+ )}
439
+ {opportunity.personIds.map((linkedPersonId) => (
440
+ <li
441
+ key={linkedPersonId}
442
+ className="flex items-start justify-between gap-2 px-3.5 py-2.5"
443
+ >
444
+ <Link
445
+ to={`/people/${linkedPersonId}`}
446
+ className="text-[13px] font-medium text-ink hover:text-accent"
447
+ >
448
+ {nameById.get(linkedPersonId) ?? linkedPersonId}
449
+ </Link>
450
+ <button
451
+ type="button"
452
+ onClick={() => {
453
+ patch({
454
+ personIds: opportunity.personIds.filter(
455
+ (candidate) => candidate !== linkedPersonId,
456
+ ),
457
+ })
458
+ }}
459
+ className="text-[11px] font-medium text-danger hover:underline"
460
+ >
461
+ Remove
462
+ </button>
463
+ </li>
464
+ ))}
465
+ </ul>
466
+
467
+ {adding && (
468
+ <form onSubmit={submit} className="space-y-2 border-t border-border bg-surface px-3.5 py-3">
469
+ <EntitySearch
470
+ options={searchable.records
471
+ .filter((person) => !linked.has(person.id))
472
+ .map((person) => ({
473
+ id: person.id,
474
+ label: person.name,
475
+ meta: person.email ?? undefined,
476
+ }))}
477
+ value={personId}
478
+ onChange={setPersonId}
479
+ onQueryChange={setSearch}
480
+ placeholder="Search people…"
481
+ emptyMessage="No people match"
482
+ required
483
+ />
484
+ <div className="flex justify-end gap-2">
485
+ <button
486
+ type="button"
487
+ onClick={reset}
488
+ className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink"
489
+ >
490
+ Cancel
491
+ </button>
492
+ <button
493
+ type="submit"
494
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover"
495
+ >
496
+ Add
497
+ </button>
498
+ </div>
499
+ </form>
500
+ )}
501
+ </section>
502
+ )
503
+ }
@@ -6,6 +6,7 @@ import { Link, useNavigate, useParams } from 'react-router'
6
6
  import { usePatch } from '../api/resource.ts'
7
7
  import type { PatchResult } from '../api/resource.ts'
8
8
  import { useCompanies, useCompany } from '../api/resources/companies.ts'
9
+ import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
9
10
  import { useMembers } from '../api/resources/members.ts'
10
11
  import {
11
12
  useDeletePartnership,
@@ -22,7 +23,11 @@ import type { ChipTone } from '../components/Chip.tsx'
22
23
  import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
23
24
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
24
25
  import { EntitySearch } from '../components/EntitySearch.tsx'
26
+ import { FormsPanel } from '../components/FormsPanel.tsx'
27
+ import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
28
+ import { useHasCustomFields } from '../components/useHasCustomFields.ts'
25
29
  import { InlineEdit } from '../components/InlineEdit.tsx'
30
+ import { ListsPanel } from '../components/ListsPanel.tsx'
26
31
  import { NotesPanel } from '../components/NotesPanel.tsx'
27
32
  import { PlanAttention } from '../components/PlanAttention.tsx'
28
33
  import { PlanPanel } from '../components/PlanPanel.tsx'
@@ -58,7 +63,9 @@ export function PartnershipDetail(): React.JSX.Element {
58
63
  const { record, isLoading, isNotFound, error } = usePartnership(id)
59
64
  const deletePartnership = useDeletePartnership()
60
65
  const moduleTabs = inSlotOrder(useRecordTabs('partnership'))
66
+ const hasCustomFields = useHasCustomFields('partnership')
61
67
  const [activeTab, setActiveTab] = useState('overview')
68
+ const formSubmissions = useFormSubmissionsForRecord('partnership', id)
62
69
 
63
70
  if (isNotFound) {
64
71
  return <NotFoundPanel label="Partnership" backTo="/partnerships" />
@@ -74,10 +81,15 @@ export function PartnershipDetail(): React.JSX.Element {
74
81
 
75
82
  const tabs: readonly RecordTabDescriptor<string>[] = [
76
83
  { id: 'overview', label: 'Overview' },
84
+ ...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
77
85
  { id: 'plan', label: 'Plan' },
78
86
  { id: 'activity', label: 'Activity' },
79
87
  { id: 'notes', label: 'Notes' },
80
88
  { id: 'decisions', label: 'Decisions' },
89
+ { id: 'lists', label: 'Lists' },
90
+ ...(formSubmissions.records.length === 0
91
+ ? []
92
+ : [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
81
93
  ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
82
94
  ]
83
95
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
@@ -119,6 +131,7 @@ export function PartnershipDetail(): React.JSX.Element {
119
131
  ariaLabel="Partnership sections"
120
132
  >
121
133
  {active === 'overview' && <PartnershipOverview partnership={record} />}
134
+ {active === 'fields' && <PartnershipFields partnership={record} />}
122
135
  {active === 'plan' && <PlanPanel targetType="partnership" targetId={record.id} />}
123
136
  {active === 'activity' && (
124
137
  <ActivitiesPanel targetType="partnership" targetId={record.id} />
@@ -127,6 +140,8 @@ export function PartnershipDetail(): React.JSX.Element {
127
140
  {active === 'decisions' && (
128
141
  <DecisionsPanel targetType="partnership" targetId={record.id} />
129
142
  )}
143
+ {active === 'lists' && <ListsPanel targetType="partnership" targetId={record.id} />}
144
+ {active === 'forms' && <FormsPanel targetType="partnership" targetId={record.id} />}
130
145
  {moduleTab?.render({ objectType: 'partnership', recordId: record.id })}
131
146
  </RecordTabs>
132
147
  </div>
@@ -364,6 +379,26 @@ function PartnershipSidebar({
364
379
  )
365
380
  }
366
381
 
382
+ function PartnershipFields({
383
+ partnership,
384
+ }: {
385
+ readonly partnership: Partnership
386
+ }): React.JSX.Element {
387
+ const { patch, error } = usePartnershipPatch(partnership)
388
+ return (
389
+ <div className="space-y-4">
390
+ {error !== null && <ErrorPanel error={error} />}
391
+ <CustomFieldsPanel
392
+ objectType="partnership"
393
+ values={partnership.customFields}
394
+ onPatch={(customFields) => {
395
+ patch({ customFields })
396
+ }}
397
+ />
398
+ </div>
399
+ )
400
+ }
401
+
367
402
  /** The key people on the partnership, linked and unlinked by replacing `person_ids`. */
368
403
  function PartnershipKeyPeople({
369
404
  partnership,
@@ -1,5 +1,4 @@
1
1
  import type { Partnership, PipelineStage } from '@kelpie/schemas'
2
- import { useState } from 'react'
3
2
  import { Link, useNavigate } from 'react-router'
4
3
 
5
4
  import { useCompanies } from '../api/resources/companies.ts'
@@ -11,16 +10,21 @@ import {
11
10
  } from '../api/resources/partnerships.ts'
12
11
  import { usePipelineStages } from '../api/resources/pipelineStages.ts'
13
12
  import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
13
+ import { useTimezone } from '../api/resources/account.ts'
14
14
  import { Chip } from '../components/Chip.tsx'
15
15
  import type { ChipTone } from '../components/Chip.tsx'
16
+ import { ColumnPicker } from '../components/ColumnPicker.tsx'
16
17
  import { DataTable } from '../components/DataTable.tsx'
17
18
  import type { Column, DataTableGroup } from '../components/DataTable.tsx'
18
19
  import { KanbanBoard } from '../components/KanbanBoard.tsx'
19
20
  import { PageHeader } from '../components/PageHeader.tsx'
21
+ import { Paginator } from '../components/Paginator.tsx'
20
22
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
21
23
  import { SegmentedControl } from '../components/SegmentedControl.tsx'
22
- import { formatDay } from '../lib/dates.ts'
24
+ import { formatDate, formatDay } from '../lib/dates.ts'
25
+ import { useListView } from '../lib/listView.ts'
23
26
  import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
27
+ import { serverSortOnly } from '../lib/sort.ts'
24
28
 
25
29
  /**
26
30
  * The Partnerships board: ongoing two-way relationships, grouped by status. The
@@ -32,6 +36,33 @@ type BoardView = 'list' | 'columns'
32
36
  type PipelineScope = 'open' | 'all'
33
37
  type ListGrouping = 'stage' | 'due'
34
38
 
39
+ const SCOPES: readonly PipelineScope[] = ['open', 'all']
40
+ const GROUPINGS: readonly ListGrouping[] = ['stage', 'due']
41
+
42
+ function readScope(stored: string | undefined): PipelineScope {
43
+ return SCOPES.includes(stored as PipelineScope) ? (stored as PipelineScope) : 'open'
44
+ }
45
+
46
+ function readGrouping(stored: string | undefined): ListGrouping {
47
+ return GROUPINGS.includes(stored as ListGrouping) ? (stored as ListGrouping) : 'stage'
48
+ }
49
+
50
+ const COLUMN_KEYS: readonly string[] = [
51
+ 'name',
52
+ 'company',
53
+ 'stage',
54
+ 'kind',
55
+ 'nextPlan',
56
+ 'owner',
57
+ 'next',
58
+ 'goals',
59
+ 'successLooksLike',
60
+ 'tags',
61
+ 'summary',
62
+ 'createdAt',
63
+ 'updatedAt',
64
+ ]
65
+
35
66
  /** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
36
67
  const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
37
68
  active: 'success',
@@ -43,19 +74,33 @@ function stageTone(stage: PipelineStage): ChipTone {
43
74
  return STAGE_TONES[stage.slug] ?? 'neutral'
44
75
  }
45
76
 
77
+ const DEFAULT_VISIBLE_KEYS: readonly string[] = [
78
+ 'name',
79
+ 'company',
80
+ 'kind',
81
+ 'nextPlan',
82
+ 'owner',
83
+ 'next',
84
+ ]
85
+
86
+ const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
87
+
46
88
  export function PartnershipsPage(): React.JSX.Element {
47
89
  const navigate = useNavigate()
48
- const [view, setView] = useState<BoardView>('list')
49
- const [scope, setScope] = useState<PipelineScope>('open')
50
- const [grouping, setGrouping] = useState<ListGrouping>('stage')
51
- const [sort, setSort] = useState<string | undefined>(undefined)
90
+
91
+ const listView = useListView('partnerships', COLUMN_KEYS, DEFAULT_VISIBLE_KEYS)
92
+ const view: BoardView = listView.mode ?? 'columns'
93
+ const scope = readScope(listView.scope)
94
+ const grouping = readGrouping(listView.grouping)
95
+ const sort = listView.sort
52
96
 
53
97
  const stages = usePipelineStages('partnership')
54
- const partnerships = usePartnerships({ sort })
98
+ const partnerships = usePartnerships({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
55
99
  const companies = useCompanies({ limit: 200 })
56
100
  const members = useMembers()
57
101
  const createPartnership = useCreatePartnership()
58
102
  const updatePartnership = useUpdatePartnership()
103
+ const timezone = useTimezone()
59
104
 
60
105
  const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
61
106
  const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
@@ -78,7 +123,7 @@ export function PartnershipsPage(): React.JSX.Element {
78
123
  { enabled: askedIds.length > 0 },
79
124
  )
80
125
  const nextPlanByPartnership = nextOpenByTarget(planItems.records)
81
- const plansTruncated = visiblePartnerships.length > MAX_PAGE_SIZE || planItems.hasMore
126
+ const plansTruncated = visiblePartnerships.length > MAX_PAGE_SIZE || planItems.hasNext
82
127
 
83
128
  const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
84
129
 
@@ -97,6 +142,8 @@ export function PartnershipsPage(): React.JSX.Element {
97
142
  await navigate(`/partnerships/${partnership.id}`)
98
143
  }
99
144
 
145
+ const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
146
+
100
147
  const columns: readonly Column<Partnership>[] = [
101
148
  {
102
149
  key: 'name',
@@ -107,16 +154,25 @@ export function PartnershipsPage(): React.JSX.Element {
107
154
  {
108
155
  key: 'company',
109
156
  header: 'Company',
157
+ getSortValue: (partnership) => companyNameById.get(partnership.companyId) ?? null,
110
158
  render: (partnership) => companyNameById.get(partnership.companyId) ?? '—',
111
159
  },
160
+ {
161
+ key: 'stage',
162
+ header: 'Stage',
163
+ getSortValue: (partnership) => stageLabelById.get(partnership.stageId) ?? null,
164
+ render: (partnership) => stageLabelById.get(partnership.stageId) ?? '—',
165
+ },
112
166
  {
113
167
  key: 'kind',
114
168
  header: 'Kind',
169
+ getSortValue: (partnership) => partnership.kind || null,
115
170
  render: (partnership) => (partnership.kind.length > 0 ? partnership.kind : '—'),
116
171
  },
117
172
  {
118
173
  key: 'nextPlan',
119
174
  header: 'Next plan',
175
+ getSortValue: (partnership) => nextPlanByPartnership.get(partnership.id)?.date ?? null,
120
176
  render: (partnership) => {
121
177
  const next = nextPlanByPartnership.get(partnership.id)
122
178
 
@@ -137,6 +193,10 @@ export function PartnershipsPage(): React.JSX.Element {
137
193
  {
138
194
  key: 'owner',
139
195
  header: 'Owner',
196
+ getSortValue: (partnership) =>
197
+ partnership.ownerId === null
198
+ ? null
199
+ : (members.nameById.get(partnership.ownerId) ?? 'Unknown'),
140
200
  render: (partnership) =>
141
201
  partnership.ownerId === null
142
202
  ? '—'
@@ -146,14 +206,71 @@ export function PartnershipsPage(): React.JSX.Element {
146
206
  key: 'next',
147
207
  header: 'Next touch',
148
208
  className: 'w-28',
209
+ getSortValue: (partnership) => partnership.nextTouchpoint,
149
210
  render: (partnership) => (
150
211
  <span className="font-mono text-[12px] text-ink-muted">
151
212
  {partnership.nextTouchpoint === null ? '—' : formatDay(partnership.nextTouchpoint)}
152
213
  </span>
153
214
  ),
154
215
  },
216
+ {
217
+ key: 'goals',
218
+ header: 'Goals',
219
+ getSortValue: (partnership) => partnership.goals || null,
220
+ render: (partnership) =>
221
+ partnership.goals.length === 0 ? '—' : (
222
+ <span className="text-ink-muted">{partnership.goals}</span>
223
+ ),
224
+ },
225
+ {
226
+ key: 'successLooksLike',
227
+ header: 'Success looks like',
228
+ getSortValue: (partnership) => partnership.successLooksLike || null,
229
+ render: (partnership) =>
230
+ partnership.successLooksLike.length === 0 ? '—' : (
231
+ <span className="text-ink-muted">{partnership.successLooksLike}</span>
232
+ ),
233
+ },
234
+ {
235
+ key: 'tags',
236
+ header: 'Tags',
237
+ getSortValue: (partnership) => partnership.tags.join(', ') || null,
238
+ render: (partnership) =>
239
+ partnership.tags.length === 0 ? '—' : (
240
+ <span className="flex flex-wrap gap-1">
241
+ {partnership.tags.map((tag) => (
242
+ <Chip key={tag}>
243
+ <span className="text-[10px]">{tag}</span>
244
+ </Chip>
245
+ ))}
246
+ </span>
247
+ ),
248
+ },
249
+ {
250
+ key: 'summary',
251
+ header: 'Summary',
252
+ getSortValue: (partnership) => partnership.summary || null,
253
+ render: (partnership) =>
254
+ partnership.summary.length === 0 ? '—' : (
255
+ <span className="text-ink-muted">{partnership.summary}</span>
256
+ ),
257
+ },
258
+ {
259
+ key: 'createdAt',
260
+ header: 'Created',
261
+ sortKey: 'created_at',
262
+ render: (partnership) => formatDate(partnership.createdAt, timezone),
263
+ },
264
+ {
265
+ key: 'updatedAt',
266
+ header: 'Updated',
267
+ sortKey: 'updated_at',
268
+ render: (partnership) => formatDate(partnership.updatedAt, timezone),
269
+ },
155
270
  ]
156
271
 
272
+ const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
273
+
157
274
  const stageGroups: readonly DataTableGroup<Partnership>[] = visibleStages.map((stage) => ({
158
275
  id: stage.id,
159
276
  label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
@@ -217,7 +334,9 @@ export function PartnershipsPage(): React.JSX.Element {
217
334
  <SegmentedControl
218
335
  ariaLabel="Status scope"
219
336
  value={scope}
220
- onChange={setScope}
337
+ onChange={(next) => {
338
+ listView.setScope(next)
339
+ }}
221
340
  options={[
222
341
  { id: 'open', label: 'Open' },
223
342
  { id: 'all', label: 'All' },
@@ -227,7 +346,9 @@ export function PartnershipsPage(): React.JSX.Element {
227
346
  <SegmentedControl
228
347
  ariaLabel="Group the list"
229
348
  value={grouping}
230
- onChange={setGrouping}
349
+ onChange={(next) => {
350
+ listView.setGrouping(next)
351
+ }}
231
352
  options={[
232
353
  { id: 'stage', label: 'By status' },
233
354
  { id: 'due', label: 'By due' },
@@ -237,12 +358,21 @@ export function PartnershipsPage(): React.JSX.Element {
237
358
  <SegmentedControl
238
359
  ariaLabel="Board or list"
239
360
  value={view}
240
- onChange={setView}
361
+ onChange={(next) => {
362
+ listView.setMode(next)
363
+ }}
241
364
  options={[
242
365
  { id: 'list', label: 'List' },
243
366
  { id: 'columns', label: 'Board' },
244
367
  ]}
245
368
  />
369
+ {view === 'list' && (
370
+ <ColumnPicker
371
+ options={pickerOptions}
372
+ visibleKeys={listView.visibleKeys}
373
+ onChange={listView.setVisibleKeys}
374
+ />
375
+ )}
246
376
  </>
247
377
  }
248
378
  />
@@ -265,6 +395,7 @@ export function PartnershipsPage(): React.JSX.Element {
265
395
  <LoadingPanel label="Loading partnerships…" />
266
396
  ) : (
267
397
  <>
398
+ {view === 'list' && <Paginator list={partnerships} placement="top" />}
268
399
  {view === 'columns' ? (
269
400
  <KanbanBoard
270
401
  stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
@@ -311,13 +442,16 @@ export function PartnershipsPage(): React.JSX.Element {
311
442
  ? {
312
443
  label: 'Show all partnerships',
313
444
  onClick: () => {
314
- setScope('all')
445
+ listView.setScope('all')
315
446
  },
316
447
  }
317
448
  : undefined
318
449
  }
319
450
  sort={sort}
320
- onSortChange={setSort}
451
+ onSortChange={(next) => {
452
+ listView.setSort(next)
453
+ }}
454
+ visibleColumnKeys={listView.visibleKeys}
321
455
  />
322
456
  )}
323
457
  {updatePartnership.error !== null && (
@@ -325,17 +459,8 @@ export function PartnershipsPage(): React.JSX.Element {
325
459
  <ErrorPanel error={updatePartnership.error} />
326
460
  </div>
327
461
  )}
328
- {partnerships.hasMore && (
329
- <button
330
- type="button"
331
- onClick={partnerships.loadMore}
332
- disabled={partnerships.isLoadingMore}
333
- 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"
334
- >
335
- {partnerships.isLoadingMore ? 'Loading…' : 'Load more'}
336
- </button>
337
- )}
338
- {companies.hasMore && (
462
+ {view === 'list' && <Paginator list={partnerships} />}
463
+ {companies.hasNext && (
339
464
  <p className="mt-2 text-[11px] text-ink-faint">
340
465
  More companies exist than one page returns, so some company names may show as “—”.
341
466
  </p>