@kelpie/ui 0.6.0 → 0.8.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 (243) hide show
  1. package/dist/api/resource.d.ts +58 -5
  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/decisions.d.ts +2 -0
  10. package/dist/api/resources/decisions.d.ts.map +1 -1
  11. package/dist/api/resources/decisions.js +1 -0
  12. package/dist/api/resources/decisions.js.map +1 -1
  13. package/dist/api/resources/emailDomainLinker.d.ts +16 -0
  14. package/dist/api/resources/emailDomainLinker.d.ts.map +1 -0
  15. package/dist/api/resources/emailDomainLinker.js +16 -0
  16. package/dist/api/resources/emailDomainLinker.js.map +1 -0
  17. package/dist/api/resources/forms.d.ts.map +1 -1
  18. package/dist/api/resources/forms.js +6 -20
  19. package/dist/api/resources/forms.js.map +1 -1
  20. package/dist/api/resources/handbookPages.d.ts +1 -1
  21. package/dist/api/resources/handbookPages.js +1 -1
  22. package/dist/api/resources/importJobs.d.ts +3 -1
  23. package/dist/api/resources/importJobs.d.ts.map +1 -1
  24. package/dist/api/resources/importJobs.js +1 -0
  25. package/dist/api/resources/importJobs.js.map +1 -1
  26. package/dist/api/resources/listMembers.d.ts +27 -0
  27. package/dist/api/resources/listMembers.d.ts.map +1 -0
  28. package/dist/api/resources/listMembers.js +108 -0
  29. package/dist/api/resources/listMembers.js.map +1 -0
  30. package/dist/api/resources/lists.d.ts +14 -0
  31. package/dist/api/resources/lists.d.ts.map +1 -0
  32. package/dist/api/resources/lists.js +34 -0
  33. package/dist/api/resources/lists.js.map +1 -0
  34. package/dist/api/resources/people.d.ts.map +1 -1
  35. package/dist/api/resources/people.js +4 -1
  36. package/dist/api/resources/people.js.map +1 -1
  37. package/dist/api/resources/publicConfig.d.ts +8 -0
  38. package/dist/api/resources/publicConfig.d.ts.map +1 -0
  39. package/dist/api/resources/publicConfig.js +22 -0
  40. package/dist/api/resources/publicConfig.js.map +1 -0
  41. package/dist/api/resources/roles.d.ts +2 -0
  42. package/dist/api/resources/roles.d.ts.map +1 -1
  43. package/dist/api/resources/roles.js +1 -1
  44. package/dist/api/resources/roles.js.map +1 -1
  45. package/dist/api/resources/sampleData.d.ts +17 -0
  46. package/dist/api/resources/sampleData.d.ts.map +1 -0
  47. package/dist/api/resources/sampleData.js +30 -0
  48. package/dist/api/resources/sampleData.js.map +1 -0
  49. package/dist/api/resources/webhooks.d.ts.map +1 -1
  50. package/dist/api/resources/webhooks.js +7 -22
  51. package/dist/api/resources/webhooks.js.map +1 -1
  52. package/dist/app/KelpieApp.d.ts.map +1 -1
  53. package/dist/app/KelpieApp.js +5 -2
  54. package/dist/app/KelpieApp.js.map +1 -1
  55. package/dist/components/ActivitiesPanel.d.ts.map +1 -1
  56. package/dist/components/ActivitiesPanel.js +2 -1
  57. package/dist/components/ActivitiesPanel.js.map +1 -1
  58. package/dist/components/ColumnPicker.d.ts +24 -0
  59. package/dist/components/ColumnPicker.d.ts.map +1 -0
  60. package/dist/components/ColumnPicker.js +56 -0
  61. package/dist/components/ColumnPicker.js.map +1 -0
  62. package/dist/components/DataTable.d.ts +16 -1
  63. package/dist/components/DataTable.d.ts.map +1 -1
  64. package/dist/components/DataTable.js +51 -10
  65. package/dist/components/DataTable.js.map +1 -1
  66. package/dist/components/DecisionsPanel.d.ts.map +1 -1
  67. package/dist/components/DecisionsPanel.js +4 -3
  68. package/dist/components/DecisionsPanel.js.map +1 -1
  69. package/dist/components/EntitySearch.d.ts.map +1 -1
  70. package/dist/components/EntitySearch.js +11 -5
  71. package/dist/components/EntitySearch.js.map +1 -1
  72. package/dist/components/EnvironmentBanner.d.ts +14 -0
  73. package/dist/components/EnvironmentBanner.d.ts.map +1 -0
  74. package/dist/components/EnvironmentBanner.js +23 -0
  75. package/dist/components/EnvironmentBanner.js.map +1 -0
  76. package/dist/components/ListsPanel.d.ts +14 -0
  77. package/dist/components/ListsPanel.d.ts.map +1 -0
  78. package/dist/components/ListsPanel.js +56 -0
  79. package/dist/components/ListsPanel.js.map +1 -0
  80. package/dist/components/NotesPanel.d.ts.map +1 -1
  81. package/dist/components/NotesPanel.js +6 -5
  82. package/dist/components/NotesPanel.js.map +1 -1
  83. package/dist/components/Paginator.d.ts +26 -0
  84. package/dist/components/Paginator.d.ts.map +1 -0
  85. package/dist/components/Paginator.js +16 -0
  86. package/dist/components/Paginator.js.map +1 -0
  87. package/dist/components/PlanPanel.d.ts.map +1 -1
  88. package/dist/components/PlanPanel.js +2 -1
  89. package/dist/components/PlanPanel.js.map +1 -1
  90. package/dist/components/Shell.d.ts.map +1 -1
  91. package/dist/components/Shell.js +1 -0
  92. package/dist/components/Shell.js.map +1 -1
  93. package/dist/index.d.ts +8 -2
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +4 -1
  96. package/dist/index.js.map +1 -1
  97. package/dist/lib/listView.d.ts +15 -0
  98. package/dist/lib/listView.d.ts.map +1 -0
  99. package/dist/lib/listView.js +66 -0
  100. package/dist/lib/listView.js.map +1 -0
  101. package/dist/lib/sort.d.ts +43 -0
  102. package/dist/lib/sort.d.ts.map +1 -0
  103. package/dist/lib/sort.js +102 -0
  104. package/dist/lib/sort.js.map +1 -0
  105. package/dist/pages/CompaniesPage.d.ts.map +1 -1
  106. package/dist/pages/CompaniesPage.js +88 -3
  107. package/dist/pages/CompaniesPage.js.map +1 -1
  108. package/dist/pages/CompanyDetail.d.ts.map +1 -1
  109. package/dist/pages/CompanyDetail.js +17 -14
  110. package/dist/pages/CompanyDetail.js.map +1 -1
  111. package/dist/pages/DealDetail.d.ts.map +1 -1
  112. package/dist/pages/DealDetail.js +3 -1
  113. package/dist/pages/DealDetail.js.map +1 -1
  114. package/dist/pages/DealsPage.d.ts.map +1 -1
  115. package/dist/pages/DealsPage.js +134 -14
  116. package/dist/pages/DealsPage.js.map +1 -1
  117. package/dist/pages/DecisionsPage.d.ts.map +1 -1
  118. package/dist/pages/DecisionsPage.js +50 -8
  119. package/dist/pages/DecisionsPage.js.map +1 -1
  120. package/dist/pages/FormsPage.d.ts.map +1 -1
  121. package/dist/pages/FormsPage.js +53 -2
  122. package/dist/pages/FormsPage.js.map +1 -1
  123. package/dist/pages/FundraisingPage.d.ts.map +1 -1
  124. package/dist/pages/FundraisingPage.js +116 -14
  125. package/dist/pages/FundraisingPage.js.map +1 -1
  126. package/dist/pages/HiringPage.d.ts.map +1 -1
  127. package/dist/pages/HiringPage.js +27 -4
  128. package/dist/pages/HiringPage.js.map +1 -1
  129. package/dist/pages/ListDetail.d.ts +11 -0
  130. package/dist/pages/ListDetail.d.ts.map +1 -0
  131. package/dist/pages/ListDetail.js +188 -0
  132. package/dist/pages/ListDetail.js.map +1 -0
  133. package/dist/pages/ListsPage.d.ts +9 -0
  134. package/dist/pages/ListsPage.d.ts.map +1 -0
  135. package/dist/pages/ListsPage.js +125 -0
  136. package/dist/pages/ListsPage.js.map +1 -0
  137. package/dist/pages/OpportunitiesPage.d.ts.map +1 -1
  138. package/dist/pages/OpportunitiesPage.js +97 -14
  139. package/dist/pages/OpportunitiesPage.js.map +1 -1
  140. package/dist/pages/OpportunityDetail.d.ts.map +1 -1
  141. package/dist/pages/OpportunityDetail.js +3 -1
  142. package/dist/pages/OpportunityDetail.js.map +1 -1
  143. package/dist/pages/PartnershipDetail.d.ts.map +1 -1
  144. package/dist/pages/PartnershipDetail.js +3 -1
  145. package/dist/pages/PartnershipDetail.js.map +1 -1
  146. package/dist/pages/PartnershipsPage.d.ts.map +1 -1
  147. package/dist/pages/PartnershipsPage.js +111 -14
  148. package/dist/pages/PartnershipsPage.js.map +1 -1
  149. package/dist/pages/PeoplePage.d.ts.map +1 -1
  150. package/dist/pages/PeoplePage.js +102 -3
  151. package/dist/pages/PeoplePage.js.map +1 -1
  152. package/dist/pages/PersonDetail.d.ts.map +1 -1
  153. package/dist/pages/PersonDetail.js +5 -3
  154. package/dist/pages/PersonDetail.js.map +1 -1
  155. package/dist/pages/PlanningPage.d.ts.map +1 -1
  156. package/dist/pages/PlanningPage.js +85 -17
  157. package/dist/pages/PlanningPage.js.map +1 -1
  158. package/dist/pages/RaiseDetail.d.ts.map +1 -1
  159. package/dist/pages/RaiseDetail.js +3 -1
  160. package/dist/pages/RaiseDetail.js.map +1 -1
  161. package/dist/pages/RoleDetail.d.ts.map +1 -1
  162. package/dist/pages/RoleDetail.js +5 -4
  163. package/dist/pages/RoleDetail.js.map +1 -1
  164. package/dist/pages/admin/DataPage.d.ts.map +1 -1
  165. package/dist/pages/admin/DataPage.js +126 -16
  166. package/dist/pages/admin/DataPage.js.map +1 -1
  167. package/dist/pages/admin/WebhookDeliveries.d.ts.map +1 -1
  168. package/dist/pages/admin/WebhookDeliveries.js +3 -2
  169. package/dist/pages/admin/WebhookDeliveries.js.map +1 -1
  170. package/dist/pages/admin/WebhooksPage.d.ts.map +1 -1
  171. package/dist/pages/admin/WebhooksPage.js +3 -2
  172. package/dist/pages/admin/WebhooksPage.js.map +1 -1
  173. package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
  174. package/dist/pages/admin/WorkspaceSettingsPage.js +45 -2
  175. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  176. package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
  177. package/dist/pages/forms/SubmissionsTable.js +2 -1
  178. package/dist/pages/forms/SubmissionsTable.js.map +1 -1
  179. package/dist/pages/handbook/HandbookPage.js +1 -1
  180. package/dist/pages/hiringDirectory.js +2 -2
  181. package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
  182. package/dist/pages/onboarding/WorkspaceStepPage.js +15 -2
  183. package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
  184. package/dist/pages/positionDirectory.js +2 -2
  185. package/package.json +2 -2
  186. package/src/api/resource.ts +149 -31
  187. package/src/api/resources/companies.ts +7 -1
  188. package/src/api/resources/decisions.ts +3 -0
  189. package/src/api/resources/emailDomainLinker.ts +43 -0
  190. package/src/api/resources/forms.ts +5 -23
  191. package/src/api/resources/handbookPages.ts +1 -1
  192. package/src/api/resources/importJobs.ts +4 -0
  193. package/src/api/resources/listMembers.ts +173 -0
  194. package/src/api/resources/lists.ts +61 -0
  195. package/src/api/resources/people.ts +4 -1
  196. package/src/api/resources/publicConfig.ts +33 -0
  197. package/src/api/resources/roles.ts +3 -1
  198. package/src/api/resources/sampleData.ts +56 -0
  199. package/src/api/resources/webhooks.ts +8 -24
  200. package/src/app/KelpieApp.tsx +11 -0
  201. package/src/components/ActivitiesPanel.tsx +2 -10
  202. package/src/components/ColumnPicker.tsx +124 -0
  203. package/src/components/DataTable.tsx +89 -10
  204. package/src/components/DecisionsPanel.tsx +4 -12
  205. package/src/components/EntitySearch.tsx +12 -5
  206. package/src/components/EnvironmentBanner.tsx +30 -0
  207. package/src/components/ListsPanel.tsx +176 -0
  208. package/src/components/NotesPanel.tsx +6 -14
  209. package/src/components/Paginator.tsx +78 -0
  210. package/src/components/PlanPanel.tsx +2 -10
  211. package/src/components/Shell.tsx +1 -0
  212. package/src/index.ts +10 -1
  213. package/src/lib/listView.ts +149 -0
  214. package/src/lib/sort.ts +143 -0
  215. package/src/pages/CompaniesPage.tsx +125 -11
  216. package/src/pages/CompanyDetail.tsx +62 -62
  217. package/src/pages/DealDetail.tsx +3 -0
  218. package/src/pages/DealsPage.tsx +178 -24
  219. package/src/pages/DecisionsPage.tsx +73 -18
  220. package/src/pages/FormsPage.tsx +81 -17
  221. package/src/pages/FundraisingPage.tsx +152 -24
  222. package/src/pages/HiringPage.tsx +39 -11
  223. package/src/pages/ListDetail.tsx +408 -0
  224. package/src/pages/ListsPage.tsx +281 -0
  225. package/src/pages/OpportunitiesPage.tsx +130 -24
  226. package/src/pages/OpportunityDetail.tsx +3 -0
  227. package/src/pages/PartnershipDetail.tsx +3 -0
  228. package/src/pages/PartnershipsPage.tsx +149 -24
  229. package/src/pages/PeoplePage.tsx +137 -11
  230. package/src/pages/PersonDetail.tsx +5 -3
  231. package/src/pages/PlanningPage.tsx +256 -29
  232. package/src/pages/RaiseDetail.tsx +3 -0
  233. package/src/pages/RoleDetail.tsx +21 -26
  234. package/src/pages/admin/DataPage.tsx +359 -36
  235. package/src/pages/admin/WebhookDeliveries.tsx +10 -15
  236. package/src/pages/admin/WebhooksPage.tsx +14 -19
  237. package/src/pages/admin/WorkspaceSettingsPage.tsx +134 -2
  238. package/src/pages/forms/SubmissionsTable.tsx +3 -10
  239. package/src/pages/handbook/HandbookPage.tsx +1 -1
  240. package/src/pages/hiringDirectory.ts +2 -2
  241. package/src/pages/onboarding/WorkspaceStepPage.tsx +31 -3
  242. package/src/pages/positionDirectory.ts +2 -2
  243. package/src/styles.css +3 -3
