@kelpie/ui 0.9.0 → 0.11.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/resources/activities.d.ts +2 -1
- package/dist/api/resources/activities.d.ts.map +1 -1
- package/dist/api/resources/activities.js +6 -2
- package/dist/api/resources/activities.js.map +1 -1
- package/dist/api/resources/conversions.d.ts +16 -0
- package/dist/api/resources/conversions.d.ts.map +1 -0
- package/dist/api/resources/conversions.js +81 -0
- package/dist/api/resources/conversions.js.map +1 -0
- package/dist/api/resources/enquiries.d.ts +2 -7
- package/dist/api/resources/enquiries.d.ts.map +1 -1
- package/dist/api/resources/enquiries.js +2 -30
- package/dist/api/resources/enquiries.js.map +1 -1
- package/dist/api/resources/importJobs.d.ts +2 -2
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js.map +1 -1
- package/dist/app/KelpieApp.js +1 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts +2 -2
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +7 -5
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/ApiKeysPanel.d.ts +0 -7
- package/dist/components/ApiKeysPanel.d.ts.map +1 -1
- package/dist/components/ApiKeysPanel.js +83 -16
- package/dist/components/ApiKeysPanel.js.map +1 -1
- package/dist/components/ConvertRecordDialog.d.ts +30 -0
- package/dist/components/ConvertRecordDialog.d.ts.map +1 -0
- package/dist/components/ConvertRecordDialog.js +50 -0
- package/dist/components/ConvertRecordDialog.js.map +1 -0
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +19 -7
- package/dist/components/Shell.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +15 -2
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +1 -7
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiryDetail.d.ts.map +1 -1
- package/dist/pages/EnquiryDetail.js +17 -25
- package/dist/pages/EnquiryDetail.js.map +1 -1
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +1 -14
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +15 -2
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +15 -2
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +15 -2
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +3 -3
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/PrivacyPage.js +3 -3
- package/dist/pages/admin/PrivacyPage.js.map +1 -1
- package/dist/pages/admin/TeamPage.js +1 -1
- package/dist/pages/admin/TeamPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +2 -12
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/forms/AddFieldMenu.js +2 -2
- package/dist/pages/forms/AddFieldMenu.js.map +1 -1
- package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
- package/dist/pages/forms/FieldsEditor.js +22 -10
- package/dist/pages/forms/FieldsEditor.js.map +1 -1
- package/dist/pages/forms/MapTargetSearch.d.ts +7 -0
- package/dist/pages/forms/MapTargetSearch.d.ts.map +1 -0
- package/dist/pages/forms/MapTargetSearch.js +32 -0
- package/dist/pages/forms/MapTargetSearch.js.map +1 -0
- package/dist/pages/forms/fieldList.d.ts +6 -7
- package/dist/pages/forms/fieldList.d.ts.map +1 -1
- package/dist/pages/forms/fieldList.js +14 -16
- package/dist/pages/forms/fieldList.js.map +1 -1
- package/dist/pages/forms/template.d.ts.map +1 -1
- package/dist/pages/forms/template.js +7 -0
- package/dist/pages/forms/template.js.map +1 -1
- package/package.json +2 -2
- package/src/api/resources/activities.ts +10 -2
- package/src/api/resources/conversions.ts +125 -0
- package/src/api/resources/enquiries.ts +9 -35
- package/src/api/resources/importJobs.ts +3 -2
- package/src/app/KelpieApp.tsx +1 -0
- package/src/components/ActivitiesPanel.tsx +20 -14
- package/src/components/ApiKeysPanel.tsx +206 -21
- package/src/components/ConvertRecordDialog.tsx +210 -0
- package/src/components/Shell.tsx +91 -36
- package/src/pages/DealDetail.tsx +35 -0
- package/src/pages/DecisionsPage.tsx +0 -14
- package/src/pages/EnquiryDetail.tsx +37 -90
- package/src/pages/ListDetail.tsx +0 -47
- package/src/pages/OpportunityDetail.tsx +35 -0
- package/src/pages/PartnershipDetail.tsx +35 -0
- package/src/pages/RaiseDetail.tsx +35 -0
- package/src/pages/admin/DataPage.tsx +14 -9
- package/src/pages/admin/PrivacyPage.tsx +4 -4
- package/src/pages/admin/TeamPage.tsx +1 -2
- package/src/pages/admin/WorkspaceSettingsPage.tsx +1 -31
- package/src/pages/forms/AddFieldMenu.tsx +2 -2
- package/src/pages/forms/FieldsEditor.tsx +45 -20
- package/src/pages/forms/MapTargetSearch.tsx +69 -0
- package/src/pages/forms/fieldList.ts +34 -16
- package/src/pages/forms/template.ts +7 -0
package/src/components/Shell.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router'
|
|
2
|
+
import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router'
|
|
3
3
|
|
|
4
4
|
import { useAccount, useTheme } from '../api/resources/account.ts'
|
|
5
5
|
import { useLogOut } from '../api/resources/session.ts'
|
|
@@ -49,11 +49,21 @@ const CORE_NAV: readonly NavItem[] = [
|
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* `nav.primary` is one flat slot (`modules.md`); the
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* `nav.primary` is one flat slot (`modules.md`); the shell renders it as visual
|
|
53
|
+
* groups: an unheaded top section, an unheaded directory pair (People,
|
|
54
|
+
* Companies), a headed "CRM" section for pipeline objects, then a headed
|
|
55
|
+
* "Tools" section for Lists and Forms. Presentation only — it stays here rather
|
|
56
|
+
* than becoming extra slots.
|
|
55
57
|
*/
|
|
56
58
|
const TOP_LEVEL_NAV_IDS: ReadonlySet<string> = new Set(['dashboard', 'handbook', 'planning', 'decisions'])
|
|
59
|
+
const DIRECTORY_NAV_IDS: ReadonlySet<string> = new Set(['people', 'companies'])
|
|
60
|
+
const TOOLS_NAV_IDS: ReadonlySet<string> = new Set(['lists', 'forms'])
|
|
61
|
+
|
|
62
|
+
const ADMIN_ENTRY_PATH = '/admin/workspace'
|
|
63
|
+
|
|
64
|
+
function isAdminRoute(pathname: string): boolean {
|
|
65
|
+
return pathname.startsWith('/admin/')
|
|
66
|
+
}
|
|
57
67
|
|
|
58
68
|
function linkClass({ isActive }: { isActive: boolean }): string {
|
|
59
69
|
return [
|
|
@@ -118,8 +128,14 @@ export function Shell(): React.JSX.Element {
|
|
|
118
128
|
|
|
119
129
|
const navItems = useVisibleNavItems(CORE_NAV, moduleNav)
|
|
120
130
|
const topLevelItems = navItems.filter((item) => TOP_LEVEL_NAV_IDS.has(item.id))
|
|
121
|
-
const
|
|
131
|
+
const nonTopLevelItems = navItems.filter((item) => !TOP_LEVEL_NAV_IDS.has(item.id))
|
|
132
|
+
const directoryItems = nonTopLevelItems.filter((item) => DIRECTORY_NAV_IDS.has(item.id))
|
|
133
|
+
const toolsItems = nonTopLevelItems.filter((item) => TOOLS_NAV_IDS.has(item.id))
|
|
134
|
+
const crmItems = nonTopLevelItems.filter(
|
|
135
|
+
(item) => !DIRECTORY_NAV_IDS.has(item.id) && !TOOLS_NAV_IDS.has(item.id),
|
|
136
|
+
)
|
|
122
137
|
const adminItems = useVisibleNavItems(CORE_ADMIN_NAV, moduleAdminNav)
|
|
138
|
+
const inAdminNav = isAdminRoute(location.pathname)
|
|
123
139
|
|
|
124
140
|
return (
|
|
125
141
|
<div className="flex min-h-screen bg-surface">
|
|
@@ -133,40 +149,79 @@ export function Shell(): React.JSX.Element {
|
|
|
133
149
|
</div>
|
|
134
150
|
|
|
135
151
|
<nav className="flex flex-1 flex-col gap-5 overflow-y-auto px-2 pb-4">
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
{inAdminNav ? (
|
|
153
|
+
<>
|
|
154
|
+
<Link
|
|
155
|
+
to="/dashboard"
|
|
156
|
+
className="inline-flex px-2 text-[12px] font-medium text-sidebar-muted transition-colors hover:text-sidebar-ink"
|
|
157
|
+
>
|
|
158
|
+
← Back
|
|
159
|
+
</Link>
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
<div>
|
|
162
|
+
<div className="mb-1 px-2 text-[11px] text-sidebar-muted">Admin</div>
|
|
163
|
+
<div className="flex flex-col gap-0.5">
|
|
164
|
+
{adminItems.map((item) => (
|
|
165
|
+
<NavLink key={item.id} to={item.to} className={linkClass}>
|
|
166
|
+
{item.label}
|
|
167
|
+
</NavLink>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</>
|
|
172
|
+
) : (
|
|
173
|
+
<>
|
|
174
|
+
<div className="flex flex-col gap-0.5">
|
|
175
|
+
{topLevelItems.map((item) => (
|
|
176
|
+
<NavLink key={item.id} to={item.to} className={linkClass}>
|
|
177
|
+
{item.label}
|
|
178
|
+
</NavLink>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
154
181
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
{directoryItems.length > 0 && (
|
|
183
|
+
<div className="flex flex-col gap-0.5">
|
|
184
|
+
{directoryItems.map((item) => (
|
|
185
|
+
<NavLink key={item.id} to={item.to} className={linkClass}>
|
|
186
|
+
{item.label}
|
|
187
|
+
</NavLink>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{crmItems.length > 0 && (
|
|
193
|
+
<div>
|
|
194
|
+
<div className="mb-1 px-2 text-[11px] text-sidebar-muted">CRM</div>
|
|
195
|
+
<div className="flex flex-col gap-0.5">
|
|
196
|
+
{crmItems.map((item) => (
|
|
197
|
+
<NavLink key={item.id} to={item.to} className={linkClass}>
|
|
198
|
+
{item.label}
|
|
199
|
+
</NavLink>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{toolsItems.length > 0 && (
|
|
206
|
+
<div>
|
|
207
|
+
<div className="mb-1 px-2 text-[11px] text-sidebar-muted">Tools</div>
|
|
208
|
+
<div className="flex flex-col gap-0.5">
|
|
209
|
+
{toolsItems.map((item) => (
|
|
210
|
+
<NavLink key={item.id} to={item.to} className={linkClass}>
|
|
211
|
+
{item.label}
|
|
212
|
+
</NavLink>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<div className="flex flex-col gap-0.5">
|
|
219
|
+
<NavLink to={ADMIN_ENTRY_PATH} className={linkClass}>
|
|
220
|
+
Admin
|
|
166
221
|
</NavLink>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
222
|
+
</div>
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
170
225
|
</nav>
|
|
171
226
|
</aside>
|
|
172
227
|
|
package/src/pages/DealDetail.tsx
CHANGED
|
@@ -6,6 +6,10 @@ import { Link, useNavigate, useParams } from 'react-router'
|
|
|
6
6
|
import { usePatch } from '../api/resource.ts'
|
|
7
7
|
import type { PatchResult } from '../api/resource.ts'
|
|
8
8
|
import { useCompanies, useCompany } from '../api/resources/companies.ts'
|
|
9
|
+
import {
|
|
10
|
+
detailPathForPipelineKind,
|
|
11
|
+
useConvertPipelineRecord,
|
|
12
|
+
} from '../api/resources/conversions.ts'
|
|
9
13
|
import { useDeal, useDeleteDeal, useUpdateDeal } from '../api/resources/deals.ts'
|
|
10
14
|
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
11
15
|
import { useMembers } from '../api/resources/members.ts'
|
|
@@ -16,6 +20,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
16
20
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
17
21
|
import { Chip } from '../components/Chip.tsx'
|
|
18
22
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
23
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
19
24
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
20
25
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
21
26
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -56,10 +61,12 @@ export function DealDetail(): React.JSX.Element {
|
|
|
56
61
|
const { id } = useParams()
|
|
57
62
|
const navigate = useNavigate()
|
|
58
63
|
const { record, isLoading, isNotFound, error } = useDeal(id)
|
|
64
|
+
const convertRecord = useConvertPipelineRecord('deal')
|
|
59
65
|
const deleteDeal = useDeleteDeal()
|
|
60
66
|
const moduleTabs = inSlotOrder(useRecordTabs('deal'))
|
|
61
67
|
const hasCustomFields = useHasCustomFields('deal')
|
|
62
68
|
const [activeTab, setActiveTab] = useState('overview')
|
|
69
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
63
70
|
const formSubmissions = useFormSubmissionsForRecord('deal', id)
|
|
64
71
|
|
|
65
72
|
if (isNotFound) {
|
|
@@ -89,6 +96,7 @@ export function DealDetail(): React.JSX.Element {
|
|
|
89
96
|
]
|
|
90
97
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
91
98
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
99
|
+
const convertedTo = record.convertedTo
|
|
92
100
|
|
|
93
101
|
return (
|
|
94
102
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -104,6 +112,13 @@ export function DealDetail(): React.JSX.Element {
|
|
|
104
112
|
<DealHeading deal={record} />
|
|
105
113
|
|
|
106
114
|
<div className="flex justify-end gap-2">
|
|
115
|
+
<ConvertRecordButton
|
|
116
|
+
convertedTo={convertedTo}
|
|
117
|
+
convert={convertRecord}
|
|
118
|
+
onOpenDialog={() => {
|
|
119
|
+
setShowConvert(true)
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
107
122
|
<AgentTasks targetType="deal" targetId={record.id} targetLabel={record.name} />
|
|
108
123
|
<DeleteRecord
|
|
109
124
|
recordLabel="Deal"
|
|
@@ -119,6 +134,26 @@ export function DealDetail(): React.JSX.Element {
|
|
|
119
134
|
/>
|
|
120
135
|
</div>
|
|
121
136
|
|
|
137
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
138
|
+
<ConvertRecordAction
|
|
139
|
+
sourceKind="deal"
|
|
140
|
+
recordId={record.id}
|
|
141
|
+
recordName={record.name}
|
|
142
|
+
companyId={record.companyId}
|
|
143
|
+
convertedTo={convertedTo}
|
|
144
|
+
convert={convertRecord}
|
|
145
|
+
showDialog={showConvert}
|
|
146
|
+
onOpenDialog={() => {
|
|
147
|
+
setShowConvert(true)
|
|
148
|
+
}}
|
|
149
|
+
onCloseDialog={() => {
|
|
150
|
+
setShowConvert(false)
|
|
151
|
+
}}
|
|
152
|
+
onConverted={(created, targetType) => {
|
|
153
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
|
|
122
157
|
<RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Deal sections">
|
|
123
158
|
{active === 'overview' && <DealOverview deal={record} />}
|
|
124
159
|
{active === 'fields' && (
|
|
@@ -60,7 +60,6 @@ const TARGET_ROUTES: Readonly<Partial<Record<RecordTargetType, string>>> = {
|
|
|
60
60
|
|
|
61
61
|
interface TargetDirectory {
|
|
62
62
|
nameFor(decision: Decision): string | undefined
|
|
63
|
-
readonly isComplete: boolean
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
function useTargetDirectory(): TargetDirectory {
|
|
@@ -95,13 +94,6 @@ function useTargetDirectory(): TargetDirectory {
|
|
|
95
94
|
|
|
96
95
|
return {
|
|
97
96
|
nameFor: (decision) => nameById.get(decision.targetId),
|
|
98
|
-
isComplete:
|
|
99
|
-
!people.hasNext &&
|
|
100
|
-
!companies.hasNext &&
|
|
101
|
-
!deals.hasNext &&
|
|
102
|
-
!opportunities.hasNext &&
|
|
103
|
-
!raises.hasNext &&
|
|
104
|
-
!partnerships.hasNext,
|
|
105
97
|
}
|
|
106
98
|
}
|
|
107
99
|
|
|
@@ -258,12 +250,6 @@ export function DecisionsPage(): React.JSX.Element {
|
|
|
258
250
|
onSortChange={setSort}
|
|
259
251
|
visibleColumnKeys={listView.visibleKeys}
|
|
260
252
|
/>
|
|
261
|
-
{!directory.isComplete && (
|
|
262
|
-
<p className="mt-2 text-[11px] text-ink-faint">
|
|
263
|
-
The workspace holds more records than one page returns, so some rows name only their
|
|
264
|
-
record type.
|
|
265
|
-
</p>
|
|
266
|
-
)}
|
|
267
253
|
<Paginator list={decisions} />
|
|
268
254
|
</>
|
|
269
255
|
)}
|
|
@@ -7,11 +7,14 @@ 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 {
|
|
10
|
-
useConvertEnquiry,
|
|
11
10
|
useDeleteEnquiry,
|
|
12
11
|
useEnquiry,
|
|
13
12
|
useUpdateEnquiry,
|
|
14
13
|
} from '../api/resources/enquiries.ts'
|
|
14
|
+
import {
|
|
15
|
+
detailPathForPipelineKind,
|
|
16
|
+
useConvertPipelineRecord,
|
|
17
|
+
} from '../api/resources/conversions.ts'
|
|
15
18
|
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
16
19
|
import { useMembers } from '../api/resources/members.ts'
|
|
17
20
|
import { usePeople } from '../api/resources/people.ts'
|
|
@@ -21,6 +24,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
21
24
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
22
25
|
import { Chip } from '../components/Chip.tsx'
|
|
23
26
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
27
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
24
28
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
25
29
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
26
30
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -60,8 +64,8 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
60
64
|
const { id } = useParams()
|
|
61
65
|
const navigate = useNavigate()
|
|
62
66
|
const { record, isLoading, isNotFound, error } = useEnquiry(id)
|
|
67
|
+
const convertRecord = useConvertPipelineRecord('enquiry')
|
|
63
68
|
const deleteEnquiry = useDeleteEnquiry()
|
|
64
|
-
const convertEnquiry = useConvertEnquiry()
|
|
65
69
|
const moduleTabs = inSlotOrder(useRecordTabs('enquiry'))
|
|
66
70
|
const hasCustomFields = useHasCustomFields('enquiry')
|
|
67
71
|
const [activeTab, setActiveTab] = useState('overview')
|
|
@@ -96,12 +100,11 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
96
100
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
97
101
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
98
102
|
|
|
99
|
-
const
|
|
100
|
-
record.
|
|
101
|
-
|
|
102
|
-
: record.
|
|
103
|
-
|
|
104
|
-
: null
|
|
103
|
+
const convertedTo =
|
|
104
|
+
record.convertedTo ??
|
|
105
|
+
(record.convertedDealId !== null
|
|
106
|
+
? { targetType: 'deal' as const, targetId: record.convertedDealId }
|
|
107
|
+
: null)
|
|
105
108
|
|
|
106
109
|
return (
|
|
107
110
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -117,28 +120,13 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
117
120
|
<EnquiryHeading enquiry={record} />
|
|
118
121
|
|
|
119
122
|
<div className="flex justify-end gap-2">
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}}
|
|
128
|
-
disabled={convertBlockedReason !== null || convertEnquiry.isPending}
|
|
129
|
-
title={convertBlockedReason ?? undefined}
|
|
130
|
-
className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-ink-muted transition hover:border-accent hover:text-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-border disabled:hover:text-ink-muted"
|
|
131
|
-
>
|
|
132
|
-
Convert to deal
|
|
133
|
-
</button>
|
|
134
|
-
) : (
|
|
135
|
-
<Link
|
|
136
|
-
to={`/deals/${record.convertedDealId}`}
|
|
137
|
-
className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-accent transition hover:border-accent hover:underline"
|
|
138
|
-
>
|
|
139
|
-
Converted — view deal
|
|
140
|
-
</Link>
|
|
141
|
-
)}
|
|
123
|
+
<ConvertRecordButton
|
|
124
|
+
convertedTo={convertedTo}
|
|
125
|
+
convert={convertRecord}
|
|
126
|
+
onOpenDialog={() => {
|
|
127
|
+
setShowConvert(true)
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
142
130
|
<AgentTasks targetType="enquiry" targetId={record.id} targetLabel={record.name} />
|
|
143
131
|
<DeleteRecord
|
|
144
132
|
recordLabel="Enquiry"
|
|
@@ -154,26 +142,25 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
154
142
|
/>
|
|
155
143
|
</div>
|
|
156
144
|
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)}
|
|
145
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
146
|
+
<ConvertRecordAction
|
|
147
|
+
sourceKind="enquiry"
|
|
148
|
+
recordId={record.id}
|
|
149
|
+
recordName={record.name}
|
|
150
|
+
companyId={record.companyId}
|
|
151
|
+
convertedTo={convertedTo}
|
|
152
|
+
convert={convertRecord}
|
|
153
|
+
showDialog={showConvert}
|
|
154
|
+
onOpenDialog={() => {
|
|
155
|
+
setShowConvert(true)
|
|
156
|
+
}}
|
|
157
|
+
onCloseDialog={() => {
|
|
158
|
+
setShowConvert(false)
|
|
159
|
+
}}
|
|
160
|
+
onConverted={(created, targetType) => {
|
|
161
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
177
164
|
|
|
178
165
|
<RecordTabs
|
|
179
166
|
tabs={tabs}
|
|
@@ -512,43 +499,3 @@ function EnquiryContacts({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.
|
|
|
512
499
|
</section>
|
|
513
500
|
)
|
|
514
501
|
}
|
|
515
|
-
|
|
516
|
-
function ConvertConfirm({
|
|
517
|
-
enquiry,
|
|
518
|
-
isPending,
|
|
519
|
-
onCancel,
|
|
520
|
-
onConfirm,
|
|
521
|
-
}: {
|
|
522
|
-
readonly enquiry: Enquiry
|
|
523
|
-
readonly isPending: boolean
|
|
524
|
-
readonly onCancel: () => void
|
|
525
|
-
readonly onConfirm: () => void
|
|
526
|
-
}): React.JSX.Element {
|
|
527
|
-
return (
|
|
528
|
-
<div className="rounded-md border border-border bg-surface-raised p-4">
|
|
529
|
-
<p className="text-[13px] text-ink">
|
|
530
|
-
Convert <span className="font-semibold">{enquiry.name}</span> to a Deal. This creates a new
|
|
531
|
-
deal (name, company, owner and linked people carry over) and moves this enquiry to the
|
|
532
|
-
first closed stage.
|
|
533
|
-
</p>
|
|
534
|
-
<div className="mt-3 flex justify-end gap-2">
|
|
535
|
-
<button
|
|
536
|
-
type="button"
|
|
537
|
-
onClick={onCancel}
|
|
538
|
-
disabled={isPending}
|
|
539
|
-
className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink disabled:opacity-50"
|
|
540
|
-
>
|
|
541
|
-
Cancel
|
|
542
|
-
</button>
|
|
543
|
-
<button
|
|
544
|
-
type="button"
|
|
545
|
-
onClick={onConfirm}
|
|
546
|
-
disabled={isPending}
|
|
547
|
-
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
|
|
548
|
-
>
|
|
549
|
-
{isPending ? 'Converting…' : 'Convert'}
|
|
550
|
-
</button>
|
|
551
|
-
</div>
|
|
552
|
-
</div>
|
|
553
|
-
)
|
|
554
|
-
}
|
package/src/pages/ListDetail.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import { Link, useNavigate, useParams } from 'react-router'
|
|
|
6
6
|
import { usePatch } from '../api/resource.ts'
|
|
7
7
|
import type { PatchResult } from '../api/resource.ts'
|
|
8
8
|
import { useCompanies } from '../api/resources/companies.ts'
|
|
9
|
-
import { useConsentPurposes } from '../api/resources/consentPurposes.ts'
|
|
10
9
|
import { useDeals } from '../api/resources/deals.ts'
|
|
11
10
|
import { useDeleteList, useList, useUpdateList } from '../api/resources/lists.ts'
|
|
12
11
|
import {
|
|
@@ -86,57 +85,11 @@ export function ListDetail(): React.JSX.Element {
|
|
|
86
85
|
|
|
87
86
|
<ListDescription list={record} />
|
|
88
87
|
|
|
89
|
-
{record.targetType === 'person' && <ListConsentPurpose list={record} />}
|
|
90
|
-
|
|
91
88
|
<ListMembers listId={record.id} targetType={record.targetType} />
|
|
92
89
|
</div>
|
|
93
90
|
)
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
function ListConsentPurpose({ list }: { readonly list: List }): React.JSX.Element {
|
|
97
|
-
const { patch, error } = useListPatch(list)
|
|
98
|
-
const purposes = useConsentPurposes({ sort: 'sort_order', limit: 200 })
|
|
99
|
-
const options = [
|
|
100
|
-
{ value: '', label: '— none —' },
|
|
101
|
-
...purposes.records.map((purpose) => ({ value: purpose.id, label: purpose.label })),
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="mb-6 rounded-md border border-border bg-surface-raised p-4 text-[13px]">
|
|
106
|
-
{error !== null && (
|
|
107
|
-
<div className="mb-2">
|
|
108
|
-
<ErrorPanel error={error} />
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
|
112
|
-
<div>
|
|
113
|
-
<div className="text-[12px] font-medium text-ink">Consent purpose</div>
|
|
114
|
-
<p className="mt-0.5 text-[12px] text-ink-muted">
|
|
115
|
-
A form submit that adds a person to this list also grants this purpose. Manual and
|
|
116
|
-
API adds capture nothing.
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
<label className="shrink-0">
|
|
120
|
-
<select
|
|
121
|
-
value={list.consentPurposeId ?? ''}
|
|
122
|
-
onChange={(event) => {
|
|
123
|
-
const value = event.target.value
|
|
124
|
-
patch({ consentPurposeId: value.length === 0 ? null : value })
|
|
125
|
-
}}
|
|
126
|
-
className="rounded-md border border-border bg-transparent px-2 py-1 text-[12px] outline-none focus:border-accent"
|
|
127
|
-
>
|
|
128
|
-
{options.map((option) => (
|
|
129
|
-
<option key={option.value} value={option.value}>
|
|
130
|
-
{option.label}
|
|
131
|
-
</option>
|
|
132
|
-
))}
|
|
133
|
-
</select>
|
|
134
|
-
</label>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
93
|
function useListPatch(list: List): PatchResult<ListInput> {
|
|
141
94
|
return usePatch(useUpdateList, list)
|
|
142
95
|
}
|
|
@@ -6,6 +6,10 @@ import { Link, useNavigate, useParams } from 'react-router'
|
|
|
6
6
|
import { usePatch } from '../api/resource.ts'
|
|
7
7
|
import type { PatchResult } from '../api/resource.ts'
|
|
8
8
|
import { useCompanies, useCompany } from '../api/resources/companies.ts'
|
|
9
|
+
import {
|
|
10
|
+
detailPathForPipelineKind,
|
|
11
|
+
useConvertPipelineRecord,
|
|
12
|
+
} from '../api/resources/conversions.ts'
|
|
9
13
|
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
10
14
|
import { useMembers } from '../api/resources/members.ts'
|
|
11
15
|
import {
|
|
@@ -20,6 +24,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
20
24
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
21
25
|
import { Chip } from '../components/Chip.tsx'
|
|
22
26
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
27
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
23
28
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
24
29
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
25
30
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -61,10 +66,12 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
61
66
|
const { id } = useParams()
|
|
62
67
|
const navigate = useNavigate()
|
|
63
68
|
const { record, isLoading, isNotFound, error } = useOpportunity(id)
|
|
69
|
+
const convertRecord = useConvertPipelineRecord('opportunity')
|
|
64
70
|
const deleteOpportunity = useDeleteOpportunity()
|
|
65
71
|
const moduleTabs = inSlotOrder(useRecordTabs('opportunity'))
|
|
66
72
|
const hasCustomFields = useHasCustomFields('opportunity')
|
|
67
73
|
const [activeTab, setActiveTab] = useState('overview')
|
|
74
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
68
75
|
const formSubmissions = useFormSubmissionsForRecord('opportunity', id)
|
|
69
76
|
|
|
70
77
|
if (isNotFound) {
|
|
@@ -94,6 +101,7 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
94
101
|
]
|
|
95
102
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
96
103
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
104
|
+
const convertedTo = record.convertedTo
|
|
97
105
|
|
|
98
106
|
return (
|
|
99
107
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -109,6 +117,13 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
109
117
|
<OpportunityHeading opportunity={record} />
|
|
110
118
|
|
|
111
119
|
<div className="flex justify-end gap-2">
|
|
120
|
+
<ConvertRecordButton
|
|
121
|
+
convertedTo={convertedTo}
|
|
122
|
+
convert={convertRecord}
|
|
123
|
+
onOpenDialog={() => {
|
|
124
|
+
setShowConvert(true)
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
112
127
|
<AgentTasks targetType="opportunity" targetId={record.id} targetLabel={record.name} />
|
|
113
128
|
<DeleteRecord
|
|
114
129
|
recordLabel="Opportunity"
|
|
@@ -124,6 +139,26 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
124
139
|
/>
|
|
125
140
|
</div>
|
|
126
141
|
|
|
142
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
143
|
+
<ConvertRecordAction
|
|
144
|
+
sourceKind="opportunity"
|
|
145
|
+
recordId={record.id}
|
|
146
|
+
recordName={record.name}
|
|
147
|
+
companyId={record.companyId}
|
|
148
|
+
convertedTo={convertedTo}
|
|
149
|
+
convert={convertRecord}
|
|
150
|
+
showDialog={showConvert}
|
|
151
|
+
onOpenDialog={() => {
|
|
152
|
+
setShowConvert(true)
|
|
153
|
+
}}
|
|
154
|
+
onCloseDialog={() => {
|
|
155
|
+
setShowConvert(false)
|
|
156
|
+
}}
|
|
157
|
+
onConverted={(created, targetType) => {
|
|
158
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
|
|
127
162
|
<RecordTabs
|
|
128
163
|
tabs={tabs}
|
|
129
164
|
active={active}
|