spine-framework-cortex 0.1.13 → 0.1.15
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/CHANGELOG.md +20 -0
- package/components/CortexSidebar.tsx +27 -0
- package/functions/custom_funnel-signal.ts +290 -539
- package/functions/webhook-handlers.ts +1 -0
- package/index.tsx +10 -0
- package/package.json +1 -1
- package/pages/crm/AccountDetailPage.tsx +192 -146
- package/pages/intelligence/IntelligencePage.tsx +31 -24
- package/pages/ops/AuditFunnelPage.tsx +191 -0
- package/pages/ops/CommandCenterPage.tsx +377 -0
- package/pages/ops/InstallFunnelPage.tsx +226 -0
- package/seed/integrations.json +26 -0
- package/seed/pipelines.json +34 -5
- package/seed/triggers.json +44 -4
- package/seed/types.json +59 -0
package/index.tsx
CHANGED
|
@@ -37,6 +37,11 @@ const CoursesPage = lazy(() => import('./pages/courses/CoursesPage'))
|
|
|
37
37
|
// Intelligence
|
|
38
38
|
const IntelligencePage = lazy(() => import('./pages/intelligence/IntelligencePage'))
|
|
39
39
|
|
|
40
|
+
// Ops / Revenue
|
|
41
|
+
const CommandCenterPage = lazy(() => import('./pages/ops/CommandCenterPage'))
|
|
42
|
+
const AuditFunnelPage = lazy(() => import('./pages/ops/AuditFunnelPage'))
|
|
43
|
+
const InstallFunnelPage = lazy(() => import('./pages/ops/InstallFunnelPage'))
|
|
44
|
+
|
|
40
45
|
const Fallback = <div className="min-h-[400px] flex items-center justify-center"><LoadingSpinner /></div>
|
|
41
46
|
|
|
42
47
|
function CortexLayout() {
|
|
@@ -96,6 +101,11 @@ function CortexLayout() {
|
|
|
96
101
|
{/* Intelligence */}
|
|
97
102
|
<Route path="intelligence" element={<IntelligencePage />} />
|
|
98
103
|
|
|
104
|
+
{/* Ops / Revenue */}
|
|
105
|
+
<Route path="ops/command-center" element={<CommandCenterPage />} />
|
|
106
|
+
<Route path="ops/audit-funnel" element={<AuditFunnelPage />} />
|
|
107
|
+
<Route path="ops/install-funnel" element={<InstallFunnelPage />} />
|
|
108
|
+
|
|
99
109
|
<Route path="*" element={<Navigate to="dashboard" replace />} />
|
|
100
110
|
</Routes>
|
|
101
111
|
</Suspense>
|
package/package.json
CHANGED
|
@@ -6,8 +6,7 @@ import { Badge } from '@core/components/ui/badge'
|
|
|
6
6
|
import { Skeleton } from '@core/components/ui/skeleton'
|
|
7
7
|
import { Button } from '@core/components/ui/button'
|
|
8
8
|
import { ScrollArea } from '@core/components/ui/scroll-area'
|
|
9
|
-
import {
|
|
10
|
-
import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Filter, TrendingUp, Target, Clock } from 'lucide-react'
|
|
9
|
+
import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Filter, TrendingUp, Target, Clock, Eye, Download, FileText, CheckCircle, Zap, Link2 } from 'lucide-react'
|
|
11
10
|
|
|
12
11
|
interface Account {
|
|
13
12
|
id: string
|
|
@@ -140,182 +139,229 @@ function ActivityTab({ accountId }: { accountId: string }) {
|
|
|
140
139
|
)
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
// ─── ACTION TYPE ICON ────────────────────────────────────────────────────────
|
|
143
|
+
function ActionIcon({ actionType }: { actionType?: string }) {
|
|
144
|
+
if (!actionType) return <Zap className="h-3.5 w-3.5 text-muted-foreground" />
|
|
145
|
+
if (actionType.includes('page_view') || actionType.includes('visit')) return <Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
|
146
|
+
if (actionType.includes('install') || actionType.includes('npm')) return <Download className="h-3.5 w-3.5 text-blue-500" />
|
|
147
|
+
if (actionType.includes('claim')) return <CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
|
148
|
+
if (actionType.includes('audit') || actionType.includes('self')) return <FileText className="h-3.5 w-3.5 text-purple-500" />
|
|
149
|
+
if (actionType.includes('signup') || actionType.includes('register')) return <User className="h-3.5 w-3.5 text-orange-500" />
|
|
150
|
+
return <Zap className="h-3.5 w-3.5 text-muted-foreground" />
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── SIGNAL TIMELINE ─────────────────────────────────────────────────────────
|
|
154
|
+
function SignalTimeline({ accountId }: { accountId: string }) {
|
|
155
|
+
const [signals, setSignals] = useState<any[]>([])
|
|
156
|
+
const [loading, setLoading] = useState(true)
|
|
157
|
+
const [showAll, setShowAll] = useState(false)
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
apiFetch(`/api/admin-data?action=list&entity=items&type_slug=funnel_signal&account_id=${accountId}&limit=50`)
|
|
161
|
+
.then(r => r.json())
|
|
162
|
+
.then(j => {
|
|
163
|
+
const items = Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : []
|
|
164
|
+
items.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
165
|
+
setSignals(items)
|
|
166
|
+
})
|
|
167
|
+
.catch(() => setSignals([]))
|
|
168
|
+
.finally(() => setLoading(false))
|
|
169
|
+
}, [accountId])
|
|
170
|
+
|
|
171
|
+
if (loading) return <div className="space-y-2">{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}</div>
|
|
172
|
+
if (signals.length === 0) return (
|
|
173
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
174
|
+
<Zap className="h-7 w-7 mx-auto mb-2 opacity-20" />
|
|
175
|
+
<p className="text-sm">No signals received yet.</p>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const visible = showAll ? signals : signals.slice(0, 8)
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
{visible.map(sig => {
|
|
184
|
+
const actionType = sig.data?.action?.action_type
|
|
185
|
+
const pipelineSlug = sig.data?.classification?.pipeline_slug
|
|
186
|
+
const rating = sig.data?.scoring_components?.raw_score?.rating
|
|
187
|
+
return (
|
|
188
|
+
<div key={sig.id} className="flex items-start gap-3 py-2 px-1 rounded hover:bg-muted/40 transition-colors">
|
|
189
|
+
<div className="mt-0.5 shrink-0"><ActionIcon actionType={actionType} /></div>
|
|
190
|
+
<div className="flex-1 min-w-0">
|
|
191
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
192
|
+
<span className="text-sm font-medium capitalize">{actionType?.replace(/_/g, ' ') || sig.title}</span>
|
|
193
|
+
{pipelineSlug && (
|
|
194
|
+
<Badge variant="outline" className="text-xs py-0 h-4 font-mono">{pipelineSlug.replace('funnel-', '')}</Badge>
|
|
195
|
+
)}
|
|
196
|
+
{rating && (
|
|
197
|
+
<span className={`text-xs font-medium ${
|
|
198
|
+
rating >= 4 ? 'text-red-600' : rating >= 3 ? 'text-yellow-600' : 'text-muted-foreground'
|
|
199
|
+
}`}>{rating}/5</span>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
<p className="text-xs text-muted-foreground">
|
|
203
|
+
{new Date(sig.created_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
})}
|
|
209
|
+
{signals.length > 8 && !showAll && (
|
|
210
|
+
<button className="text-xs text-primary hover:underline pt-1" onClick={() => setShowAll(true)}>
|
|
211
|
+
View all {signals.length} signals
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── SCORE BREAKDOWN ─────────────────────────────────────────────────────────
|
|
219
|
+
const PIPELINE_LABELS: Record<string, string> = {
|
|
220
|
+
'funnel-ai-audit': 'Audit Intent',
|
|
221
|
+
'funnel-ai-audit-portal-signup': 'Portal Signup',
|
|
222
|
+
'funnel-ai-audit-hot': 'Audit Hot',
|
|
223
|
+
'funnel-technical': 'Technical Intent',
|
|
224
|
+
'funnel-technical-installed': 'Install Activity',
|
|
225
|
+
'funnel-technical-claimed': 'Claim Intent',
|
|
226
|
+
'funnel-general': 'General',
|
|
227
|
+
anonymous: 'Anonymous',
|
|
228
|
+
identified: 'Identified',
|
|
229
|
+
installed: 'Installed',
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ScoreBreakdown({ ratings }: { ratings?: Record<string, any> }) {
|
|
233
|
+
if (!ratings || Object.keys(ratings).length === 0) return (
|
|
234
|
+
<p className="text-xs text-muted-foreground">No scoring data yet.</p>
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="space-y-2.5">
|
|
239
|
+
{Object.entries(ratings).map(([key, val]) => {
|
|
240
|
+
if (!val || typeof val.rating !== 'number') return null
|
|
241
|
+
const displayPct = Math.round((val.raw_score / 25) * 100)
|
|
242
|
+
const label = PIPELINE_LABELS[key] || key
|
|
243
|
+
const color = displayPct >= 80 ? 'bg-green-500' : displayPct >= 50 ? 'bg-yellow-500' : displayPct >= 25 ? 'bg-blue-400' : 'bg-gray-300'
|
|
244
|
+
return (
|
|
245
|
+
<div key={key} className="flex items-center gap-3">
|
|
246
|
+
<span className="text-sm text-muted-foreground w-36 shrink-0 truncate">{label}</span>
|
|
247
|
+
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
248
|
+
<div className={`h-full rounded-full ${color}`} style={{ width: `${displayPct}%` }} />
|
|
249
|
+
</div>
|
|
250
|
+
<span className="text-xs font-semibold w-8 text-right tabular-nums">{displayPct}</span>
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
})}
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── FUNNEL TAB (replaces old FilterTab) ─────────────────────────────────────
|
|
259
|
+
function FunnelTab({ account }: { account: Account }) {
|
|
146
260
|
const temp = account.data?.temperature
|
|
261
|
+
const stage = account.data?.lifecycle_stage
|
|
147
262
|
const ratings = account.data?.ratings
|
|
148
|
-
const attr = account.data?.attribution
|
|
149
263
|
const queue = account.data?.queue
|
|
264
|
+
const claimStatus = (account.data as any)?.claim_status
|
|
265
|
+
const instanceId = (account.data as any)?.instance_id
|
|
150
266
|
|
|
151
267
|
return (
|
|
152
268
|
<div className="p-6 space-y-6">
|
|
153
|
-
{/*
|
|
269
|
+
{/* Top stat row */}
|
|
154
270
|
<div className="grid grid-cols-3 gap-4">
|
|
155
271
|
<div className="border rounded-lg p-4">
|
|
156
|
-
<div className="flex items-center gap-2 mb-
|
|
157
|
-
<Target className="h-
|
|
158
|
-
<span className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
272
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
273
|
+
<Target className="h-3.5 w-3.5 text-muted-foreground" />
|
|
274
|
+
<span className="text-xs text-muted-foreground uppercase tracking-wider">Stage</span>
|
|
159
275
|
</div>
|
|
160
|
-
{stage
|
|
161
|
-
<Badge className="capitalize
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
) : (
|
|
165
|
-
<span className="text-muted-foreground text-sm">Not set</span>
|
|
166
|
-
)}
|
|
276
|
+
{stage
|
|
277
|
+
? <Badge className="capitalize" variant={stage === 'installed' ? 'default' : 'secondary'}>{stage}</Badge>
|
|
278
|
+
: <span className="text-sm text-muted-foreground">Not set</span>
|
|
279
|
+
}
|
|
167
280
|
</div>
|
|
168
|
-
|
|
169
281
|
<div className="border rounded-lg p-4">
|
|
170
|
-
<div className="flex items-center gap-2 mb-
|
|
171
|
-
<TrendingUp className="h-
|
|
172
|
-
<span className="text-xs text-muted-foreground uppercase tracking-wider">
|
|
173
|
-
</div>
|
|
174
|
-
<div className="flex items-center gap-3">
|
|
175
|
-
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
|
176
|
-
<div
|
|
177
|
-
className={`h-full rounded-full ${score >= 70 ? 'bg-green-500' : score >= 40 ? 'bg-yellow-500' : 'bg-gray-400'}`}
|
|
178
|
-
style={{ width: `${Math.min(score, 100)}%` }}
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
<span className="font-semibold">{score}</span>
|
|
282
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
283
|
+
<TrendingUp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
284
|
+
<span className="text-xs text-muted-foreground uppercase tracking-wider">Temperature</span>
|
|
182
285
|
</div>
|
|
286
|
+
{temp ? (
|
|
287
|
+
<Badge variant="outline" className={`capitalize ${
|
|
288
|
+
temp === 'hot' ? 'border-red-400 text-red-600 bg-red-50' :
|
|
289
|
+
temp === 'warm' ? 'border-yellow-400 text-yellow-600 bg-yellow-50' :
|
|
290
|
+
'border-blue-400 text-blue-600 bg-blue-50'
|
|
291
|
+
}`}>{temp}</Badge>
|
|
292
|
+
) : <span className="text-sm text-muted-foreground">—</span>}
|
|
183
293
|
</div>
|
|
184
|
-
|
|
185
294
|
<div className="border rounded-lg p-4">
|
|
186
|
-
<div className="flex items-center gap-2 mb-
|
|
187
|
-
<Clock className="h-
|
|
295
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
296
|
+
<Clock className="h-3.5 w-3.5 text-muted-foreground" />
|
|
188
297
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Last Signal</span>
|
|
189
298
|
</div>
|
|
190
299
|
<span className="text-sm">
|
|
191
|
-
{account.data?.last_signal_at
|
|
192
|
-
? new Date(account.data.last_signal_at).toLocaleDateString()
|
|
193
|
-
: 'Never'
|
|
194
|
-
}
|
|
300
|
+
{account.data?.last_signal_at ? new Date(account.data.last_signal_at).toLocaleDateString() : 'Never'}
|
|
195
301
|
</span>
|
|
196
302
|
</div>
|
|
197
303
|
</div>
|
|
198
304
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<div className="
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
temp === 'hot' ? 'border-red-400 text-red-600 bg-red-50' :
|
|
207
|
-
temp === 'warm' ? 'border-yellow-400 text-yellow-600 bg-yellow-50' :
|
|
208
|
-
'border-blue-400 text-blue-600 bg-blue-50'
|
|
209
|
-
}`}
|
|
210
|
-
>
|
|
211
|
-
{temp}
|
|
212
|
-
</Badge>
|
|
305
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
306
|
+
{/* Left: Signal Timeline */}
|
|
307
|
+
<div className="space-y-3">
|
|
308
|
+
<h3 className="font-medium text-sm flex items-center gap-2">
|
|
309
|
+
<Zap className="h-4 w-4 text-muted-foreground" /> Signal Timeline
|
|
310
|
+
</h3>
|
|
311
|
+
<SignalTimeline accountId={account.id} />
|
|
213
312
|
</div>
|
|
214
|
-
)}
|
|
215
313
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
314
|
+
{/* Right: Score Breakdown + Claim Status */}
|
|
315
|
+
<div className="space-y-4">
|
|
316
|
+
<div className="border rounded-lg p-4 space-y-3">
|
|
317
|
+
<h3 className="font-medium text-sm flex items-center gap-2">
|
|
318
|
+
<TrendingUp className="h-4 w-4 text-muted-foreground" /> Signal Score Breakdown
|
|
319
|
+
</h3>
|
|
320
|
+
<ScoreBreakdown ratings={ratings} />
|
|
222
321
|
</div>
|
|
223
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
224
|
-
Type: {queue.primary_opportunity_type?.replace(/_/g, ' ') || 'Unknown'}
|
|
225
|
-
</p>
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
322
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<div className="flex items-center justify-between">
|
|
236
|
-
<span className="text-sm text-muted-foreground">Anonymous</span>
|
|
237
|
-
<div className="flex items-center gap-2">
|
|
238
|
-
<div className="flex gap-0.5">
|
|
239
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
240
|
-
<div
|
|
241
|
-
key={i}
|
|
242
|
-
className={`w-2 h-2 rounded-full ${i < ratings.anonymous!.rating ? 'bg-blue-500' : 'bg-gray-200'}`}
|
|
243
|
-
/>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
<span className="text-xs font-medium w-12 text-right">{ratings.anonymous.raw_score}</span>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
{ratings.identified && (
|
|
251
|
-
<div className="flex items-center justify-between">
|
|
252
|
-
<span className="text-sm text-muted-foreground">Identified</span>
|
|
253
|
-
<div className="flex items-center gap-2">
|
|
254
|
-
<div className="flex gap-0.5">
|
|
255
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
256
|
-
<div
|
|
257
|
-
key={i}
|
|
258
|
-
className={`w-2 h-2 rounded-full ${i < ratings.identified!.rating ? 'bg-green-500' : 'bg-gray-200'}`}
|
|
259
|
-
/>
|
|
260
|
-
))}
|
|
261
|
-
</div>
|
|
262
|
-
<span className="text-xs font-medium w-12 text-right">{ratings.identified.raw_score}</span>
|
|
263
|
-
</div>
|
|
323
|
+
{/* Opportunity queue alert */}
|
|
324
|
+
{queue?.pending_opportunity_id && (
|
|
325
|
+
<div className="border rounded-lg p-4 bg-yellow-50 border-yellow-200">
|
|
326
|
+
<div className="flex items-center gap-2">
|
|
327
|
+
<Filter className="h-4 w-4 text-yellow-600" />
|
|
328
|
+
<span className="font-medium text-sm">Opportunity in Queue</span>
|
|
264
329
|
</div>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<div className="flex gap-0.5">
|
|
271
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
272
|
-
<div
|
|
273
|
-
key={i}
|
|
274
|
-
className={`w-2 h-2 rounded-full ${i < ratings.engaged!.rating ? 'bg-orange-500' : 'bg-gray-200'}`}
|
|
275
|
-
/>
|
|
276
|
-
))}
|
|
277
|
-
</div>
|
|
278
|
-
<span className="text-xs font-medium w-12 text-right">{ratings.engaged.raw_score}</span>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
330
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
331
|
+
{queue.primary_opportunity_type?.replace(/_/g, ' ') || 'Pending review'}
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
285
335
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
336
|
+
{/* Identity / Claim Status */}
|
|
337
|
+
<div className="border rounded-lg p-4 space-y-3">
|
|
338
|
+
<h3 className="font-medium text-sm flex items-center gap-2">
|
|
339
|
+
<Link2 className="h-4 w-4 text-muted-foreground" /> Identity / Claim Status
|
|
340
|
+
</h3>
|
|
341
|
+
<div className="space-y-2 text-sm">
|
|
292
342
|
<div className="flex justify-between">
|
|
293
|
-
<span className="text-muted-foreground">
|
|
294
|
-
<span className="text-
|
|
295
|
-
<span className="font-medium">{attr.anonymous_first_touch.referrer || 'Direct'}</span>
|
|
296
|
-
<span className="text-xs text-muted-foreground block">
|
|
297
|
-
{attr.anonymous_first_touch.at ? new Date(attr.anonymous_first_touch.at).toLocaleDateString() : ''}
|
|
298
|
-
</span>
|
|
299
|
-
</span>
|
|
343
|
+
<span className="text-muted-foreground">Instance ID</span>
|
|
344
|
+
<span className="font-mono text-xs">{instanceId ? instanceId.slice(0, 14) + '…' : '—'}</span>
|
|
300
345
|
</div>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<span className="text-xs text-muted-foreground block">
|
|
308
|
-
{attr.identified_first_touch.at ? new Date(attr.identified_first_touch.at).toLocaleDateString() : ''}
|
|
309
|
-
</span>
|
|
310
|
-
</span>
|
|
346
|
+
<div className="flex justify-between items-center">
|
|
347
|
+
<span className="text-muted-foreground">Claim Status</span>
|
|
348
|
+
{claimStatus === 'claimed'
|
|
349
|
+
? <Badge className="text-xs bg-green-100 text-green-700 border-green-200">Claimed</Badge>
|
|
350
|
+
: <Badge variant="outline" className="text-xs">Unclaimed</Badge>
|
|
351
|
+
}
|
|
311
352
|
</div>
|
|
312
|
-
|
|
353
|
+
{!claimStatus && (
|
|
354
|
+
<Button variant="outline" size="sm" className="w-full mt-1 text-xs gap-1.5">
|
|
355
|
+
<Link2 className="h-3.5 w-3.5" /> Create Claim Link
|
|
356
|
+
</Button>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
313
359
|
</div>
|
|
314
360
|
</div>
|
|
315
|
-
|
|
361
|
+
</div>
|
|
316
362
|
|
|
317
|
-
{/* Empty
|
|
318
|
-
{!stage && !
|
|
363
|
+
{/* Empty state */}
|
|
364
|
+
{!stage && !temp && !ratings && !queue?.pending_opportunity_id && (
|
|
319
365
|
<div className="text-center py-12 text-muted-foreground">
|
|
320
366
|
<Filter className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
321
367
|
<p className="text-sm">No funnel data yet.</p>
|
|
@@ -381,7 +427,7 @@ export default function AccountDetailPage() {
|
|
|
381
427
|
|
|
382
428
|
<ScrollArea className="flex-1">
|
|
383
429
|
<TabsContent value="people" className="mt-0"><PeopleTab accountId={id!} /></TabsContent>
|
|
384
|
-
<TabsContent value="funnel" className="mt-0"><
|
|
430
|
+
<TabsContent value="funnel" className="mt-0"><FunnelTab account={account} /></TabsContent>
|
|
385
431
|
<TabsContent value="tickets" className="mt-0"><ItemsTab accountId={id!} typeSlug="support_ticket" emptyText="No tickets." /></TabsContent>
|
|
386
432
|
<TabsContent value="deals" className="mt-0"><ItemsTab accountId={id!} typeSlug="deal" emptyText="No deals." /></TabsContent>
|
|
387
433
|
<TabsContent value="health" className="mt-0"><ItemsTab accountId={id!} typeSlug="csm_health" emptyText="No health records." /></TabsContent>
|
|
@@ -11,11 +11,11 @@ interface FunnelSignal {
|
|
|
11
11
|
id: string
|
|
12
12
|
title: string
|
|
13
13
|
data: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
action?: { action_type?: string; action_value?: number }
|
|
15
|
+
identity?: { account_id?: string; person_id?: string; anonymous_id?: string }
|
|
16
|
+
classification?: { stage?: string; source?: string }
|
|
17
|
+
scoring_components?: { raw_score?: { rating?: number; calculated?: number } }
|
|
18
|
+
processing?: { received_at?: string; scored_at?: string }
|
|
19
19
|
}
|
|
20
20
|
created_at: string
|
|
21
21
|
}
|
|
@@ -95,7 +95,7 @@ export default function IntelligencePage() {
|
|
|
95
95
|
|
|
96
96
|
useEffect(() => {
|
|
97
97
|
if (accountsData) {
|
|
98
|
-
setAccounts(accountsData.filter((acc: any) => acc.data?.lead_score !== undefined))
|
|
98
|
+
setAccounts(accountsData.filter((acc: any) => acc.data?.lead_score !== undefined || acc.data?.lifecycle_stage))
|
|
99
99
|
}
|
|
100
100
|
}, [accountsData])
|
|
101
101
|
|
|
@@ -206,28 +206,35 @@ export default function IntelligencePage() {
|
|
|
206
206
|
<CardContent>
|
|
207
207
|
<div className="space-y-3">
|
|
208
208
|
{signals
|
|
209
|
-
.filter(signal => signal.data
|
|
209
|
+
.filter(signal => signal.data?.identity?.account_id === selectedAccountId)
|
|
210
210
|
.slice(0, 5)
|
|
211
|
-
.map((signal) =>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div>
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
211
|
+
.map((signal) => {
|
|
212
|
+
const actionType = signal.data?.action?.action_type
|
|
213
|
+
const rating = signal.data?.scoring_components?.raw_score?.rating
|
|
214
|
+
const receivedAt = signal.data?.processing?.received_at || signal.created_at
|
|
215
|
+
return (
|
|
216
|
+
<div key={signal.id} className="flex items-center justify-between p-3 border rounded">
|
|
217
|
+
<div className="flex items-center space-x-3">
|
|
218
|
+
<Badge className={getSignalTypeColor(actionType || '')}>
|
|
219
|
+
{actionType || signal.title}
|
|
220
|
+
</Badge>
|
|
221
|
+
<div>
|
|
222
|
+
<div className="font-medium">{signal.title}</div>
|
|
223
|
+
<div className="text-sm text-gray-500">
|
|
224
|
+
{new Date(receivedAt).toLocaleDateString()}
|
|
225
|
+
</div>
|
|
221
226
|
</div>
|
|
222
227
|
</div>
|
|
228
|
+
{rating !== undefined && (
|
|
229
|
+
<div className={`font-bold ${
|
|
230
|
+
rating >= 4 ? 'text-green-600' : rating >= 3 ? 'text-yellow-600' : 'text-gray-500'
|
|
231
|
+
}`}>
|
|
232
|
+
{rating}/5
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
223
235
|
</div>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}`}>
|
|
227
|
-
{signal.data.score_delta > 0 ? '+' : ''}{signal.data.score_delta}
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
))}
|
|
236
|
+
)
|
|
237
|
+
})}
|
|
231
238
|
</div>
|
|
232
239
|
</CardContent>
|
|
233
240
|
</Card>
|