@@ -1,16 +1,27 @@
1
- import { PIPELINE_KIND_LABELS, PIPELINE_KINDS, PLAN_ITEM_STATUS_LABELS } from '@kelpie/schemas'
2
- import type { PipelineKind, PlanItem } from '@kelpie/schemas'
1
+ import {
2
+ PIPELINE_KIND_LABELS,
3
+ PIPELINE_KINDS,
4
+ PLAN_ITEM_STATUS_LABELS,
5
+ PLAN_ITEM_STATUSES,
6
+ } from '@kelpie/schemas'
7
+ import type { PipelineKind, PlanItem, PlanItemStatus } from '@kelpie/schemas'
3
8
  import { useState } from 'react'
9
+ import type { FormEvent } from 'react'
4
10
 
5
11
  import { useDeals } from '../api/resources/deals.ts'
6
12
  import { useMembers } from '../api/resources/members.ts'
7
13
  import { useOpportunities } from '../api/resources/opportunities.ts'
8
14
  import { usePartnerships } from '../api/resources/partnerships.ts'
9
- import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
15
+ import {
16
+ MAX_PAGE_SIZE,
17
+ useCreatePlanItem,
18
+ usePlanItems,
19
+ } from '../api/resources/planItems.ts'
10
20
  import { useRaises } from '../api/resources/raises.ts'
