@crmy/web 0.5.5 → 0.5.6
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/assets/index-CskfWp8E.js +560 -0
- package/dist/assets/index-D763l57m.css +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +4 -1
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -158
- package/src/api/client.ts +0 -82
- package/src/api/hooks.ts +0 -689
- package/src/components/CustomFields.tsx +0 -240
- package/src/components/NavLink.tsx +0 -28
- package/src/components/crm/AIFab.tsx +0 -37
- package/src/components/crm/AccountDrawer.tsx +0 -372
- package/src/components/crm/ActivityTimeline.tsx +0 -115
- package/src/components/crm/AssignmentDrawer.tsx +0 -396
- package/src/components/crm/BriefingPanel.tsx +0 -217
- package/src/components/crm/CommandPalette.tsx +0 -254
- package/src/components/crm/ContactAvatar.tsx +0 -49
- package/src/components/crm/ContactDrawer.tsx +0 -438
- package/src/components/crm/ContextPanel.tsx +0 -200
- package/src/components/crm/CrmWidgets.tsx +0 -417
- package/src/components/crm/DrawerShell.tsx +0 -77
- package/src/components/crm/ListToolbar.tsx +0 -252
- package/src/components/crm/OpportunityDrawer.tsx +0 -372
- package/src/components/crm/PaginationBar.tsx +0 -111
- package/src/components/crm/QuickAddDrawer.tsx +0 -652
- package/src/components/crm/ShortcutsOverlay.tsx +0 -65
- package/src/components/crm/UseCaseDrawer.tsx +0 -454
- package/src/components/layout/MobileNav.tsx +0 -49
- package/src/components/layout/Sidebar.tsx +0 -157
- package/src/components/layout/TopBar.tsx +0 -54
- package/src/components/settings/ActorsSettings.tsx +0 -1190
- package/src/components/ui/accordion.tsx +0 -52
- package/src/components/ui/alert-dialog.tsx +0 -104
- package/src/components/ui/alert.tsx +0 -43
- package/src/components/ui/aspect-ratio.tsx +0 -5
- package/src/components/ui/avatar.tsx +0 -38
- package/src/components/ui/badge.tsx +0 -29
- package/src/components/ui/breadcrumb.tsx +0 -90
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/calendar.tsx +0 -54
- package/src/components/ui/card.tsx +0 -43
- package/src/components/ui/carousel.tsx +0 -224
- package/src/components/ui/chart.tsx +0 -303
- package/src/components/ui/checkbox.tsx +0 -26
- package/src/components/ui/collapsible.tsx +0 -9
- package/src/components/ui/command.tsx +0 -132
- package/src/components/ui/context-menu.tsx +0 -178
- package/src/components/ui/date-picker.tsx +0 -313
- package/src/components/ui/dialog.tsx +0 -95
- package/src/components/ui/drawer.tsx +0 -87
- package/src/components/ui/dropdown-menu.tsx +0 -179
- package/src/components/ui/form.tsx +0 -129
- package/src/components/ui/hover-card.tsx +0 -27
- package/src/components/ui/input-otp.tsx +0 -61
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/menubar.tsx +0 -207
- package/src/components/ui/navigation-menu.tsx +0 -120
- package/src/components/ui/pagination.tsx +0 -81
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -23
- package/src/components/ui/radio-group.tsx +0 -36
- package/src/components/ui/resizable.tsx +0 -37
- package/src/components/ui/scroll-area.tsx +0 -38
- package/src/components/ui/select.tsx +0 -143
- package/src/components/ui/separator.tsx +0 -20
- package/src/components/ui/sheet.tsx +0 -107
- package/src/components/ui/sidebar.tsx +0 -637
- package/src/components/ui/skeleton.tsx +0 -7
- package/src/components/ui/slider.tsx +0 -23
- package/src/components/ui/sonner.tsx +0 -24
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -72
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -21
- package/src/components/ui/toast.tsx +0 -111
- package/src/components/ui/toaster.tsx +0 -24
- package/src/components/ui/toggle-group.tsx +0 -49
- package/src/components/ui/toggle.tsx +0 -37
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/components/ui/use-toast.ts +0 -1
- package/src/components/ui/utils.ts +0 -9
- package/src/contexts/AgentSettingsContext.tsx +0 -24
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-toast.ts +0 -186
- package/src/hooks/useKeyboardShortcuts.ts +0 -95
- package/src/hooks/useTheme.ts +0 -24
- package/src/index.css +0 -245
- package/src/lib/entityColors.ts +0 -18
- package/src/lib/stageConfig.ts +0 -32
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -25
- package/src/pages/Accounts.tsx +0 -205
- package/src/pages/Activities.tsx +0 -251
- package/src/pages/Agent.tsx +0 -237
- package/src/pages/AgentSettings.tsx +0 -544
- package/src/pages/Assignments.tsx +0 -750
- package/src/pages/Contacts.tsx +0 -200
- package/src/pages/Dashboard.tsx +0 -143
- package/src/pages/Inbox.tsx +0 -615
- package/src/pages/NotFound.tsx +0 -24
- package/src/pages/Opportunities.tsx +0 -386
- package/src/pages/SearchResults.tsx +0 -49
- package/src/pages/Settings.tsx +0 -1884
- package/src/pages/UseCases.tsx +0 -396
- package/src/pages/auth/Login.tsx +0 -261
- package/src/pages/hitl/HITL.tsx +0 -101
- package/src/store/appStore.ts +0 -103
- package/src/vite-env.d.ts +0 -14
- package/tailwind.config.js +0 -121
- package/tsconfig.json +0 -24
- package/vite.config.ts +0 -27
- /package/{public → dist}/android-chrome-192x192.png +0 -0
- /package/{public → dist}/android-chrome-512x512.png +0 -0
- /package/{public → dist}/apple-touch-icon.png +0 -0
- /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
- /package/{public → dist}/favicon-16x16.png +0 -0
- /package/{public → dist}/favicon-32x32.png +0 -0
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/site.webmanifest +0 -0
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { motion, AnimatePresence } from 'framer-motion';
|
|
2
|
-
import { useAppStore } from '@/store/appStore';
|
|
3
|
-
|
|
4
|
-
const shortcuts = [
|
|
5
|
-
{ group: 'Global', items: [
|
|
6
|
-
{ keys: ['⌘', 'K'], label: 'Command palette' },
|
|
7
|
-
{ keys: ['⌘', '⇧', 'A'], label: 'AI Agent panel' },
|
|
8
|
-
{ keys: ['⌘', '⇧', 'Z'], label: 'Zen mode' },
|
|
9
|
-
{ keys: ['?'], label: 'Keyboard shortcuts' },
|
|
10
|
-
{ keys: ['Esc'], label: 'Close drawer / modal' },
|
|
11
|
-
]},
|
|
12
|
-
{ group: 'Navigation', items: [
|
|
13
|
-
{ keys: ['G', 'H'], label: 'Go to Dashboard' },
|
|
14
|
-
{ keys: ['G', 'C'], label: 'Go to Contacts' },
|
|
15
|
-
{ keys: ['G', 'D'], label: 'Go to Opportunities' },
|
|
16
|
-
]},
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
export function ShortcutsOverlay() {
|
|
20
|
-
const { shortcutsOpen, setShortcutsOpen } = useAppStore();
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<AnimatePresence>
|
|
24
|
-
{shortcutsOpen && (
|
|
25
|
-
<>
|
|
26
|
-
<motion.div
|
|
27
|
-
initial={{ opacity: 0 }}
|
|
28
|
-
animate={{ opacity: 1 }}
|
|
29
|
-
exit={{ opacity: 0 }}
|
|
30
|
-
className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm"
|
|
31
|
-
onClick={() => setShortcutsOpen(false)}
|
|
32
|
-
/>
|
|
33
|
-
<motion.div
|
|
34
|
-
initial={{ opacity: 0, scale: 0.95 }}
|
|
35
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
36
|
-
exit={{ opacity: 0, scale: 0.95 }}
|
|
37
|
-
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md bg-card border border-border rounded-lg shadow-2xl p-6"
|
|
38
|
-
>
|
|
39
|
-
<h2 className="font-display font-bold text-lg text-foreground mb-4">Keyboard Shortcuts</h2>
|
|
40
|
-
{shortcuts.map((group) => (
|
|
41
|
-
<div key={group.group} className="mb-4">
|
|
42
|
-
<h3 className="text-xs font-mono text-muted-foreground uppercase tracking-wider mb-2">{group.group}</h3>
|
|
43
|
-
<div className="space-y-2">
|
|
44
|
-
{group.items.map((item) => (
|
|
45
|
-
<div key={item.label} className="flex items-center justify-between">
|
|
46
|
-
<span className="text-sm text-foreground">{item.label}</span>
|
|
47
|
-
<div className="flex gap-1">
|
|
48
|
-
{item.keys.map((key) => (
|
|
49
|
-
<kbd key={key} className="px-1.5 py-0.5 text-xs font-mono bg-muted text-muted-foreground rounded border border-border">
|
|
50
|
-
{key}
|
|
51
|
-
</kbd>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
))}
|
|
59
|
-
<p className="text-xs text-muted-foreground mt-4">Press <kbd className="px-1 py-0.5 text-xs font-mono bg-muted rounded border border-border">Esc</kbd> to close</p>
|
|
60
|
-
</motion.div>
|
|
61
|
-
</>
|
|
62
|
-
)}
|
|
63
|
-
</AnimatePresence>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 CRMy Contributors
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
import { useState } from 'react';
|
|
5
|
-
import { useUseCase, useUseCaseTimeline, useUpdateUseCase, useDeleteUseCase, useUsers, useCustomFields, useOpportunities } from '@/api/hooks';
|
|
6
|
-
import { useNavigate } from 'react-router-dom';
|
|
7
|
-
import { useAppStore } from '@/store/appStore';
|
|
8
|
-
import { useAgentSettings } from '@/contexts/AgentSettingsContext';
|
|
9
|
-
import { Sparkles, Calendar, Bot, DollarSign, Pencil, ChevronLeft, Trash2, FileText } from 'lucide-react';
|
|
10
|
-
import { ContextPanel } from './ContextPanel';
|
|
11
|
-
import { BriefingPanel } from './BriefingPanel';
|
|
12
|
-
import { CustomFieldsSection } from './CrmWidgets';
|
|
13
|
-
import { ActivityTimeline } from './ActivityTimeline';
|
|
14
|
-
import { useCaseStageConfig } from '@/lib/stageConfig';
|
|
15
|
-
import { toast } from '@/components/ui/use-toast';
|
|
16
|
-
import { DatePicker } from '@/components/ui/date-picker';
|
|
17
|
-
|
|
18
|
-
const inputClass = 'w-full h-10 px-3 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring';
|
|
19
|
-
const labelClass = 'text-xs font-mono text-muted-foreground uppercase tracking-wider';
|
|
20
|
-
|
|
21
|
-
const UC_STAGES = ['discovery', 'poc', 'production', 'scaling', 'sunset'];
|
|
22
|
-
|
|
23
|
-
function UseCaseStageBadge({ stage }: { stage: string }) {
|
|
24
|
-
const config = useCaseStageConfig[stage] ?? { label: stage, color: '#94a3b8' };
|
|
25
|
-
return (
|
|
26
|
-
<span
|
|
27
|
-
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
|
|
28
|
-
style={{ backgroundColor: config.color + '18', color: config.color }}
|
|
29
|
-
>
|
|
30
|
-
{config.label}
|
|
31
|
-
</span>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function UseCaseEditForm({
|
|
36
|
-
useCase,
|
|
37
|
-
onSave,
|
|
38
|
-
onCancel,
|
|
39
|
-
onDelete,
|
|
40
|
-
isSaving,
|
|
41
|
-
}: {
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
-
useCase: any;
|
|
44
|
-
onSave: (data: Record<string, unknown>) => void;
|
|
45
|
-
onCancel: () => void;
|
|
46
|
-
onDelete: () => void;
|
|
47
|
-
isSaving: boolean;
|
|
48
|
-
}) {
|
|
49
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
50
|
-
const [fields, setFields] = useState<Record<string, string>>({
|
|
51
|
-
name: useCase.name ?? '',
|
|
52
|
-
stage: useCase.stage ?? 'discovery',
|
|
53
|
-
description: useCase.description ?? '',
|
|
54
|
-
attributed_arr: useCase.attributed_arr != null ? String(useCase.attributed_arr) : '',
|
|
55
|
-
currency_code: useCase.currency_code ?? 'USD',
|
|
56
|
-
expansion_potential: useCase.expansion_potential != null ? String(useCase.expansion_potential) : '',
|
|
57
|
-
unit_label: useCase.unit_label ?? '',
|
|
58
|
-
consumption_unit: useCase.consumption_unit ?? '',
|
|
59
|
-
consumption_capacity: useCase.consumption_capacity != null ? String(useCase.consumption_capacity) : '',
|
|
60
|
-
started_at: useCase.started_at ? useCase.started_at.slice(0, 10) : '',
|
|
61
|
-
target_prod_date: useCase.target_prod_date ? useCase.target_prod_date.slice(0, 10) : '',
|
|
62
|
-
sunset_date: useCase.sunset_date ? useCase.sunset_date.slice(0, 10) : '',
|
|
63
|
-
tags: Array.isArray(useCase.tags) ? useCase.tags.join(', ') : '',
|
|
64
|
-
opportunity_id: useCase.opportunity_id ?? '',
|
|
65
|
-
owner_id: useCase.owner_id ?? '',
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const [customFieldValues, setCustomFieldValues] = useState<Record<string, string>>(() => {
|
|
69
|
-
const init: Record<string, string> = {};
|
|
70
|
-
if (useCase.custom_fields) {
|
|
71
|
-
for (const [k, v] of Object.entries(useCase.custom_fields as Record<string, unknown>)) {
|
|
72
|
-
init[k] = String(v ?? '');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return init;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
-
const { data: usersData } = useUsers() as any;
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
-
const users: any[] = usersData?.data ?? [];
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
const { data: oppsData } = useOpportunities({ limit: 200 }) as any;
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
-
const opportunities: any[] = oppsData?.data ?? [];
|
|
86
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
-
const { data: customFieldDefs } = useCustomFields('use_case') as any;
|
|
88
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
-
const fieldDefs: any[] = customFieldDefs?.fields ?? [];
|
|
90
|
-
|
|
91
|
-
const set = (key: string, val: string) => setFields(prev => ({ ...prev, [key]: val }));
|
|
92
|
-
const setCF = (key: string, val: string) => setCustomFieldValues(prev => ({ ...prev, [key]: val }));
|
|
93
|
-
|
|
94
|
-
const handleSave = () => {
|
|
95
|
-
const payload: Record<string, unknown> = {};
|
|
96
|
-
const numericKeys = ['attributed_arr', 'expansion_potential', 'consumption_capacity'];
|
|
97
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
98
|
-
if (k === 'tags') continue; // handled separately
|
|
99
|
-
if (v === '') continue;
|
|
100
|
-
if (numericKeys.includes(k)) payload[k] = Number(v) || 0;
|
|
101
|
-
else payload[k] = v;
|
|
102
|
-
}
|
|
103
|
-
// Tags: parse comma-separated string into array
|
|
104
|
-
const tagsRaw = fields.tags.trim();
|
|
105
|
-
payload.tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
106
|
-
const cfPayload: Record<string, unknown> = {};
|
|
107
|
-
for (const def of fieldDefs) {
|
|
108
|
-
const val = customFieldValues[def.field_key] ?? '';
|
|
109
|
-
if (val === '') continue;
|
|
110
|
-
if (def.field_type === 'number') cfPayload[def.field_key] = Number(val);
|
|
111
|
-
else if (def.field_type === 'boolean') cfPayload[def.field_key] = val === 'true';
|
|
112
|
-
else cfPayload[def.field_key] = val;
|
|
113
|
-
}
|
|
114
|
-
if (Object.keys(cfPayload).length > 0) payload.custom_fields = cfPayload;
|
|
115
|
-
onSave(payload);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const sectionLabel = (text: string) => (
|
|
119
|
-
<div className="border-t border-border pt-3 mt-1">
|
|
120
|
-
<p className={labelClass}>{text}</p>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div className="flex flex-col h-full">
|
|
126
|
-
<div className="flex items-center gap-2 px-5 py-3 border-b border-border">
|
|
127
|
-
<button onClick={onCancel} className="flex items-center gap-1 text-xs text-accent hover:underline">
|
|
128
|
-
<ChevronLeft className="w-3.5 h-3.5" /> Back
|
|
129
|
-
</button>
|
|
130
|
-
<span className="text-xs text-muted-foreground ml-auto">Editing use case</span>
|
|
131
|
-
</div>
|
|
132
|
-
<div className="flex-1 overflow-y-auto p-5 space-y-4">
|
|
133
|
-
|
|
134
|
-
{/* Basic */}
|
|
135
|
-
<div className="space-y-1.5">
|
|
136
|
-
<label className={labelClass}>Name<span className="text-destructive ml-0.5">*</span></label>
|
|
137
|
-
<input type="text" value={fields.name} onChange={e => set('name', e.target.value)} placeholder="Use case name" className={inputClass} />
|
|
138
|
-
</div>
|
|
139
|
-
<div className="space-y-1.5">
|
|
140
|
-
<label className={labelClass}>Stage</label>
|
|
141
|
-
<select value={fields.stage} onChange={e => set('stage', e.target.value)} className={`${inputClass} pr-3`}>
|
|
142
|
-
{UC_STAGES.map(s => (
|
|
143
|
-
<option key={s} value={s}>{useCaseStageConfig[s]?.label ?? s}</option>
|
|
144
|
-
))}
|
|
145
|
-
</select>
|
|
146
|
-
</div>
|
|
147
|
-
<div className="space-y-1.5">
|
|
148
|
-
<label className={labelClass}>Description</label>
|
|
149
|
-
<textarea
|
|
150
|
-
value={fields.description}
|
|
151
|
-
onChange={e => set('description', e.target.value)}
|
|
152
|
-
placeholder="Optional description"
|
|
153
|
-
rows={3}
|
|
154
|
-
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring resize-none"
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
<div className="space-y-1.5">
|
|
158
|
-
<label className={labelClass}>Tags <span className="normal-case font-normal">(comma-separated)</span></label>
|
|
159
|
-
<input type="text" value={fields.tags} onChange={e => set('tags', e.target.value)} placeholder="e.g. ai, billing, high-priority" className={inputClass} />
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
{/* Commercial */}
|
|
163
|
-
{sectionLabel('Commercial')}
|
|
164
|
-
<div className="space-y-1.5">
|
|
165
|
-
<label className={labelClass}>Attributed ARR ($)</label>
|
|
166
|
-
<input type="number" value={fields.attributed_arr} onChange={e => set('attributed_arr', e.target.value)} placeholder="e.g. 120000" className={inputClass} />
|
|
167
|
-
</div>
|
|
168
|
-
<div className="space-y-1.5">
|
|
169
|
-
<label className={labelClass}>Expansion Potential ($)</label>
|
|
170
|
-
<input type="number" value={fields.expansion_potential} onChange={e => set('expansion_potential', e.target.value)} placeholder="e.g. 50000" className={inputClass} />
|
|
171
|
-
</div>
|
|
172
|
-
<div className="space-y-1.5">
|
|
173
|
-
<label className={labelClass}>Currency</label>
|
|
174
|
-
<input type="text" value={fields.currency_code} onChange={e => set('currency_code', e.target.value.toUpperCase().slice(0, 3))} placeholder="USD" maxLength={3} className={inputClass} />
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{/* Consumption */}
|
|
178
|
-
{sectionLabel('Consumption')}
|
|
179
|
-
<div className="space-y-1.5">
|
|
180
|
-
<label className={labelClass}>Unit Label</label>
|
|
181
|
-
<input type="text" value={fields.unit_label} onChange={e => set('unit_label', e.target.value)} placeholder="e.g. API calls, seats, documents" className={inputClass} />
|
|
182
|
-
</div>
|
|
183
|
-
<div className="space-y-1.5">
|
|
184
|
-
<label className={labelClass}>Consumption Unit</label>
|
|
185
|
-
<input type="text" value={fields.consumption_unit} onChange={e => set('consumption_unit', e.target.value)} placeholder="e.g. calls/month" className={inputClass} />
|
|
186
|
-
</div>
|
|
187
|
-
<div className="space-y-1.5">
|
|
188
|
-
<label className={labelClass}>Consumption Capacity</label>
|
|
189
|
-
<input type="number" value={fields.consumption_capacity} onChange={e => set('consumption_capacity', e.target.value)} placeholder="e.g. 10000" className={inputClass} />
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
{/* Timeline */}
|
|
193
|
-
{sectionLabel('Timeline')}
|
|
194
|
-
<div className="space-y-1.5">
|
|
195
|
-
<label className={labelClass}>Start Date</label>
|
|
196
|
-
<DatePicker value={fields.started_at} onChange={val => set('started_at', val)} placeholder="Select start date" />
|
|
197
|
-
</div>
|
|
198
|
-
<div className="space-y-1.5">
|
|
199
|
-
<label className={labelClass}>Production Date</label>
|
|
200
|
-
<DatePicker value={fields.target_prod_date} onChange={val => set('target_prod_date', val)} placeholder="Select production date" />
|
|
201
|
-
</div>
|
|
202
|
-
<div className="space-y-1.5">
|
|
203
|
-
<label className={labelClass}>Sunset Date</label>
|
|
204
|
-
<DatePicker value={fields.sunset_date} onChange={val => set('sunset_date', val)} placeholder="Select sunset date" />
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
{/* Ownership */}
|
|
208
|
-
{sectionLabel('Ownership')}
|
|
209
|
-
{opportunities.length > 0 && (
|
|
210
|
-
<div className="space-y-1.5">
|
|
211
|
-
<label className={labelClass}>Linked Opportunity</label>
|
|
212
|
-
<select value={fields.opportunity_id} onChange={e => set('opportunity_id', e.target.value)} className={`${inputClass} pr-3`}>
|
|
213
|
-
<option value="">None</option>
|
|
214
|
-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
215
|
-
{opportunities.map((o: any) => (
|
|
216
|
-
<option key={o.id} value={o.id}>{o.name}</option>
|
|
217
|
-
))}
|
|
218
|
-
</select>
|
|
219
|
-
</div>
|
|
220
|
-
)}
|
|
221
|
-
{users.length > 0 && (
|
|
222
|
-
<div className="space-y-1.5">
|
|
223
|
-
<label className={labelClass}>Owner</label>
|
|
224
|
-
<select value={fields.owner_id} onChange={e => set('owner_id', e.target.value)} className={`${inputClass} pr-3`}>
|
|
225
|
-
<option value="">Unassigned</option>
|
|
226
|
-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
227
|
-
{users.map((u: any) => (
|
|
228
|
-
<option key={u.id} value={u.id}>{u.name || u.email}</option>
|
|
229
|
-
))}
|
|
230
|
-
</select>
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
|
|
234
|
-
{/* Custom Fields */}
|
|
235
|
-
{fieldDefs.length > 0 && (
|
|
236
|
-
<>
|
|
237
|
-
{sectionLabel('Custom Fields')}
|
|
238
|
-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
239
|
-
{fieldDefs.map((def: any) => (
|
|
240
|
-
<div key={def.field_key} className="space-y-1.5">
|
|
241
|
-
<label className={labelClass}>{def.label}{def.required && <span className="text-destructive ml-0.5">*</span>}</label>
|
|
242
|
-
{(def.field_type === 'text' || !def.field_type) && (
|
|
243
|
-
<input type="text" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
|
|
244
|
-
)}
|
|
245
|
-
{def.field_type === 'number' && (
|
|
246
|
-
<input type="number" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
|
|
247
|
-
)}
|
|
248
|
-
{def.field_type === 'date' && (
|
|
249
|
-
<DatePicker value={customFieldValues[def.field_key] ?? ''} onChange={val => setCF(def.field_key, val)} required={def.required} />
|
|
250
|
-
)}
|
|
251
|
-
{def.field_type === 'boolean' && (
|
|
252
|
-
<div className="flex items-center gap-2 h-10">
|
|
253
|
-
<input type="checkbox" checked={customFieldValues[def.field_key] === 'true'} onChange={e => setCF(def.field_key, e.target.checked ? 'true' : 'false')} className="w-4 h-4 rounded border-border accent-primary" />
|
|
254
|
-
<span className="text-sm text-foreground">Yes</span>
|
|
255
|
-
</div>
|
|
256
|
-
)}
|
|
257
|
-
{(def.field_type === 'select' || def.field_type === 'multi_select') && (
|
|
258
|
-
<select value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={`${inputClass} pr-3`}>
|
|
259
|
-
<option value="">Select…</option>
|
|
260
|
-
{(def.options ?? []).map((opt: string) => <option key={opt} value={opt}>{opt}</option>)}
|
|
261
|
-
</select>
|
|
262
|
-
)}
|
|
263
|
-
</div>
|
|
264
|
-
))}
|
|
265
|
-
</>
|
|
266
|
-
)}
|
|
267
|
-
|
|
268
|
-
{useCase.created_at && (
|
|
269
|
-
<div className="flex items-center justify-between py-2 border-t border-border mt-2">
|
|
270
|
-
<span className="text-xs text-muted-foreground">Created</span>
|
|
271
|
-
<span className="text-xs text-muted-foreground">{new Date(useCase.created_at as string).toLocaleDateString()}</span>
|
|
272
|
-
</div>
|
|
273
|
-
)}
|
|
274
|
-
<button
|
|
275
|
-
onClick={handleSave}
|
|
276
|
-
disabled={!fields.name.trim() || isSaving}
|
|
277
|
-
className="w-full h-10 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-40 transition-colors"
|
|
278
|
-
>
|
|
279
|
-
{isSaving ? 'Saving…' : 'Save Changes'}
|
|
280
|
-
</button>
|
|
281
|
-
{!confirmDelete ? (
|
|
282
|
-
<button
|
|
283
|
-
onClick={() => setConfirmDelete(true)}
|
|
284
|
-
className="w-full h-9 rounded-md border border-destructive/40 text-destructive text-sm font-medium hover:bg-destructive/10 transition-colors flex items-center justify-center gap-1.5"
|
|
285
|
-
>
|
|
286
|
-
<Trash2 className="w-3.5 h-3.5" /> Delete Use Case
|
|
287
|
-
</button>
|
|
288
|
-
) : (
|
|
289
|
-
<div className="flex gap-2">
|
|
290
|
-
<button onClick={() => setConfirmDelete(false)} className="flex-1 h-9 rounded-md border border-border text-sm text-muted-foreground hover:bg-muted/50 transition-colors">
|
|
291
|
-
Cancel
|
|
292
|
-
</button>
|
|
293
|
-
<button onClick={onDelete} className="flex-1 h-9 rounded-md bg-destructive text-destructive-foreground text-sm font-medium hover:bg-destructive/90 transition-colors">
|
|
294
|
-
Confirm Delete
|
|
295
|
-
</button>
|
|
296
|
-
</div>
|
|
297
|
-
)}
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function UseCaseDrawer() {
|
|
304
|
-
const { drawerEntityId, openAIWithContext, closeDrawer } = useAppStore();
|
|
305
|
-
const { enabled: agentEnabled } = useAgentSettings();
|
|
306
|
-
const navigate = useNavigate();
|
|
307
|
-
const [editing, setEditing] = useState(false);
|
|
308
|
-
const [briefing, setBriefing] = useState(false);
|
|
309
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
-
const { data: useCaseData, isLoading } = useUseCase(drawerEntityId ?? '') as any;
|
|
311
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
312
|
-
const { data: timelineData } = useUseCaseTimeline(drawerEntityId ?? '') as any;
|
|
313
|
-
const updateUseCase = useUpdateUseCase(drawerEntityId ?? '');
|
|
314
|
-
const deleteUseCase = useDeleteUseCase(drawerEntityId ?? '');
|
|
315
|
-
|
|
316
|
-
if (isLoading) {
|
|
317
|
-
return (
|
|
318
|
-
<div className="flex flex-col gap-4 p-6 animate-pulse">
|
|
319
|
-
<div className="h-6 bg-muted rounded w-3/4" />
|
|
320
|
-
<div className="h-4 bg-muted rounded w-1/2" />
|
|
321
|
-
</div>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!useCaseData?.use_case) {
|
|
326
|
-
return <div className="p-4 text-muted-foreground">Use case not found</div>;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const useCase = useCaseData.use_case;
|
|
330
|
-
const name: string = useCase.name ?? '';
|
|
331
|
-
const stage: string = useCase.stage ?? '';
|
|
332
|
-
const arr: number = useCase.attributed_arr ?? 0;
|
|
333
|
-
const healthScore: number = useCase.health_score ?? 0;
|
|
334
|
-
|
|
335
|
-
const timeline: Array<Record<string, unknown>> = timelineData?.data ?? timelineData ?? [];
|
|
336
|
-
|
|
337
|
-
if (briefing) {
|
|
338
|
-
return <BriefingPanel subjectType="use_case" subjectId={drawerEntityId!} onClose={() => setBriefing(false)} />;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (editing) {
|
|
342
|
-
return (
|
|
343
|
-
<UseCaseEditForm
|
|
344
|
-
useCase={useCase}
|
|
345
|
-
onSave={async (data) => {
|
|
346
|
-
try {
|
|
347
|
-
await updateUseCase.mutateAsync(data);
|
|
348
|
-
setEditing(false);
|
|
349
|
-
toast({ title: 'Use case updated' });
|
|
350
|
-
} catch (err) {
|
|
351
|
-
toast({ title: 'Failed to update use case', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
|
|
352
|
-
}
|
|
353
|
-
}}
|
|
354
|
-
onCancel={() => setEditing(false)}
|
|
355
|
-
onDelete={async () => {
|
|
356
|
-
try {
|
|
357
|
-
await deleteUseCase.mutateAsync();
|
|
358
|
-
closeDrawer();
|
|
359
|
-
toast({ title: 'Use case deleted' });
|
|
360
|
-
} catch (err) {
|
|
361
|
-
toast({ title: 'Failed to delete use case', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
isSaving={updateUseCase.isPending}
|
|
365
|
-
/>
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return (
|
|
370
|
-
<div className="flex flex-col">
|
|
371
|
-
{/* Header */}
|
|
372
|
-
<div className="p-6 border-b border-border">
|
|
373
|
-
<h2 className="font-display font-extrabold text-xl text-foreground">{name}</h2>
|
|
374
|
-
<div className="flex items-center gap-2 mt-3">
|
|
375
|
-
{stage && <UseCaseStageBadge stage={stage} />}
|
|
376
|
-
</div>
|
|
377
|
-
<div className="flex gap-2 mt-4">
|
|
378
|
-
<button
|
|
379
|
-
onClick={() => setEditing(true)}
|
|
380
|
-
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-muted text-foreground text-sm font-medium hover:bg-muted/80 transition-all press-scale"
|
|
381
|
-
>
|
|
382
|
-
<Pencil className="w-3.5 h-3.5" /> Edit
|
|
383
|
-
</button>
|
|
384
|
-
<button
|
|
385
|
-
onClick={() => setBriefing(true)}
|
|
386
|
-
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-muted text-foreground text-sm font-medium hover:bg-muted/80 transition-all press-scale"
|
|
387
|
-
>
|
|
388
|
-
<FileText className="w-3.5 h-3.5" /> Brief
|
|
389
|
-
</button>
|
|
390
|
-
{agentEnabled && (
|
|
391
|
-
<button
|
|
392
|
-
onClick={() => {
|
|
393
|
-
openAIWithContext({ type: 'use-case', id: useCase.id, name, detail: stage });
|
|
394
|
-
closeDrawer();
|
|
395
|
-
navigate('/agent');
|
|
396
|
-
}}
|
|
397
|
-
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl border border-accent/30 bg-accent/5 text-accent text-sm font-semibold hover:bg-accent/10 transition-all ml-auto press-scale"
|
|
398
|
-
>
|
|
399
|
-
<Sparkles className="w-3.5 h-3.5" /> Chat
|
|
400
|
-
</button>
|
|
401
|
-
)}
|
|
402
|
-
</div>
|
|
403
|
-
</div>
|
|
404
|
-
|
|
405
|
-
{/* Stats */}
|
|
406
|
-
<div className="grid grid-cols-3 gap-3 p-4 mx-4 mt-4">
|
|
407
|
-
{[
|
|
408
|
-
{ icon: DollarSign, label: 'Attributed ARR', value: arr ? `$${(arr / 1000).toFixed(0)}K` : '—' },
|
|
409
|
-
{ icon: Calendar, label: 'Stage', value: (useCaseStageConfig[stage]?.label ?? stage) || '—' },
|
|
410
|
-
{ icon: Bot, label: 'Health', value: healthScore ? String(healthScore) : '—' },
|
|
411
|
-
].map((stat) => (
|
|
412
|
-
<div key={stat.label} className="bg-muted/50 rounded-xl p-3 text-center">
|
|
413
|
-
<stat.icon className="w-4 h-4 text-muted-foreground mx-auto mb-1" />
|
|
414
|
-
<p className="text-sm font-display font-bold text-foreground truncate">{stat.value}</p>
|
|
415
|
-
<p className="text-[10px] text-muted-foreground">{stat.label}</p>
|
|
416
|
-
</div>
|
|
417
|
-
))}
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
{/* Details */}
|
|
421
|
-
<div className="p-4 mx-4 mt-2 space-y-3">
|
|
422
|
-
<h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide">Details</h3>
|
|
423
|
-
{[
|
|
424
|
-
{ label: 'Stage', value: useCaseStageConfig[stage]?.label ?? stage },
|
|
425
|
-
{ label: 'Health Score', value: healthScore ? String(healthScore) : undefined },
|
|
426
|
-
{ label: 'Prod Date', value: useCase.target_prod_date ? new Date(useCase.target_prod_date as string).toLocaleDateString() : undefined },
|
|
427
|
-
{ label: 'Created', value: useCase.created_at ? new Date(useCase.created_at as string).toLocaleDateString() : undefined },
|
|
428
|
-
]
|
|
429
|
-
.filter((f) => f.value)
|
|
430
|
-
.map((field) => (
|
|
431
|
-
<div key={field.label} className="flex items-center justify-between">
|
|
432
|
-
<span className="text-xs text-muted-foreground">{field.label}</span>
|
|
433
|
-
<span className="text-sm text-foreground">{field.value}</span>
|
|
434
|
-
</div>
|
|
435
|
-
))}
|
|
436
|
-
</div>
|
|
437
|
-
|
|
438
|
-
{/* Custom Fields */}
|
|
439
|
-
<CustomFieldsSection objectType="use_case" values={(useCase.custom_fields ?? {}) as Record<string, unknown>} />
|
|
440
|
-
|
|
441
|
-
{/* Context */}
|
|
442
|
-
<ContextPanel subjectType="use_case" subjectId={drawerEntityId!} />
|
|
443
|
-
|
|
444
|
-
{/* Timeline */}
|
|
445
|
-
{timeline.length > 0 && (
|
|
446
|
-
<div className="p-4 mx-4 mt-2 mb-6">
|
|
447
|
-
<h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide mb-3">Timeline</h3>
|
|
448
|
-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
449
|
-
<ActivityTimeline activities={timeline as any[]} />
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
</div>
|
|
453
|
-
);
|
|
454
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { Link, useLocation } from 'react-router-dom';
|
|
2
|
-
import { LayoutDashboard, Users, Building2, Briefcase, Activity, Settings } from 'lucide-react';
|
|
3
|
-
import { motion } from 'framer-motion';
|
|
4
|
-
|
|
5
|
-
const tabs = [
|
|
6
|
-
{ icon: LayoutDashboard, label: 'Home', path: '/' },
|
|
7
|
-
{ icon: Users, label: 'Contacts', path: '/contacts' },
|
|
8
|
-
{ icon: Building2, label: 'Accounts', path: '/accounts' },
|
|
9
|
-
{ icon: Briefcase, label: 'Opps', path: '/opportunities' },
|
|
10
|
-
{ icon: Activity, label: 'Activity', path: '/activities' },
|
|
11
|
-
{ icon: Settings, label: 'Settings', path: '/settings' },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
export function MobileNav() {
|
|
15
|
-
const location = useLocation();
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 flex items-center justify-around h-16 bg-card/95 backdrop-blur-md border-t border-border safe-area-bottom">
|
|
19
|
-
{tabs.map((tab) => {
|
|
20
|
-
const active = tab.path === '/' ? location.pathname === '/' : location.pathname.startsWith(tab.path);
|
|
21
|
-
return (
|
|
22
|
-
<Link
|
|
23
|
-
key={tab.path}
|
|
24
|
-
to={tab.path}
|
|
25
|
-
className="relative flex flex-col items-center justify-center py-1 min-w-[56px] min-h-[44px]"
|
|
26
|
-
>
|
|
27
|
-
{active && (
|
|
28
|
-
<motion.div
|
|
29
|
-
layoutId="mobile-nav-pill"
|
|
30
|
-
className="absolute inset-x-1 -top-0.5 bottom-1 rounded-2xl bg-primary/10"
|
|
31
|
-
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
|
32
|
-
/>
|
|
33
|
-
)}
|
|
34
|
-
<tab.icon className={`w-6 h-6 relative z-10 transition-colors ${active ? 'text-primary' : 'text-muted-foreground'}`} />
|
|
35
|
-
{active && (
|
|
36
|
-
<motion.span
|
|
37
|
-
initial={{ opacity: 0, y: 4 }}
|
|
38
|
-
animate={{ opacity: 1, y: 0 }}
|
|
39
|
-
className="text-[10px] font-display font-semibold text-primary relative z-10 mt-0.5"
|
|
40
|
-
>
|
|
41
|
-
{tab.label}
|
|
42
|
-
</motion.span>
|
|
43
|
-
)}
|
|
44
|
-
</Link>
|
|
45
|
-
);
|
|
46
|
-
})}
|
|
47
|
-
</nav>
|
|
48
|
-
);
|
|
49
|
-
}
|