@kelpie/ui 0.8.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.
- 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
package/src/components/Shell.tsx
CHANGED
|
@@ -22,6 +22,8 @@ import { ErrorBoundary } from './ErrorBoundary.tsx'
|
|
|
22
22
|
const CORE_ADMIN_NAV: readonly NavItem[] = [
|
|
23
23
|
{ id: 'workspace', label: 'Workspace', to: '/admin/workspace', order: 100 },
|
|
24
24
|
{ id: 'team', label: 'Team', to: '/admin/team', order: 200 },
|
|
25
|
+
{ id: 'fields', label: 'Fields', to: '/admin/fields', order: 250 },
|
|
26
|
+
{ id: 'privacy', label: 'Privacy', to: '/admin/privacy', order: 275 },
|
|
25
27
|
{ id: 'data', label: 'Data', to: '/admin/data', order: 300 },
|
|
26
28
|
{ id: 'api-keys', label: 'API keys', to: '/admin/api-keys', order: 350 },
|
|
27
29
|
{ id: 'webhooks', label: 'Webhooks', to: '/admin/webhooks', order: 400 },
|
|
@@ -34,6 +36,7 @@ const CORE_NAV: readonly NavItem[] = [
|
|
|
34
36
|
{ id: 'people', label: 'People', to: '/people', order: 100 },
|
|
35
37
|
{ id: 'hiring', label: 'Hiring', to: '/hiring', order: 150 },
|
|
36
38
|
{ id: 'companies', label: 'Companies', to: '/companies', order: 200 },
|
|
39
|
+
{ id: 'enquiries', label: 'Enquiries', to: '/enquiries', order: 250 },
|
|
37
40
|
{ id: 'deals', label: 'Deals', to: '/deals', order: 300 },
|
|
38
41
|
{ id: 'opportunities', label: 'Opportunities', to: '/opportunities', order: 350 },
|
|
39
42
|
{ id: 'fundraising', label: 'Fundraising', to: '/fundraising', order: 360 },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CustomFieldObjectType } from '@kelpie/schemas'
|
|
2
|
+
|
|
3
|
+
import { useCustomFields } from '../api/resources/customFields.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether this workspace has any custom fields for the given object type.
|
|
7
|
+
*
|
|
8
|
+
* A detail page reads this to decide whether to render its Fields tab at all —
|
|
9
|
+
* a workspace with no definitions should not see a Fields tab full of empty
|
|
10
|
+
* space with only a "go set one up" pointer.
|
|
11
|
+
*
|
|
12
|
+
* Lives in its own file so `CustomFieldsPanel.tsx` can stay Fast-Refresh clean
|
|
13
|
+
* (`react/only-export-components`).
|
|
14
|
+
*/
|
|
15
|
+
export function useHasCustomFields(objectType: CustomFieldObjectType): boolean {
|
|
16
|
+
const definitions = useCustomFields({ objectType, sort: 'sort_order', limit: 200 })
|
|
17
|
+
return definitions.records.length > 0
|
|
18
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
useUpdateCompany,
|
|
13
13
|
} from '../api/resources/companies.ts'
|
|
14
14
|
import { useDeals } from '../api/resources/deals.ts'
|
|
15
|
+
import { useEnquiries } from '../api/resources/enquiries.ts'
|
|
16
|
+
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
15
17
|
import { useOpportunities } from '../api/resources/opportunities.ts'
|
|
16
18
|
import { usePartnerships } from '../api/resources/partnerships.ts'
|
|
17
19
|
import { usePeople } from '../api/resources/people.ts'
|
|
@@ -29,6 +31,9 @@ import type { ChipTone } from '../components/Chip.tsx'
|
|
|
29
31
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
30
32
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
31
33
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
34
|
+
import { FormsPanel } from '../components/FormsPanel.tsx'
|
|
35
|
+
import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
|
|
36
|
+
import { useHasCustomFields } from '../components/useHasCustomFields.ts'
|
|
32
37
|
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
33
38
|
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
34
39
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
@@ -69,7 +74,9 @@ export function CompanyDetail(): React.JSX.Element {
|
|
|
69
74
|
const { record, isLoading, isNotFound, error } = useCompany(id)
|
|
70
75
|
const deleteCompany = useDeleteCompany()
|
|
71
76
|
const moduleTabs = inSlotOrder(useRecordTabs('company'))
|
|
77
|
+
const hasCustomFields = useHasCustomFields('company')
|
|
72
78
|
const [activeTab, setActiveTab] = useState('overview')
|
|
79
|
+
const formSubmissions = useFormSubmissionsForRecord('company', id)
|
|
73
80
|
|
|
74
81
|
if (isNotFound) {
|
|
75
82
|
return <NotFoundPanel label="Company" backTo="/companies" />
|
|
@@ -85,11 +92,15 @@ export function CompanyDetail(): React.JSX.Element {
|
|
|
85
92
|
|
|
86
93
|
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
87
94
|
{ id: 'overview', label: 'Overview' },
|
|
95
|
+
...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
|
|
88
96
|
{ id: 'activity', label: 'Activity' },
|
|
89
97
|
{ id: 'people', label: 'People' },
|
|
90
98
|
{ id: 'notes', label: 'Notes' },
|
|
91
99
|
{ id: 'decisions', label: 'Decisions' },
|
|
92
100
|
{ id: 'lists', label: 'Lists' },
|
|
101
|
+
...(formSubmissions.records.length === 0
|
|
102
|
+
? []
|
|
103
|
+
: [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
|
|
93
104
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
94
105
|
]
|
|
95
106
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -131,11 +142,13 @@ export function CompanyDetail(): React.JSX.Element {
|
|
|
131
142
|
ariaLabel="Company sections"
|
|
132
143
|
>
|
|
133
144
|
{active === 'overview' && <CompanyOverview company={record} />}
|
|
145
|
+
{active === 'fields' && <CompanyFields company={record} />}
|
|
134
146
|
{active === 'activity' && <ActivitiesPanel targetType="company" targetId={record.id} />}
|
|
135
147
|
{active === 'people' && <CompanyPeople company={record} />}
|
|
136
148
|
{active === 'notes' && <NotesPanel targetType="company" targetId={record.id} />}
|
|
137
149
|
{active === 'decisions' && <DecisionsPanel targetType="company" targetId={record.id} />}
|
|
138
150
|
{active === 'lists' && <ListsPanel targetType="company" targetId={record.id} />}
|
|
151
|
+
{active === 'forms' && <FormsPanel targetType="company" targetId={record.id} />}
|
|
139
152
|
{moduleTab?.render({ objectType: 'company', recordId: record.id })}
|
|
140
153
|
</RecordTabs>
|
|
141
154
|
</div>
|
|
@@ -196,6 +209,7 @@ function CompanyOverview({ company }: { readonly company: Company }): React.JSX.
|
|
|
196
209
|
const opportunities = useOpportunities({ companyIds: [company.id] })
|
|
197
210
|
const raises = useRaises({ companyIds: [company.id] })
|
|
198
211
|
const partnerships = usePartnerships({ companyIds: [company.id] })
|
|
212
|
+
const enquiries = useEnquiries({ companyIds: [company.id] })
|
|
199
213
|
|
|
200
214
|
return (
|
|
201
215
|
<div className="space-y-8">
|
|
@@ -211,8 +225,13 @@ function CompanyOverview({ company }: { readonly company: Company }): React.JSX.
|
|
|
211
225
|
opportunities={opportunities.records}
|
|
212
226
|
raises={raises.records}
|
|
213
227
|
partnerships={partnerships.records}
|
|
228
|
+
enquiries={enquiries.records}
|
|
214
229
|
isLoading={
|
|
215
|
-
deals.isLoading ||
|
|
230
|
+
deals.isLoading ||
|
|
231
|
+
opportunities.isLoading ||
|
|
232
|
+
raises.isLoading ||
|
|
233
|
+
partnerships.isLoading ||
|
|
234
|
+
enquiries.isLoading
|
|
216
235
|
}
|
|
217
236
|
/>
|
|
218
237
|
<LatestActivity targetType="company" targetId={company.id} />
|
|
@@ -357,6 +376,22 @@ function CompanySidebar({ company }: { readonly company: Company }): React.JSX.E
|
|
|
357
376
|
)
|
|
358
377
|
}
|
|
359
378
|
|
|
379
|
+
function CompanyFields({ company }: { readonly company: Company }): React.JSX.Element {
|
|
380
|
+
const { patch, error } = useCompanyPatch(company)
|
|
381
|
+
return (
|
|
382
|
+
<div className="space-y-4">
|
|
383
|
+
{error !== null && <ErrorPanel error={error} />}
|
|
384
|
+
<CustomFieldsPanel
|
|
385
|
+
objectType="company"
|
|
386
|
+
values={company.customFields}
|
|
387
|
+
onPatch={(customFields) => {
|
|
388
|
+
patch({ customFields })
|
|
389
|
+
}}
|
|
390
|
+
/>
|
|
391
|
+
</div>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
360
395
|
/** The other side of the Position link: who holds a title here. */
|
|
361
396
|
function CompanyPeople({ company }: { readonly company: Company }): React.JSX.Element {
|
|
362
397
|
const positions = usePositions({ companyIds: [company.id] })
|
package/src/pages/DealDetail.tsx
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
import { useDeal, useDeleteDeal, useUpdateDeal } from '../api/resources/deals.ts'
|
|
10
|
+
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
10
11
|
import { useMembers } from '../api/resources/members.ts'
|
|
11
12
|
import { usePeople } from '../api/resources/people.ts'
|
|
12
13
|
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
@@ -18,6 +19,9 @@ import type { ChipTone } from '../components/Chip.tsx'
|
|
|
18
19
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
19
20
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
20
21
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
22
|
+
import { FormsPanel } from '../components/FormsPanel.tsx'
|
|
23
|
+
import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
|
|
24
|
+
import { useHasCustomFields } from '../components/useHasCustomFields.ts'
|
|
21
25
|
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
22
26
|
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
23
27
|
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
@@ -54,7 +58,9 @@ export function DealDetail(): React.JSX.Element {
|
|
|
54
58
|
const { record, isLoading, isNotFound, error } = useDeal(id)
|
|
55
59
|
const deleteDeal = useDeleteDeal()
|
|
56
60
|
const moduleTabs = inSlotOrder(useRecordTabs('deal'))
|
|
61
|
+
const hasCustomFields = useHasCustomFields('deal')
|
|
57
62
|
const [activeTab, setActiveTab] = useState('overview')
|
|
63
|
+
const formSubmissions = useFormSubmissionsForRecord('deal', id)
|
|
58
64
|
|
|
59
65
|
if (isNotFound) {
|
|
60
66
|
return <NotFoundPanel label="Deal" backTo="/deals" />
|
|
@@ -70,11 +76,15 @@ export function DealDetail(): React.JSX.Element {
|
|
|
70
76
|
|
|
71
77
|
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
72
78
|
{ id: 'overview', label: 'Overview' },
|
|
79
|
+
...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
|
|
73
80
|
{ id: 'plan', label: 'Plan' },
|
|
74
81
|
{ id: 'activity', label: 'Activity' },
|
|
75
82
|
{ id: 'notes', label: 'Notes' },
|
|
76
83
|
{ id: 'decisions', label: 'Decisions' },
|
|
77
84
|
{ id: 'lists', label: 'Lists' },
|
|
85
|
+
...(formSubmissions.records.length === 0
|
|
86
|
+
? []
|
|
87
|
+
: [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
|
|
78
88
|
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
79
89
|
]
|
|
80
90
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
@@ -111,11 +121,15 @@ export function DealDetail(): React.JSX.Element {
|
|
|
111
121
|
|
|
112
122
|
<RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Deal sections">
|
|
113
123
|
{active === 'overview' && <DealOverview deal={record} />}
|
|
124
|
+
{active === 'fields' && (
|
|
125
|
+
<DealFields deal={record} />
|
|
126
|
+
)}
|
|
114
127
|
{active === 'plan' && <PlanPanel targetType="deal" targetId={record.id} />}
|
|
115
128
|
{active === 'activity' && <ActivitiesPanel targetType="deal" targetId={record.id} />}
|
|
116
129
|
{active === 'notes' && <NotesPanel targetType="deal" targetId={record.id} />}
|
|
117
130
|
{active === 'decisions' && <DecisionsPanel targetType="deal" targetId={record.id} />}
|
|
118
131
|
{active === 'lists' && <ListsPanel targetType="deal" targetId={record.id} />}
|
|
132
|
+
{active === 'forms' && <FormsPanel targetType="deal" targetId={record.id} />}
|
|
119
133
|
{moduleTab?.render({ objectType: 'deal', recordId: record.id })}
|
|
120
134
|
</RecordTabs>
|
|
121
135
|
</div>
|
|
@@ -242,6 +256,23 @@ function DealOverview({ deal }: { readonly deal: Deal }): React.JSX.Element {
|
|
|
242
256
|
)
|
|
243
257
|
}
|
|
244
258
|
|
|
259
|
+
/** The Fields tab: the workspace's custom fields for this deal. */
|
|
260
|
+
function DealFields({ deal }: { readonly deal: Deal }): React.JSX.Element {
|
|
261
|
+
const { patch, error } = useDealPatch(deal)
|
|
262
|
+
return (
|
|
263
|
+
<div className="space-y-4">
|
|
264
|
+
{error !== null && <ErrorPanel error={error} />}
|
|
265
|
+
<CustomFieldsPanel
|
|
266
|
+
objectType="deal"
|
|
267
|
+
values={deal.customFields}
|
|
268
|
+
onPatch={(customFields) => {
|
|
269
|
+
patch({ customFields })
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
245
276
|
/** Whole units in the box, cents on the wire. Blank clears the value. */
|
|
246
277
|
function toValueCents(raw: string): number | null | undefined {
|
|
247
278
|
const trimmed = raw.trim()
|
|
@@ -44,6 +44,7 @@ const TARGET_LABELS: Readonly<Record<RecordTargetType, string>> = {
|
|
|
44
44
|
opportunity: 'Opportunity',
|
|
45
45
|
partnership: 'Partnership',
|
|
46
46
|
raise: 'Raise',
|
|
47
|
+
enquiry: 'Enquiry',
|
|
47
48
|
candidate: 'Candidate',
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -54,6 +55,7 @@ const TARGET_ROUTES: Readonly<Partial<Record<RecordTargetType, string>>> = {
|
|
|
54
55
|
opportunity: '/opportunities',
|
|
55
56
|
raise: '/fundraising',
|
|
56
57
|
partnership: '/partnerships',
|
|
58
|
+
enquiry: '/enquiries',
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
interface TargetDirectory {
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type { Enquiry, PipelineStage } from '@kelpie/schemas'
|
|
2
|
+
import { Link, useNavigate } from 'react-router'
|
|
3
|
+
|
|
4
|
+
import { useCompanies } from '../api/resources/companies.ts'
|
|
5
|
+
import {
|
|
6
|
+
useCreateEnquiry,
|
|
7
|
+
useEnquiries,
|
|
8
|
+
useUpdateEnquiry,
|
|
9
|
+
} from '../api/resources/enquiries.ts'
|
|
10
|
+
import { useMembers } from '../api/resources/members.ts'
|
|
11
|
+
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
12
|
+
import { MAX_PAGE_SIZE, usePlanItems } from '../api/resources/planItems.ts'
|
|
13
|
+
import { useTimezone } from '../api/resources/account.ts'
|
|
14
|
+
import { Chip } from '../components/Chip.tsx'
|
|
15
|
+
import type { ChipTone } from '../components/Chip.tsx'
|
|
16
|
+
import { ColumnPicker } from '../components/ColumnPicker.tsx'
|
|
17
|
+
import { DataTable } from '../components/DataTable.tsx'
|
|
18
|
+
import type { Column, DataTableGroup } from '../components/DataTable.tsx'
|
|
19
|
+
import { KanbanBoard } from '../components/KanbanBoard.tsx'
|
|
20
|
+
import { PageHeader } from '../components/PageHeader.tsx'
|
|
21
|
+
import { Paginator } from '../components/Paginator.tsx'
|
|
22
|
+
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
23
|
+
import { SegmentedControl } from '../components/SegmentedControl.tsx'
|
|
24
|
+
import { formatDate, formatDay } from '../lib/dates.ts'
|
|
25
|
+
import { useListView } from '../lib/listView.ts'
|
|
26
|
+
import { DUE_BUCKETS, byDateThenTitle, dueBucketFor, nextOpenByTarget } from '../lib/plan.ts'
|
|
27
|
+
import { serverSortOnly } from '../lib/sort.ts'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The Enquiries pipeline: inbound requests that may become a Deal. The same
|
|
31
|
+
* board-and-list pair as Opportunities over the shared kanban, with `source`
|
|
32
|
+
* in place of `kind` and a company that may be absent.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
type BoardView = 'list' | 'columns'
|
|
36
|
+
type PipelineScope = 'open' | 'all'
|
|
37
|
+
type ListGrouping = 'stage' | 'due'
|
|
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
|
+
'source',
|
|
53
|
+
'company',
|
|
54
|
+
'stage',
|
|
55
|
+
'nextPlan',
|
|
56
|
+
'owner',
|
|
57
|
+
'tags',
|
|
58
|
+
'summary',
|
|
59
|
+
'createdAt',
|
|
60
|
+
'updatedAt',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
/** Tones keyed by the seeded slugs. A renamed stage keeps its slug and its tone. */
|
|
64
|
+
const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
|
|
65
|
+
new: 'accent',
|
|
66
|
+
in_progress: 'warning',
|
|
67
|
+
closed: 'neutral',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stageTone(stage: PipelineStage): ChipTone {
|
|
71
|
+
return STAGE_TONES[stage.slug] ?? 'neutral'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DEFAULT_VISIBLE_KEYS: readonly string[] = [
|
|
75
|
+
'name',
|
|
76
|
+
'source',
|
|
77
|
+
'company',
|
|
78
|
+
'nextPlan',
|
|
79
|
+
'owner',
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
const SERVER_SORT_KEYS: readonly string[] = ['name', 'created_at', 'updated_at']
|
|
83
|
+
|
|
84
|
+
export function EnquiriesPage(): React.JSX.Element {
|
|
85
|
+
const navigate = useNavigate()
|
|
86
|
+
|
|
87
|
+
const listView = useListView('enquiries', COLUMN_KEYS, DEFAULT_VISIBLE_KEYS)
|
|
88
|
+
const view: BoardView = listView.mode ?? 'columns'
|
|
89
|
+
const scope = readScope(listView.scope)
|
|
90
|
+
const grouping = readGrouping(listView.grouping)
|
|
91
|
+
const sort = listView.sort
|
|
92
|
+
|
|
93
|
+
const stages = usePipelineStages('enquiry')
|
|
94
|
+
const enquiries = useEnquiries({ sort: serverSortOnly(sort, SERVER_SORT_KEYS) })
|
|
95
|
+
const companies = useCompanies({ limit: 200 })
|
|
96
|
+
const members = useMembers()
|
|
97
|
+
const createEnquiry = useCreateEnquiry()
|
|
98
|
+
const updateEnquiry = useUpdateEnquiry()
|
|
99
|
+
const timezone = useTimezone()
|
|
100
|
+
|
|
101
|
+
const allStages = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
102
|
+
const visibleStages = scope === 'open' ? allStages.filter((stage) => stage.open) : allStages
|
|
103
|
+
const visibleStageIds = new Set(visibleStages.map((stage) => stage.id))
|
|
104
|
+
const visibleEnquiries = enquiries.records.filter((enquiry) =>
|
|
105
|
+
visibleStageIds.has(enquiry.stageId),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const askedIds = visibleEnquiries.slice(0, MAX_PAGE_SIZE).map((enquiry) => enquiry.id)
|
|
109
|
+
const planItems = usePlanItems(
|
|
110
|
+
{
|
|
111
|
+
targetType: 'enquiry',
|
|
112
|
+
targetIds: askedIds,
|
|
113
|
+
statuses: ['todo', 'in_progress'],
|
|
114
|
+
limit: MAX_PAGE_SIZE,
|
|
115
|
+
},
|
|
116
|
+
{ enabled: askedIds.length > 0 },
|
|
117
|
+
)
|
|
118
|
+
const nextPlanByEnquiry = nextOpenByTarget(planItems.records)
|
|
119
|
+
const plansTruncated = visibleEnquiries.length > MAX_PAGE_SIZE || planItems.hasNext
|
|
120
|
+
|
|
121
|
+
const companyNameById = new Map(companies.records.map((company) => [company.id, company.name]))
|
|
122
|
+
|
|
123
|
+
async function addEnquiry(): Promise<void> {
|
|
124
|
+
const enquiry = await createEnquiry.runAsync({ name: 'New enquiry' })
|
|
125
|
+
|
|
126
|
+
await navigate(`/enquiries/${enquiry.id}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const stageLabelById = new Map(allStages.map((stage) => [stage.id, stage.label]))
|
|
130
|
+
|
|
131
|
+
const columns: readonly Column<Enquiry>[] = [
|
|
132
|
+
{
|
|
133
|
+
key: 'name',
|
|
134
|
+
header: 'Enquiry',
|
|
135
|
+
sortKey: 'name',
|
|
136
|
+
render: (enquiry) => <span className="font-medium text-ink">{enquiry.name}</span>,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: 'source',
|
|
140
|
+
header: 'Source',
|
|
141
|
+
getSortValue: (enquiry) => enquiry.source || null,
|
|
142
|
+
render: (enquiry) => (enquiry.source.length > 0 ? enquiry.source : '—'),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'company',
|
|
146
|
+
header: 'Company',
|
|
147
|
+
getSortValue: (enquiry) =>
|
|
148
|
+
enquiry.companyId === null ? null : (companyNameById.get(enquiry.companyId) ?? null),
|
|
149
|
+
render: (enquiry) =>
|
|
150
|
+
enquiry.companyId === null
|
|
151
|
+
? '—'
|
|
152
|
+
: (companyNameById.get(enquiry.companyId) ?? '—'),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'stage',
|
|
156
|
+
header: 'Stage',
|
|
157
|
+
getSortValue: (enquiry) => stageLabelById.get(enquiry.stageId) ?? null,
|
|
158
|
+
render: (enquiry) => stageLabelById.get(enquiry.stageId) ?? '—',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
key: 'nextPlan',
|
|
162
|
+
header: 'Next plan',
|
|
163
|
+
getSortValue: (enquiry) => nextPlanByEnquiry.get(enquiry.id)?.date ?? null,
|
|
164
|
+
render: (enquiry) => {
|
|
165
|
+
const next = nextPlanByEnquiry.get(enquiry.id)
|
|
166
|
+
|
|
167
|
+
if (next === undefined) {
|
|
168
|
+
return '—'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<span className="text-ink-muted">
|
|
173
|
+
{next.title}
|
|
174
|
+
<span className="ml-1.5 font-mono text-[11px] text-ink-faint">
|
|
175
|
+
{formatDay(next.date)}
|
|
176
|
+
</span>
|
|
177
|
+
</span>
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: 'owner',
|
|
183
|
+
header: 'Owner',
|
|
184
|
+
getSortValue: (enquiry) =>
|
|
185
|
+
enquiry.ownerId === null ? null : (members.nameById.get(enquiry.ownerId) ?? 'Unknown'),
|
|
186
|
+
render: (enquiry) =>
|
|
187
|
+
enquiry.ownerId === null
|
|
188
|
+
? '—'
|
|
189
|
+
: (members.nameById.get(enquiry.ownerId) ?? 'Unknown'),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'tags',
|
|
193
|
+
header: 'Tags',
|
|
194
|
+
getSortValue: (enquiry) => enquiry.tags.join(', ') || null,
|
|
195
|
+
render: (enquiry) =>
|
|
196
|
+
enquiry.tags.length === 0 ? '—' : (
|
|
197
|
+
<span className="flex flex-wrap gap-1">
|
|
198
|
+
{enquiry.tags.map((tag) => (
|
|
199
|
+
<Chip key={tag}>
|
|
200
|
+
<span className="text-[10px]">{tag}</span>
|
|
201
|
+
</Chip>
|
|
202
|
+
))}
|
|
203
|
+
</span>
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: 'summary',
|
|
208
|
+
header: 'Summary',
|
|
209
|
+
getSortValue: (enquiry) => enquiry.summary || null,
|
|
210
|
+
render: (enquiry) =>
|
|
211
|
+
enquiry.summary.length === 0 ? '—' : (
|
|
212
|
+
<span className="text-ink-muted">{enquiry.summary}</span>
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: 'createdAt',
|
|
217
|
+
header: 'Created',
|
|
218
|
+
sortKey: 'created_at',
|
|
219
|
+
render: (enquiry) => formatDate(enquiry.createdAt, timezone),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'updatedAt',
|
|
223
|
+
header: 'Updated',
|
|
224
|
+
sortKey: 'updated_at',
|
|
225
|
+
render: (enquiry) => formatDate(enquiry.updatedAt, timezone),
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
const pickerOptions = columns.map((column) => ({ key: column.key, label: column.header }))
|
|
230
|
+
|
|
231
|
+
const stageGroups: readonly DataTableGroup<Enquiry>[] = visibleStages.map((stage) => ({
|
|
232
|
+
id: stage.id,
|
|
233
|
+
label: <Chip tone={stageTone(stage)}>{stage.label}</Chip>,
|
|
234
|
+
rows: visibleEnquiries.filter((enquiry) => enquiry.stageId === stage.id),
|
|
235
|
+
}))
|
|
236
|
+
|
|
237
|
+
const dueGroups: readonly DataTableGroup<Enquiry>[] = DUE_BUCKETS.map((bucket) => ({
|
|
238
|
+
id: bucket.id,
|
|
239
|
+
label: bucket.label,
|
|
240
|
+
rows: visibleEnquiries
|
|
241
|
+
.filter(
|
|
242
|
+
(enquiry) => dueBucketFor(nextPlanByEnquiry.get(enquiry.id)?.date) === bucket.id,
|
|
243
|
+
)
|
|
244
|
+
.sort((left, right) => {
|
|
245
|
+
const leftNext = nextPlanByEnquiry.get(left.id)
|
|
246
|
+
const rightNext = nextPlanByEnquiry.get(right.id)
|
|
247
|
+
|
|
248
|
+
if (leftNext === undefined || rightNext === undefined) {
|
|
249
|
+
return left.name.localeCompare(right.name)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return byDateThenTitle(leftNext, rightNext) || left.name.localeCompare(right.name)
|
|
253
|
+
}),
|
|
254
|
+
}))
|
|
255
|
+
|
|
256
|
+
const cards = visibleEnquiries.map((enquiry) => {
|
|
257
|
+
const next = nextPlanByEnquiry.get(enquiry.id)
|
|
258
|
+
const companyName =
|
|
259
|
+
enquiry.companyId === null ? undefined : companyNameById.get(enquiry.companyId)
|
|
260
|
+
const meta = [
|
|
261
|
+
enquiry.source.length > 0 ? enquiry.source : undefined,
|
|
262
|
+
companyName,
|
|
263
|
+
next?.title,
|
|
264
|
+
]
|
|
265
|
+
.filter((part) => part !== undefined)
|
|
266
|
+
.join(' · ')
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
id: enquiry.id,
|
|
270
|
+
stage: enquiry.stageId,
|
|
271
|
+
title: enquiry.name,
|
|
272
|
+
meta,
|
|
273
|
+
href: `/enquiries/${enquiry.id}`,
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const isLoading = enquiries.isLoading || stages.isLoading
|
|
278
|
+
const loadError = enquiries.error ?? stages.error
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="animate-fade-in">
|
|
282
|
+
<PageHeader
|
|
283
|
+
title="Enquiries"
|
|
284
|
+
onAdd={() => {
|
|
285
|
+
void addEnquiry()
|
|
286
|
+
}}
|
|
287
|
+
addLabel="Add enquiry"
|
|
288
|
+
actions={
|
|
289
|
+
<>
|
|
290
|
+
<Link
|
|
291
|
+
to="/enquiries/settings"
|
|
292
|
+
className="rounded-md border border-border bg-surface-raised px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink"
|
|
293
|
+
>
|
|
294
|
+
Settings
|
|
295
|
+
</Link>
|
|
296
|
+
<SegmentedControl
|
|
297
|
+
ariaLabel="Stage scope"
|
|
298
|
+
value={scope}
|
|
299
|
+
onChange={(next) => {
|
|
300
|
+
listView.setScope(next)
|
|
301
|
+
}}
|
|
302
|
+
options={[
|
|
303
|
+
{ id: 'open', label: 'Open' },
|
|
304
|
+
{ id: 'all', label: 'All' },
|
|
305
|
+
]}
|
|
306
|
+
/>
|
|
307
|
+
{view === 'list' && (
|
|
308
|
+
<SegmentedControl
|
|
309
|
+
ariaLabel="Group the list"
|
|
310
|
+
value={grouping}
|
|
311
|
+
onChange={(next) => {
|
|
312
|
+
listView.setGrouping(next)
|
|
313
|
+
}}
|
|
314
|
+
options={[
|
|
315
|
+
{ id: 'stage', label: 'By stage' },
|
|
316
|
+
{ id: 'due', label: 'By due' },
|
|
317
|
+
]}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
<SegmentedControl
|
|
321
|
+
ariaLabel="Board or list"
|
|
322
|
+
value={view}
|
|
323
|
+
onChange={(next) => {
|
|
324
|
+
listView.setMode(next)
|
|
325
|
+
}}
|
|
326
|
+
options={[
|
|
327
|
+
{ id: 'list', label: 'List' },
|
|
328
|
+
{ id: 'columns', label: 'Board' },
|
|
329
|
+
]}
|
|
330
|
+
/>
|
|
331
|
+
{view === 'list' && (
|
|
332
|
+
<ColumnPicker
|
|
333
|
+
options={pickerOptions}
|
|
334
|
+
visibleKeys={listView.visibleKeys}
|
|
335
|
+
onChange={listView.setVisibleKeys}
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
</>
|
|
339
|
+
}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{createEnquiry.error !== null && (
|
|
343
|
+
<div className="mb-3">
|
|
344
|
+
<ErrorPanel error={createEnquiry.error} />
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{loadError !== null ? (
|
|
349
|
+
<ErrorPanel error={loadError} />
|
|
350
|
+
) : isLoading ? (
|
|
351
|
+
<LoadingPanel label="Loading enquiries…" />
|
|
352
|
+
) : (
|
|
353
|
+
<>
|
|
354
|
+
{view === 'list' && <Paginator list={enquiries} placement="top" />}
|
|
355
|
+
{view === 'columns' ? (
|
|
356
|
+
<KanbanBoard
|
|
357
|
+
stages={visibleStages.map((stage) => ({ id: stage.id, label: stage.label }))}
|
|
358
|
+
cards={cards}
|
|
359
|
+
onMove={(enquiryId, stageId) => {
|
|
360
|
+
const enquiry = enquiries.records.find(
|
|
361
|
+
(candidate) => candidate.id === enquiryId,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if (enquiry !== undefined && enquiry.stageId !== stageId) {
|
|
365
|
+
updateEnquiry.run({ id: enquiryId, changes: { stageId } })
|
|
366
|
+
}
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
) : (
|
|
370
|
+
<DataTable
|
|
371
|
+
columns={columns}
|
|
372
|
+
groups={grouping === 'stage' ? stageGroups : dueGroups}
|
|
373
|
+
getRowId={(enquiry) => enquiry.id}
|
|
374
|
+
onRowClick={(enquiry) => {
|
|
375
|
+
void navigate(`/enquiries/${enquiry.id}`)
|
|
376
|
+
}}
|
|
377
|
+
emptyMessage={
|
|
378
|
+
enquiries.records.length === 0
|
|
379
|
+
? 'No enquiries yet'
|
|
380
|
+
: scope === 'open'
|
|
381
|
+
? 'No open enquiries'
|
|
382
|
+
: 'No enquiries in this view'
|
|
383
|
+
}
|
|
384
|
+
emptyDescription={
|
|
385
|
+
enquiries.records.length > 0 && scope === 'open'
|
|
386
|
+
? 'You have enquiries, but none are open right now.'
|
|
387
|
+
: undefined
|
|
388
|
+
}
|
|
389
|
+
emptyAction={
|
|
390
|
+
enquiries.records.length === 0
|
|
391
|
+
? {
|
|
392
|
+
label: 'Add enquiry',
|
|
393
|
+
onClick: () => {
|
|
394
|
+
void addEnquiry()
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
: scope === 'open'
|
|
398
|
+
? {
|
|
399
|
+
label: 'Show all enquiries',
|
|
400
|
+
onClick: () => {
|
|
401
|
+
listView.setScope('all')
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
: undefined
|
|
405
|
+
}
|
|
406
|
+
sort={sort}
|
|
407
|
+
onSortChange={(next) => {
|
|
408
|
+
listView.setSort(next)
|
|
409
|
+
}}
|
|
410
|
+
visibleColumnKeys={listView.visibleKeys}
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
{updateEnquiry.error !== null && (
|
|
414
|
+
<div className="mt-3">
|
|
415
|
+
<ErrorPanel error={updateEnquiry.error} />
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
{view === 'list' && <Paginator list={enquiries} />}
|
|
419
|
+
{companies.hasNext && (
|
|
420
|
+
<p className="mt-2 text-[11px] text-ink-faint">
|
|
421
|
+
More companies exist than one page returns, so some company names may show as “—”.
|
|
422
|
+
</p>
|
|
423
|
+
)}
|
|
424
|
+
{plansTruncated && (
|
|
425
|
+
<p className="mt-2 text-[11px] text-ink-faint">
|
|
426
|
+
More plan items exist than one page returns, so some enquiries may show no next
|
|
427
|
+
plan.
|
|
428
|
+
</p>
|
|
429
|
+
)}
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
)
|
|
434
|
+
}
|