11
21
  import { Chip } from '../components/Chip.tsx'
12
22
  import { PageHeader } from '../components/PageHeader.tsx'
13
23
  import { PlanTargetLink } from '../components/PlanAttention.tsx'
24
+ import { Paginator } from '../components/Paginator.tsx'
14
25
  import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
15
26
  import { SegmentedControl } from '../components/SegmentedControl.tsx'
16
27
  import { formatDay } from '../lib/dates.ts'
@@ -23,6 +34,9 @@ import { monthBounds, planStatusTone, todayIso } from '../lib/plan.ts'
23
34
  * upcoming and pages through it; the calendar wants one month, which is what
24
35
  * `?from=` and `?to=` are for, so moving between months is a request rather than
25
36
  * a filter over rows the page happens to hold.
37
+ *
38
+ * Adding a plan item lives here as well as on each record's Plan panel: the page
39
+ * is the workspace-wide view of the same resource, so create belongs here too.
26
40
  */
27
41
 
28
42
  type ViewMode = 'list' | 'calendar'
@@ -35,15 +49,27 @@ const CELL_ITEM_LIMIT = 3
35
49
 
36
50
  const MONTH_AND_YEAR = new Intl.DateTimeFormat(undefined, { month: 'long', year: 'numeric' })
37
51
 
