@kelpie/ui 0.8.0 → 0.10.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.
- package/dist/api/resource.d.ts +1 -1
- package/dist/api/resource.d.ts.map +1 -1
- package/dist/api/resource.js.map +1 -1
- package/dist/api/resources/consentPurposes.d.ts +13 -0
- package/dist/api/resources/consentPurposes.d.ts.map +1 -0
- package/dist/api/resources/consentPurposes.js +39 -0
- package/dist/api/resources/consentPurposes.js.map +1 -0
- package/dist/api/resources/customFields.d.ts +15 -0
- package/dist/api/resources/customFields.d.ts.map +1 -0
- package/dist/api/resources/customFields.js +41 -0
- package/dist/api/resources/customFields.js.map +1 -0
- package/dist/api/resources/enquiries.d.ts +39 -0
- package/dist/api/resources/enquiries.d.ts.map +1 -0
- package/dist/api/resources/enquiries.js +68 -0
- package/dist/api/resources/enquiries.js.map +1 -0
- package/dist/api/resources/forms.d.ts +21 -1
- package/dist/api/resources/forms.d.ts.map +1 -1
- package/dist/api/resources/forms.js +41 -0
- package/dist/api/resources/forms.js.map +1 -1
- package/dist/api/resources/importJobs.d.ts +5 -0
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js +3 -0
- package/dist/api/resources/importJobs.js.map +1 -1
- package/dist/api/resources/opportunities.d.ts +2 -0
- package/dist/api/resources/opportunities.d.ts.map +1 -1
- package/dist/api/resources/opportunities.js +1 -0
- package/dist/api/resources/opportunities.js.map +1 -1
- package/dist/api/resources/sampleData.d.ts.map +1 -1
- package/dist/api/resources/sampleData.js +1 -0
- package/dist/api/resources/sampleData.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +7 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +1 -0
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/CustomFieldsPanel.d.ts +23 -0
- package/dist/components/CustomFieldsPanel.d.ts.map +1 -0
- package/dist/components/CustomFieldsPanel.js +116 -0
- package/dist/components/CustomFieldsPanel.js.map +1 -0
- package/dist/components/FormsPanel.d.ts +18 -0
- package/dist/components/FormsPanel.d.ts.map +1 -0
- package/dist/components/FormsPanel.js +37 -0
- package/dist/components/FormsPanel.js.map +1 -0
- package/dist/components/PlanAttention.d.ts +3 -2
- package/dist/components/PlanAttention.d.ts.map +1 -1
- package/dist/components/PlanAttention.js +7 -3
- package/dist/components/PlanAttention.js.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +3 -0
- package/dist/components/Shell.js.map +1 -1
- package/dist/components/useHasCustomFields.d.ts +13 -0
- package/dist/components/useHasCustomFields.d.ts.map +1 -0
- package/dist/components/useHasCustomFields.js +16 -0
- package/dist/components/useHasCustomFields.js.map +1 -0
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +24 -2
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +18 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +2 -0
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiriesPage.d.ts +2 -0
- package/dist/pages/EnquiriesPage.d.ts.map +1 -0
- package/dist/pages/EnquiriesPage.js +251 -0
- package/dist/pages/EnquiriesPage.js.map +1 -0
- package/dist/pages/EnquiryDetail.d.ts +2 -0
- package/dist/pages/EnquiryDetail.d.ts.map +1 -0
- package/dist/pages/EnquiryDetail.js +207 -0
- package/dist/pages/EnquiryDetail.js.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts +3 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.js +34 -0
- package/dist/pages/EnquiryStageSettingsPage.js.map +1 -0
- package/dist/pages/FormDetail.d.ts.map +1 -1
- package/dist/pages/FormDetail.js +19 -1
- package/dist/pages/FormDetail.js.map +1 -1
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +27 -1
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +62 -3
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +17 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +17 -0
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +63 -3
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +3 -0
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +11 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.js +1 -1
- package/dist/pages/RoleDetail.js.map +1 -1
- package/dist/pages/SearchPage.d.ts.map +1 -1
- package/dist/pages/SearchPage.js +1 -0
- package/dist/pages/SearchPage.js.map +1 -1
- package/dist/pages/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +18 -3
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/FieldsPage.d.ts +2 -0
- package/dist/pages/admin/FieldsPage.d.ts.map +1 -0
- package/dist/pages/admin/FieldsPage.js +164 -0
- package/dist/pages/admin/FieldsPage.js.map +1 -0
- package/dist/pages/admin/PrivacyPage.d.ts +9 -0
- package/dist/pages/admin/PrivacyPage.d.ts.map +1 -0
- package/dist/pages/admin/PrivacyPage.js +170 -0
- package/dist/pages/admin/PrivacyPage.js.map +1 -0
- package/dist/pages/dashboard/attention.d.ts.map +1 -1
- package/dist/pages/dashboard/attention.js +3 -0
- package/dist/pages/dashboard/attention.js.map +1 -1
- package/dist/pages/forms/AddFieldMenu.d.ts +17 -0
- package/dist/pages/forms/AddFieldMenu.d.ts.map +1 -0
- package/dist/pages/forms/AddFieldMenu.js +43 -0
- package/dist/pages/forms/AddFieldMenu.js.map +1 -0
- package/dist/pages/forms/EmbedPanel.d.ts +4 -0
- package/dist/pages/forms/EmbedPanel.d.ts.map +1 -1
- package/dist/pages/forms/EmbedPanel.js +62 -3
- package/dist/pages/forms/EmbedPanel.js.map +1 -1
- package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
- package/dist/pages/forms/FieldsEditor.js +179 -36
- package/dist/pages/forms/FieldsEditor.js.map +1 -1
- package/dist/pages/forms/FormActions.d.ts +6 -0
- package/dist/pages/forms/FormActions.d.ts.map +1 -0
- package/dist/pages/forms/FormActions.js +149 -0
- package/dist/pages/forms/FormActions.js.map +1 -0
- package/dist/pages/forms/FormSettings.d.ts.map +1 -1
- package/dist/pages/forms/FormSettings.js +15 -19
- package/dist/pages/forms/FormSettings.js.map +1 -1
- package/dist/pages/forms/SubmissionDetail.d.ts +2 -0
- package/dist/pages/forms/SubmissionDetail.d.ts.map +1 -0
- package/dist/pages/forms/SubmissionDetail.js +76 -0
- package/dist/pages/forms/SubmissionDetail.js.map +1 -0
- package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
- package/dist/pages/forms/SubmissionsTable.js +56 -8
- package/dist/pages/forms/SubmissionsTable.js.map +1 -1
- package/dist/pages/forms/fieldList.d.ts +11 -5
- package/dist/pages/forms/fieldList.d.ts.map +1 -1
- package/dist/pages/forms/fieldList.js +66 -6
- package/dist/pages/forms/fieldList.js.map +1 -1
- package/dist/pages/forms/template.d.ts +17 -0
- package/dist/pages/forms/template.d.ts.map +1 -1
- package/dist/pages/forms/template.js +93 -0
- package/dist/pages/forms/template.js.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/package.json +2 -2
- package/src/api/resource.ts +6 -1
- package/src/api/resources/consentPurposes.ts +83 -0
- package/src/api/resources/customFields.ts +88 -0
- package/src/api/resources/enquiries.ts +118 -0
- package/src/api/resources/forms.ts +58 -0
- package/src/api/resources/importJobs.ts +9 -0
- package/src/api/resources/opportunities.ts +3 -0
- package/src/api/resources/sampleData.ts +1 -0
- package/src/app/KelpieApp.tsx +12 -0
- package/src/components/ActivitiesPanel.tsx +1 -0
- package/src/components/CustomFieldsPanel.tsx +330 -0
- package/src/components/FormsPanel.tsx +86 -0
- package/src/components/PlanAttention.tsx +20 -3
- package/src/components/Shell.tsx +3 -0
- package/src/components/useHasCustomFields.ts +18 -0
- package/src/pages/CompanyDetail.tsx +36 -1
- package/src/pages/DealDetail.tsx +31 -0
- package/src/pages/DecisionsPage.tsx +2 -0
- package/src/pages/EnquiriesPage.tsx +434 -0
- package/src/pages/EnquiryDetail.tsx +554 -0
- package/src/pages/EnquiryStageSettingsPage.tsx +63 -0
- package/src/pages/FormDetail.tsx +23 -1
- package/src/pages/ListDetail.tsx +62 -0
- package/src/pages/OpportunityDetail.tsx +165 -2
- package/src/pages/PartnershipDetail.tsx +32 -0
- package/src/pages/PeoplePage.tsx +20 -0
- package/src/pages/PersonDetail.tsx +152 -3
- package/src/pages/PlanningPage.tsx +3 -0
- package/src/pages/RaiseDetail.tsx +21 -0
- package/src/pages/RoleDetail.tsx +1 -1
- package/src/pages/SearchPage.tsx +1 -0
- package/src/pages/admin/DataPage.tsx +51 -3
- package/src/pages/admin/FieldsPage.tsx +455 -0
- package/src/pages/admin/PrivacyPage.tsx +461 -0
- package/src/pages/dashboard/attention.ts +3 -0
- package/src/pages/forms/AddFieldMenu.tsx +121 -0
- package/src/pages/forms/EmbedPanel.tsx +163 -2
- package/src/pages/forms/FieldsEditor.tsx +539 -165
- package/src/pages/forms/FormActions.tsx +552 -0
- package/src/pages/forms/FormSettings.tsx +29 -70
- package/src/pages/forms/SubmissionDetail.tsx +249 -0
- package/src/pages/forms/SubmissionsTable.tsx +109 -16
- package/src/pages/forms/fieldList.ts +68 -5
- package/src/pages/forms/template.ts +103 -0
- package/src/pages/onboarding/WorkspaceStepPage.tsx +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { IN_PROCESS, PREFERRED_CHANNELS } from '@kelpie/schemas'
|
|
2
|
-
import type {
|
|
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,8 +29,11 @@ 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'
|
|
@@ -70,10 +81,12 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
70
81
|
const { record, isLoading, isNotFound, error } = usePerson(id)
|
|
71
82
|
const deletePerson = useDeletePerson()
|
|
72
83
|
const moduleTabs = inSlotOrder(useRecordTabs('person'))
|
|
84
|
+
const hasCustomFields = useHasCustomFields('person')
|
|
73
85
|
const [activeTab, setActiveTab] = useState('overview')
|
|
74
86
|
const candidacies = useCandidates({ personIds: id === undefined ? [] : [id] }, {
|
|
75
87
|
enabled: id !== undefined,
|
|
76
88
|
})
|
|
89
|
+
const formSubmissions = useFormSubmissionsForRecord('person', id)
|
|
77
90
|
|
|
78
91
|
if (isNotFound) {
|
|
79
92
|
return <NotFoundPanel label="Person" backTo="/people" />
|
|
@@ -89,6 +102,7 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
89
102
|
|
|
90
103
|
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
91
104
|
{ id: 'overview', label: 'Overview' },
|
|
105
|
+
...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
|
|
92
106
|
{ id: 'activity', label: 'Activity' },
|
|
93
107
|
...(candidacies.records.length === 0
|
|
94
108
|
? []
|
|
@@ -96,6 +110,9 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
96
110
|
{ id: 'notes', label: 'Notes' },
|
|
97
111
|
{ id: 'decisions', label: 'Decisions' },
|
|
98
112
|
{ id: 'lists', label: 'Lists' },
|
|
113
|
+
...(formSubmissions.records.length === 0
|
|
114
|
+
? []
|
|
115
|
+
: [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
|
|
99
116
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
100
117
|
]
|
|
101
118
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -132,6 +149,7 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
132
149
|
|
|
133
150
|
<RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Person sections">
|
|
134
151
|
{active === 'overview' && <PersonOverview person={record} />}
|
|
152
|
+
{active === 'fields' && <PersonFields person={record} />}
|
|
135
153
|
{active === 'activity' && <ActivitiesPanel targetType="person" targetId={record.id} />}
|
|
136
154
|
{active === 'hiring' && (
|
|
137
155
|
<PersonHiring candidacies={candidacies.records} personName={record.name} />
|
|
@@ -139,12 +157,15 @@ export function PersonDetail(): React.JSX.Element {
|
|
|
139
157
|
{active === 'notes' && <NotesPanel targetType="person" targetId={record.id} />}
|
|
140
158
|
{active === 'decisions' && <DecisionsPanel targetType="person" targetId={record.id} />}
|
|
141
159
|
{active === 'lists' && <ListsPanel targetType="person" targetId={record.id} />}
|
|
160
|
+
{active === 'forms' && <FormsPanel targetType="person" targetId={record.id} />}
|
|
142
161
|
{moduleTab?.render({ objectType: 'person', recordId: record.id })}
|
|
143
162
|
</RecordTabs>
|
|
144
163
|
</div>
|
|
145
164
|
|
|
146
165
|
<aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
|
|
166
|
+
<PersonNameParts person={record} />
|
|
147
167
|
<PersonSidebar person={record} />
|
|
168
|
+
<PersonConsents person={record} />
|
|
148
169
|
<PersonPositions person={record} />
|
|
149
170
|
</aside>
|
|
150
171
|
</div>
|
|
@@ -200,6 +221,7 @@ function PersonOverview({ person }: { readonly person: Person }): React.JSX.Elem
|
|
|
200
221
|
const { patch, error } = usePersonPatch(person)
|
|
201
222
|
const deals = useDeals({ personIds: [person.id] })
|
|
202
223
|
const partnerships = usePartnerships({ personIds: [person.id] })
|
|
224
|
+
const enquiries = useEnquiries({ personIds: [person.id] })
|
|
203
225
|
|
|
204
226
|
return (
|
|
205
227
|
<div className="space-y-8">
|
|
@@ -213,7 +235,8 @@ function PersonOverview({ person }: { readonly person: Person }): React.JSX.Elem
|
|
|
213
235
|
<RelatedPlanAttention
|
|
214
236
|
deals={deals.records}
|
|
215
237
|
partnerships={partnerships.records}
|
|
216
|
-
|
|
238
|
+
enquiries={enquiries.records}
|
|
239
|
+
isLoading={deals.isLoading || partnerships.isLoading || enquiries.isLoading}
|
|
217
240
|
/>
|
|
218
241
|
<LatestActivity targetType="person" targetId={person.id} />
|
|
219
242
|
</div>
|
|
@@ -300,6 +323,46 @@ function PersonHiring({
|
|
|
300
323
|
)
|
|
301
324
|
}
|
|
302
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
|
+
|
|
303
366
|
function PersonSidebar({ person }: { readonly person: Person }): React.JSX.Element {
|
|
304
367
|
const { patch, error } = usePersonPatch(person)
|
|
305
368
|
|
|
@@ -379,6 +442,22 @@ function PersonSidebar({ person }: { readonly person: Person }): React.JSX.Eleme
|
|
|
379
442
|
)
|
|
380
443
|
}
|
|
381
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
|
+
|
|
382
461
|
/**
|
|
383
462
|
* The companies this person holds a title at.
|
|
384
463
|
*
|
|
@@ -526,3 +605,73 @@ function PersonPositions({ person }: { readonly person: Person }): React.JSX.Ele
|
|
|
526
605
|
</section>
|
|
527
606
|
)
|
|
528
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
|
+
}
|
|
@@ -9,6 +9,7 @@ import { useState } from 'react'
|
|
|
9
9
|
import type { FormEvent } from 'react'
|
|
10
10
|
|
|
11
11
|
import { useDeals } from '../api/resources/deals.ts'
|
|
12
|
+
import { useEnquiries } from '../api/resources/enquiries.ts'
|
|
12
13
|
import { useMembers } from '../api/resources/members.ts'
|
|
13
14
|
import { useOpportunities } from '../api/resources/opportunities.ts'
|
|
14
15
|
import { usePartnerships } from '../api/resources/partnerships.ts'
|
|
@@ -213,8 +214,10 @@ function usePipelineTargets(): {
|
|
|
213
214
|
const opportunities = useOpportunities({ limit: MAX_PAGE_SIZE })
|
|
214
215
|
const raises = useRaises({ limit: MAX_PAGE_SIZE })
|
|
215
216
|
const partnerships = usePartnerships({ limit: MAX_PAGE_SIZE })
|
|
217
|
+
const enquiries = useEnquiries({ limit: MAX_PAGE_SIZE })
|
|
216
218
|
|
|
217
219
|
const byKind: Readonly<Record<PipelineKind, readonly PipelineTarget[]>> = {
|
|
220
|
+
enquiry: enquiries.records.map((record) => ({ id: record.id, name: record.name })),
|
|
218
221
|
deal: deals.records.map((record) => ({ id: record.id, name: record.name })),
|
|
219
222
|
opportunity: opportunities.records.map((record) => ({ id: record.id, name: record.name })),
|
|
220
223
|
raise: raises.records.map((record) => ({ id: record.id, name: record.name })),
|
|
@@ -18,6 +18,8 @@ import type { ChipTone } from '../components/Chip.tsx'
|
|
|
18
18
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
19
19
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
20
20
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
21
|
+
import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
|
|
22
|
+
import { useHasCustomFields } from '../components/useHasCustomFields.ts'
|
|
21
23
|
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
22
24
|
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
23
25
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
@@ -60,6 +62,7 @@ export function RaiseDetail(): React.JSX.Element {
|
|
|
60
62
|
const { record, isLoading, isNotFound, error } = useRaise(id)
|
|
61
63
|
const deleteRaise = useDeleteRaise()
|
|
62
64
|
const moduleTabs = inSlotOrder(useRecordTabs('raise'))
|
|
65
|
+
const hasCustomFields = useHasCustomFields('raise')
|
|
63
66
|
const [activeTab, setActiveTab] = useState('overview')
|
|
64
67
|
|
|
65
68
|
if (isNotFound) {
|
|
@@ -76,6 +79,7 @@ export function RaiseDetail(): React.JSX.Element {
|
|
|
76
79
|
|
|
77
80
|
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
78
81
|
{ id: 'overview', label: 'Overview' },
|
|
82
|
+
...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
|
|
79
83
|
{ id: 'plan', label: 'Plan' },
|
|
80
84
|
{ id: 'activity', label: 'Activity' },
|
|
81
85
|
{ id: 'notes', label: 'Notes' },
|
|
@@ -117,6 +121,7 @@ export function RaiseDetail(): React.JSX.Element {
|
|
|
117
121
|
|
|
118
122
|
<RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Raise sections">
|
|
119
123
|
{active === 'overview' && <RaiseOverview raise={record} />}
|
|
124
|
+
{active === 'fields' && <RaiseFields raise={record} />}
|
|
120
125
|
{active === 'plan' && <PlanPanel targetType="raise" targetId={record.id} />}
|
|
121
126
|
{active === 'activity' && <ActivitiesPanel targetType="raise" targetId={record.id} />}
|
|
122
127
|
{active === 'notes' && <NotesPanel targetType="raise" targetId={record.id} />}
|
|
@@ -384,6 +389,22 @@ function RaiseSidebar({ raise }: { readonly raise: Raise }): React.JSX.Element {
|
|
|
384
389
|
)
|
|
385
390
|
}
|
|
386
391
|
|
|
392
|
+
function RaiseFields({ raise }: { readonly raise: Raise }): React.JSX.Element {
|
|
393
|
+
const { patch, error } = useRaisePatch(raise)
|
|
394
|
+
return (
|
|
395
|
+
<div className="space-y-4">
|
|
396
|
+
{error !== null && <ErrorPanel error={error} />}
|
|
397
|
+
<CustomFieldsPanel
|
|
398
|
+
objectType="raise"
|
|
399
|
+
values={raise.customFields}
|
|
400
|
+
onPatch={(customFields) => {
|
|
401
|
+
patch({ customFields })
|
|
402
|
+
}}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
387
408
|
/** The key people on the raise, linked and unlinked by replacing `person_ids`. */
|
|
388
409
|
function RaiseKeyPeople({ raise }: { readonly raise: Raise }): React.JSX.Element {
|
|
389
410
|
const { patch, error } = useRaisePatch(raise)
|
package/src/pages/RoleDetail.tsx
CHANGED
|
@@ -243,7 +243,7 @@ function RoleCandidates({ role }: { readonly role: Role }): React.JSX.Element {
|
|
|
243
243
|
) : (
|
|
244
244
|
<>
|
|
245
245
|
<Paginator list={candidates} placement="top" />
|
|
246
|
-
<ul className="
|
|
246
|
+
<ul className="rounded-md border border-border">
|
|
247
247
|
{candidates.records.map((candidate) => (
|
|
248
248
|
<CandidateRow
|
|
249
249
|
key={candidate.id}
|
package/src/pages/SearchPage.tsx
CHANGED
|
@@ -19,6 +19,7 @@ const COLLECTIONS: Readonly<Record<SearchCollection, { label: string; path: (id:
|
|
|
19
19
|
person: { label: 'People', path: (id) => `/people/${id}` },
|
|
20
20
|
role: { label: 'Roles', path: (id) => `/hiring/${id}` },
|
|
21
21
|
company: { label: 'Companies', path: (id) => `/companies/${id}` },
|
|
22
|
+
enquiry: { label: 'Enquiries', path: (id) => `/enquiries/${id}` },
|
|
22
23
|
deal: { label: 'Deals', path: (id) => `/deals/${id}` },
|
|
23
24
|
opportunity: { label: 'Opportunities', path: (id) => `/opportunities/${id}` },
|
|
24
25
|
raise: { label: 'Fundraising', path: (id) => `/fundraising/${id}` },
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
import { useEffect, useState } from 'react'
|
|
28
28
|
import type { ChangeEvent, DragEvent, FormEvent, ReactNode } from 'react'
|
|
29
29
|
|
|
30
|
+
import { useConsentPurposes } from '../../api/resources/consentPurposes.ts'
|
|
30
31
|
import { useRelinkEmailDomains } from '../../api/resources/emailDomainLinker.ts'
|
|
31
32
|
import {
|
|
32
33
|
saveCsv,
|
|
@@ -73,6 +74,7 @@ export function DataPage(): ReactNode {
|
|
|
73
74
|
const [columnMap, setColumnMap] = useState<ImportColumnMap>({})
|
|
74
75
|
const [conflictMode, setConflictMode] = useState<ImportConflictMode>('skip')
|
|
75
76
|
const [onMissingCompany, setOnMissingCompany] = useState<OnMissingCompany>('skip')
|
|
77
|
+
const [consentPurposeId, setConsentPurposeId] = useState<string | null>(null)
|
|
76
78
|
const [matchKeyId, setMatchKeyId] = useState<string>(() => defaultMatchKeyId('companies'))
|
|
77
79
|
const [jobId, setJobId] = useState<string | undefined>(undefined)
|
|
78
80
|
const [problem, setProblem] = useState<string | null>(null)
|
|
@@ -125,6 +127,7 @@ export function DataPage(): ReactNode {
|
|
|
125
127
|
setColumnMap({})
|
|
126
128
|
setConflictMode('skip')
|
|
127
129
|
setOnMissingCompany('skip')
|
|
130
|
+
setConsentPurposeId(null)
|
|
128
131
|
setMatchKeyId(defaultMatchKeyId(object))
|
|
129
132
|
setJobId(undefined)
|
|
130
133
|
setProblem(null)
|
|
@@ -145,6 +148,7 @@ export function DataPage(): ReactNode {
|
|
|
145
148
|
conflictMode,
|
|
146
149
|
onMissingCompany,
|
|
147
150
|
matchKeyId,
|
|
151
|
+
consentPurposeId,
|
|
148
152
|
...(map === undefined ? {} : { columnMap: map }),
|
|
149
153
|
})
|
|
150
154
|
|
|
@@ -428,11 +432,13 @@ export function DataPage(): ReactNode {
|
|
|
428
432
|
columnMap={columnMap}
|
|
429
433
|
conflictMode={conflictMode}
|
|
430
434
|
onMissingCompany={onMissingCompany}
|
|
435
|
+
consentPurposeId={consentPurposeId}
|
|
431
436
|
matchKeyId={matchKeyId}
|
|
432
437
|
isPending={createJob.isPending}
|
|
433
438
|
onColumnMapChange={setColumnMap}
|
|
434
439
|
onConflictModeChange={setConflictMode}
|
|
435
440
|
onMissingCompanyChange={setOnMissingCompany}
|
|
441
|
+
onConsentPurposeChange={setConsentPurposeId}
|
|
436
442
|
onMatchKeyChange={setMatchKeyId}
|
|
437
443
|
onSubmit={(event) => runStep(onRerun(event))}
|
|
438
444
|
onBack={() => {
|
|
@@ -468,11 +474,13 @@ interface MappingFormProps {
|
|
|
468
474
|
readonly columnMap: ImportColumnMap
|
|
469
475
|
readonly conflictMode: ImportConflictMode
|
|
470
476
|
readonly onMissingCompany: OnMissingCompany
|
|
477
|
+
readonly consentPurposeId: string | null
|
|
471
478
|
readonly matchKeyId: string
|
|
472
479
|
readonly isPending: boolean
|
|
473
480
|
readonly onColumnMapChange: (map: ImportColumnMap) => void
|
|
474
481
|
readonly onConflictModeChange: (mode: ImportConflictMode) => void
|
|
475
482
|
readonly onMissingCompanyChange: (mode: OnMissingCompany) => void
|
|
483
|
+
readonly onConsentPurposeChange: (id: string | null) => void
|
|
476
484
|
readonly onMatchKeyChange: (id: string) => void
|
|
477
485
|
readonly onSubmit: (event: FormEvent) => void
|
|
478
486
|
readonly onBack: () => void
|
|
@@ -535,6 +543,12 @@ function MappingForm(props: MappingFormProps): ReactNode {
|
|
|
535
543
|
</select>
|
|
536
544
|
</Field>
|
|
537
545
|
) : null}
|
|
546
|
+
{showMissingCompany ? (
|
|
547
|
+
<ConsentPurposeField
|
|
548
|
+
value={props.consentPurposeId}
|
|
549
|
+
onChange={props.onConsentPurposeChange}
|
|
550
|
+
/>
|
|
551
|
+
) : null}
|
|
538
552
|
</div>
|
|
539
553
|
<p className="text-[12px] text-ink-muted">
|
|
540
554
|
{props.conflictMode === 'update'
|
|
@@ -954,6 +968,40 @@ function Field({
|
|
|
954
968
|
)
|
|
955
969
|
}
|
|
956
970
|
|
|
971
|
+
/**
|
|
972
|
+
* People imports only. Required whenever the map names `consent_status` or
|
|
973
|
+
* `consent_at` — the server refuses the job otherwise, so the picker is next
|
|
974
|
+
* to the columns rather than tucked away.
|
|
975
|
+
*/
|
|
976
|
+
function ConsentPurposeField({
|
|
977
|
+
value,
|
|
978
|
+
onChange,
|
|
979
|
+
}: {
|
|
980
|
+
readonly value: string | null
|
|
981
|
+
readonly onChange: (id: string | null) => void
|
|
982
|
+
}): ReactNode {
|
|
983
|
+
const purposes = useConsentPurposes({ sort: 'sort_order', limit: 200 })
|
|
984
|
+
|
|
985
|
+
return (
|
|
986
|
+
<Field label="Consent purpose">
|
|
987
|
+
<select
|
|
988
|
+
value={value ?? ''}
|
|
989
|
+
onChange={(event) => {
|
|
990
|
+
onChange(event.target.value.length === 0 ? null : event.target.value)
|
|
991
|
+
}}
|
|
992
|
+
className="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-[13px] outline-none focus:border-accent"
|
|
993
|
+
>
|
|
994
|
+
<option value="">— none —</option>
|
|
995
|
+
{purposes.records.map((purpose) => (
|
|
996
|
+
<option key={purpose.id} value={purpose.id}>
|
|
997
|
+
{purpose.label}
|
|
998
|
+
</option>
|
|
999
|
+
))}
|
|
1000
|
+
</select>
|
|
1001
|
+
</Field>
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
957
1005
|
function messageOf(error: Error | null | undefined): string | null {
|
|
958
1006
|
return error === null || error === undefined ? null : error.message
|
|
959
1007
|
}
|
|
@@ -1011,9 +1059,9 @@ function SampleDataSection(): ReactNode {
|
|
|
1011
1059
|
<p className="text-[12px] text-ink-muted">
|
|
1012
1060
|
Added {counts.companies} companies, {counts.people} people, {counts.positions}{' '}
|
|
1013
1061
|
positions, {counts.deals} deals, {counts.opportunities} opportunities,{' '}
|
|
1014
|
-
{counts.raises} raises, {counts.partnerships} partnerships, {counts.
|
|
1015
|
-
{counts.
|
|
1016
|
-
{counts.notes} notes.
|
|
1062
|
+
{counts.raises} raises, {counts.partnerships} partnerships, {counts.enquiries}{' '}
|
|
1063
|
+
enquiries, {counts.roles} roles, {counts.candidates} candidates,{' '}
|
|
1064
|
+
{counts.planItems} plan items, and {counts.notes} notes.
|
|
1017
1065
|
</p>
|
|
1018
1066
|
) : null}
|
|
1019
1067
|
{failure === null ? null : (
|