52
+ const inputClass =
53
+ 'h-9 w-full rounded-md border border-border bg-surface-raised px-2.5 text-[13px] outline-none focus:border-accent'
54
+
55
+ interface PipelineTarget {
56
+ readonly id: string
57
+ readonly name: string
58
+ }
59
+
38
60
  export function PlanningPage(): React.JSX.Element {
39
61
  const [view, setView] = useState<ViewMode>('list')
40
62
  const [typeFilter, setTypeFilter] = useState<TypeFilter>('all')
63
+ const [adding, setAdding] = useState(false)
41
64
  const [month, setMonth] = useState(() => {
42
65
  const now = new Date()
43
66
 
44
67
  return { year: now.getFullYear(), month: now.getMonth() }
45
68
  })
46
69
 
70
+ const targets = usePipelineTargets()
71
+ const createItem = useCreatePlanItem()
72
+
47
73
  const typeQuery = typeFilter === 'all' ? {} : { targetType: typeFilter }
48
74
  // A month is a bounded question, so the calendar asks for it at the largest
49
75
  // page size and draws the answer whole. The list pages the ordinary way.
@@ -65,6 +91,10 @@ export function PlanningPage(): React.JSX.Element {
65
91
  <div className="animate-fade-in">
66
92
  <PageHeader
67
93
  title="Planning"
94
+ onAdd={() => {
95
+ setAdding((current) => !current)
96
+ }}
97
+ addLabel="Add plan item"
68
98
  actions={
69
99
  <>
70
100
  <label className="sr-only" htmlFor="plan-type-filter">
@@ -98,6 +128,33 @@ export function PlanningPage(): React.JSX.Element {
98
128
  }
99
129
  />
100
130
 
131
+ {adding && (
132
+ <AddPlanItemForm
133
+ initialTargetType={typeFilter === 'all' ? 'deal' : typeFilter}
134
+ targetsByKind={targets.byKind}
135
+ isPending={createItem.isPending}
136
+ error={createItem.error}
137
+ onCancel={() => {
138
+ setAdding(false)
139
+ }}
140
+ onSubmit={(fields) => {
141
+ createItem
142
+ .runAsync({
143
+ targetType: fields.targetType,
144
+ targetId: fields.targetId,
145
+ date: fields.date,
146
+ title: fields.title,
147
+ ownerId: fields.ownerId.length === 0 ? null : fields.ownerId,
148
+ status: fields.status,
149
+ })
150
+ .then(() => {
151
+ setAdding(false)
152
+ })
153
+ .catch(() => undefined)
154
+ }}
155
+ />
156
+ )}
157
+
101
158
  {view === 'calendar' && (
102
159
  <div className="mb-4 flex items-center gap-2">
103
160
  <button
@@ -131,47 +188,217 @@ export function PlanningPage(): React.JSX.Element {
131
188
  ) : items.isLoading ? (
132
189
  <LoadingPanel label="Loading plan…" />
133
190
  ) : view === 'list' ? (
134
- <PlanList items={items.records} />
191
+ <>
192
+ <Paginator list={items} placement="top" />
193
+ <PlanList items={items.records} targetNames={targets.nameById} />
194
+ </>
135
195
  ) : (
136
196
  <PlanCalendar year={month.year} month={month.month} items={items.records} />
137
197
  )}
138
198
 
139
- {items.hasMore && (
140
- <button
141
- type="button"
142
- onClick={items.loadMore}
143
- disabled={items.isLoadingMore}
144
- 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"
145
- >
146
- {items.isLoadingMore ? 'Loading…' : 'Load more'}
147
- </button>
148
- )}
199
+ {view === 'list' && <Paginator list={items} />}
149
200
  </div>
150
201
  )
151
202
  }
152
203
 
153
204
  /**
154
- * Names for the records the loaded items point at.
155
- *
156
- * The directory is one page per pipeline, matching the Deal contacts precedent:
157
- * past it a row still says what kind of record it belongs to.
205
+ * Pipeline records the add form can attach to, and the name map the list uses
206
+ * for PlanTargetLink. One page per kind, same ceiling as the rest of Planning.
158
207
  */
159
- function useTargetNames(): ReadonlyMap<string, string> {
160
- const deals = useDeals({ limit: 200 })
161
- const opportunities = useOpportunities({ limit: 200 })
162
- const raises = useRaises({ limit: 200 })
163
- const partnerships = usePartnerships({ limit: 200 })
164
-
165
- return new Map(
166
- [...deals.records, ...opportunities.records, ...raises.records, ...partnerships.records].map(
167
- (record) => [record.id, record.name],
208
+ function usePipelineTargets(): {
209
+ readonly byKind: Readonly<Record<PipelineKind, readonly PipelineTarget[]>>
210
+ readonly nameById: ReadonlyMap<string, string>
211
+ } {
212
+ const deals = useDeals({ limit: MAX_PAGE_SIZE })
213
+ const opportunities = useOpportunities({ limit: MAX_PAGE_SIZE })
214
+ const raises = useRaises({ limit: MAX_PAGE_SIZE })
215
+ const partnerships = usePartnerships({ limit: MAX_PAGE_SIZE })
216
+
217
+ const byKind: Readonly<Record<PipelineKind, readonly PipelineTarget[]>> = {
218
+ deal: deals.records.map((record) => ({ id: record.id, name: record.name })),
219
+ opportunity: opportunities.records.map((record) => ({ id: record.id, name: record.name })),
220
+ raise: raises.records.map((record) => ({ id: record.id, name: record.name })),
221
+ partnership: partnerships.records.map((record) => ({ id: record.id, name: record.name })),
222
+ }
223
+
224
+ return {
225
+ byKind,
226
+ nameById: new Map(
227
+ Object.values(byKind)
228
+ .flat()
229
+ .map((record) => [record.id, record.name]),
168
230
  ),
231
+ }
232
+ }
233
+
234
+ interface AddPlanItemFields {
235
+ readonly targetType: PipelineKind
236
+ readonly targetId: string
237
+ readonly date: string
238
+ readonly title: string
239
+ readonly ownerId: string
240
+ readonly status: PlanItemStatus
241
+ }
242
+
243
+ function AddPlanItemForm({
244
+ initialTargetType,
245
+ targetsByKind,
246
+ isPending,
247
+ error,
248
+ onSubmit,
249
+ onCancel,
250
+ }: {
251
+ readonly initialTargetType: PipelineKind
252
+ readonly targetsByKind: Readonly<Record<PipelineKind, readonly PipelineTarget[]>>
253
+ readonly isPending: boolean
254
+ readonly error: Error | null
255
+ readonly onSubmit: (fields: AddPlanItemFields) => void
256
+ readonly onCancel: () => void
257
+ }): React.JSX.Element {
258
+ const members = useMembers()
259
+ const [draft, setDraft] = useState<AddPlanItemFields>({
260
+ targetType: initialTargetType,
261
+ targetId: '',
262
+ date: todayIso(),
263
+ title: '',
264
+ ownerId: '',
265
+ status: 'todo',
266
+ })
267
+
268
+ const targets = targetsByKind[draft.targetType]
269
+
270
+ function submit(event: FormEvent): void {
271
+ event.preventDefault()
272
+
273
+ const title = draft.title.trim()
274
+
275
+ if (title.length === 0 || draft.date.length === 0 || draft.targetId.length === 0) {
276
+ return
277
+ }
278
+
279
+ onSubmit({ ...draft, title })
280
+ }
281
+
282
+ return (
283
+ <form onSubmit={submit} className="mb-4 space-y-3 rounded-md border border-border p-3">
284
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-[11rem_minmax(0,1fr)]">
285
+ <select
286
+ value={draft.targetType}
287
+ onChange={(event) => {
288
+ const targetType = event.target.value as PipelineKind
289
+
290
+ setDraft((current) => ({ ...current, targetType, targetId: '' }))
291
+ }}
292
+ aria-label="Record type"
293
+ className={inputClass}
294
+ >
295
+ {PIPELINE_KINDS.map((kind) => (
296
+ <option key={kind} value={kind}>
297
+ {PIPELINE_KIND_LABELS[kind]}
298
+ </option>
299
+ ))}
300
+ </select>
301
+ <select
302
+ value={draft.targetId}
303
+ onChange={(event) => {
304
+ setDraft((current) => ({ ...current, targetId: event.target.value }))
305
+ }}
306
+ required
307
+ aria-label="Record"
308
+ className={inputClass}
309
+ >
310
+ <option value="">{targets.length === 0 ? 'No records' : 'Select…'}</option>
311
+ {targets.map((target) => (
312
+ <option key={target.id} value={target.id}>
313
+ {target.name}
314
+ </option>
315
+ ))}
316
+ </select>
317
+
318
+ <input
319
+ type="date"
320
+ value={draft.date}
321
+ onChange={(event) => {
322
+ setDraft((current) => ({ ...current, date: event.target.value }))
323
+ }}
324
+ required
325
+ aria-label="Date"
326
+ className={inputClass}
327
+ />
328
+ <input
329
+ value={draft.title}
330
+ onChange={(event) => {
331
+ setDraft((current) => ({ ...current, title: event.target.value }))
332
+ }}
333
+ placeholder="What happens…"
334
+ required
335
+ autoFocus
336
+ aria-label="What happens"
337
+ className={inputClass}
338
+ />
339
+
340
+ <select
341
+ value={draft.ownerId}
342
+ onChange={(event) => {
343
+ setDraft((current) => ({ ...current, ownerId: event.target.value }))
344
+ }}
345
+ aria-label="Owner"
346
+ className={inputClass}
347
+ >
348
+ <option value="">Unassigned</option>
349
+ {members.members.map((member) => (
350
+ <option key={member.id} value={member.id}>
351
+ {member.name}
352
+ </option>
353
+ ))}
354
+ </select>
355
+ <select
356
+ value={draft.status}
357
+ onChange={(event) => {
358
+ setDraft((current) => ({
359
+ ...current,
360
+ status: event.target.value as PlanItemStatus,
361
+ }))
362
+ }}
363
+ aria-label="Status"
364
+ className={inputClass}
365
+ >
366
+ {PLAN_ITEM_STATUSES.map((status) => (
367
+ <option key={status} value={status}>
368
+ {PLAN_ITEM_STATUS_LABELS[status]}
369
+ </option>
370
+ ))}
371
+ </select>
372
+ </div>
373
+ {error !== null && <ErrorPanel error={error} />}
374
+ <div className="flex justify-end gap-2">
375
+ <button
376
+ type="button"
377
+ onClick={onCancel}
378
+ className="rounded-md px-2.5 py-1.5 text-[12px] font-medium text-ink-muted hover:text-ink"
379
+ >
380
+ Cancel
381
+ </button>
382
+ <button
383
+ type="submit"
384
+ disabled={isPending || targets.length === 0}
385
+ className="h-9 rounded-md bg-accent px-3 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
386
+ >
387
+ {isPending ? 'Adding…' : 'Add'}
388
+ </button>
389
+ </div>
390
+ </form>
169
391
  )
170
392
  }
171
393
 
172
- function PlanList({ items }: { readonly items: readonly PlanItem[] }): React.JSX.Element {
394
+ function PlanList({
395
+ items,
396
+ targetNames,
397
+ }: {
398
+ readonly items: readonly PlanItem[]
399
+ readonly targetNames: ReadonlyMap<string, string>
400
+ }): React.JSX.Element {
173
401
  const members = useMembers()
174
- const targetNames = useTargetNames()
175
402
 
176
403
  if (items.length === 0) {
177
404
  return <p className="text-[13px] text-ink-faint">No plan items match this filter.</p>
@@ -19,6 +19,7 @@ import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
19
19
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
20
20
  import { EntitySearch } from '../components/EntitySearch.tsx'
21
21
  import { InlineEdit } from '../components/InlineEdit.tsx'
22
+ import { ListsPanel } from '../components/ListsPanel.tsx'
22
23
  import { NotesPanel } from '../components/NotesPanel.tsx'
23
24
  import { PlanAttention } from '../components/PlanAttention.tsx'
24
25
  import { PlanPanel } from '../components/PlanPanel.tsx'
@@ -79,6 +80,7 @@ export function RaiseDetail(): React.JSX.Element {
79
80
  { id: 'activity', label: 'Activity' },
80
81
  { id: 'notes', label: 'Notes' },
81
82
  { id: 'decisions', label: 'Decisions' },
83
+ { id: 'lists', label: 'Lists' },
82
84
  ...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
83
85
  ]
84
86
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
@@ -119,6 +121,7 @@ export function RaiseDetail(): React.JSX.Element {
119
121
  {active === 'activity' && <ActivitiesPanel targetType="raise" targetId={record.id} />}
120
122
  {active === 'notes' && <NotesPanel targetType="raise" targetId={record.id} />}
121
123
  {active === 'decisions' && <DecisionsPanel targetType="raise" targetId={record.id} />}
124
+ {active === 'lists' && <ListsPanel targetType="raise" targetId={record.id} />}
122
125
  {moduleTab?.render({ objectType: 'raise', recordId: record.id })}
123
126
  </RecordTabs>
124
127
  </div>
@@ -15,6 +15,7 @@ import { Chip } from '../components/Chip.tsx'
15
15
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
16
16
  import { EntitySearch } from '../components/EntitySearch.tsx'
17
17
  import { InlineEdit } from '../components/InlineEdit.tsx'
18
+ import { Paginator } from '../components/Paginator.tsx'
18
19
  import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
19
20
  import { SectionHeader } from '../components/SectionHeader.tsx'
20
21
  import {
@@ -240,34 +241,28 @@ function RoleCandidates({ role }: { readonly role: Role }): React.JSX.Element {
240
241
  ) : candidates.records.length === 0 && !adding ? (
241
242
  <p className="text-[13px] text-ink-faint">No candidates yet.</p>
242
243
  ) : (
243
- <ul className="overflow-hidden rounded-md border border-border">
244
- {candidates.records.map((candidate) => (
245
- <CandidateRow
246
- key={candidate.id}
247
- candidate={candidate}
248
- personName={names.nameFor(candidate.personId)}
249
- referrerName={
250
- candidate.referrerPersonId === null
251
- ? undefined
252
- : names.nameFor(candidate.referrerPersonId)
253
- }
254
- note={notes.noteFor(candidate.id)}
255
- noteResolved={notes.isComplete || notes.noteFor(candidate.id) !== undefined}
256
- />
257
- ))}
258
- </ul>
244
+ <>
245
+ <Paginator list={candidates} placement="top" />
246
+ <ul className="overflow-hidden rounded-md border border-border">
247
+ {candidates.records.map((candidate) => (
248
+ <CandidateRow
249
+ key={candidate.id}
250
+ candidate={candidate}
251
+ personName={names.nameFor(candidate.personId)}
252
+ referrerName={
253
+ candidate.referrerPersonId === null
254
+ ? undefined
255
+ : names.nameFor(candidate.referrerPersonId)
256
+ }
257
+ note={notes.noteFor(candidate.id)}
258
+ noteResolved={notes.isComplete || notes.noteFor(candidate.id) !== undefined}
259
+ />
260
+ ))}
261
+ </ul>
262
+ </>
259
263
  )}
260
264
 
261
- {candidates.hasMore && (
262
- <button
263
- type="button"
264
- onClick={candidates.loadMore}
265
- disabled={candidates.isLoadingMore}
266
- 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"
267
- >
268
- {candidates.isLoadingMore ? 'Loading…' : 'Load more'}
269
- </button>
270
- )}
265
+ <Paginator list={candidates} />
271
266
  </section>
272
267
  )
273
268
  }