@crmy/web 0.5.1 → 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.
Files changed (121) hide show
  1. package/dist/assets/index-CskfWp8E.js +560 -0
  2. package/dist/assets/index-D763l57m.css +1 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +4 -1
  5. package/postcss.config.js +0 -6
  6. package/src/App.tsx +0 -158
  7. package/src/api/client.ts +0 -82
  8. package/src/api/hooks.ts +0 -689
  9. package/src/components/CustomFields.tsx +0 -240
  10. package/src/components/NavLink.tsx +0 -28
  11. package/src/components/crm/AIFab.tsx +0 -37
  12. package/src/components/crm/AccountDrawer.tsx +0 -372
  13. package/src/components/crm/ActivityTimeline.tsx +0 -115
  14. package/src/components/crm/AssignmentDrawer.tsx +0 -396
  15. package/src/components/crm/BriefingPanel.tsx +0 -217
  16. package/src/components/crm/CommandPalette.tsx +0 -254
  17. package/src/components/crm/ContactAvatar.tsx +0 -49
  18. package/src/components/crm/ContactDrawer.tsx +0 -438
  19. package/src/components/crm/ContextPanel.tsx +0 -200
  20. package/src/components/crm/CrmWidgets.tsx +0 -417
  21. package/src/components/crm/DrawerShell.tsx +0 -77
  22. package/src/components/crm/ListToolbar.tsx +0 -252
  23. package/src/components/crm/OpportunityDrawer.tsx +0 -372
  24. package/src/components/crm/PaginationBar.tsx +0 -111
  25. package/src/components/crm/QuickAddDrawer.tsx +0 -652
  26. package/src/components/crm/ShortcutsOverlay.tsx +0 -65
  27. package/src/components/crm/UseCaseDrawer.tsx +0 -454
  28. package/src/components/layout/MobileNav.tsx +0 -49
  29. package/src/components/layout/Sidebar.tsx +0 -157
  30. package/src/components/layout/TopBar.tsx +0 -54
  31. package/src/components/settings/ActorsSettings.tsx +0 -1190
  32. package/src/components/ui/accordion.tsx +0 -52
  33. package/src/components/ui/alert-dialog.tsx +0 -104
  34. package/src/components/ui/alert.tsx +0 -43
  35. package/src/components/ui/aspect-ratio.tsx +0 -5
  36. package/src/components/ui/avatar.tsx +0 -38
  37. package/src/components/ui/badge.tsx +0 -29
  38. package/src/components/ui/breadcrumb.tsx +0 -90
  39. package/src/components/ui/button.tsx +0 -47
  40. package/src/components/ui/calendar.tsx +0 -54
  41. package/src/components/ui/card.tsx +0 -43
  42. package/src/components/ui/carousel.tsx +0 -224
  43. package/src/components/ui/chart.tsx +0 -303
  44. package/src/components/ui/checkbox.tsx +0 -26
  45. package/src/components/ui/collapsible.tsx +0 -9
  46. package/src/components/ui/command.tsx +0 -132
  47. package/src/components/ui/context-menu.tsx +0 -178
  48. package/src/components/ui/date-picker.tsx +0 -313
  49. package/src/components/ui/dialog.tsx +0 -95
  50. package/src/components/ui/drawer.tsx +0 -87
  51. package/src/components/ui/dropdown-menu.tsx +0 -179
  52. package/src/components/ui/form.tsx +0 -129
  53. package/src/components/ui/hover-card.tsx +0 -27
  54. package/src/components/ui/input-otp.tsx +0 -61
  55. package/src/components/ui/input.tsx +0 -22
  56. package/src/components/ui/label.tsx +0 -17
  57. package/src/components/ui/menubar.tsx +0 -207
  58. package/src/components/ui/navigation-menu.tsx +0 -120
  59. package/src/components/ui/pagination.tsx +0 -81
  60. package/src/components/ui/popover.tsx +0 -29
  61. package/src/components/ui/progress.tsx +0 -23
  62. package/src/components/ui/radio-group.tsx +0 -36
  63. package/src/components/ui/resizable.tsx +0 -37
  64. package/src/components/ui/scroll-area.tsx +0 -38
  65. package/src/components/ui/select.tsx +0 -143
  66. package/src/components/ui/separator.tsx +0 -20
  67. package/src/components/ui/sheet.tsx +0 -107
  68. package/src/components/ui/sidebar.tsx +0 -637
  69. package/src/components/ui/skeleton.tsx +0 -7
  70. package/src/components/ui/slider.tsx +0 -23
  71. package/src/components/ui/sonner.tsx +0 -24
  72. package/src/components/ui/switch.tsx +0 -27
  73. package/src/components/ui/table.tsx +0 -72
  74. package/src/components/ui/tabs.tsx +0 -53
  75. package/src/components/ui/textarea.tsx +0 -21
  76. package/src/components/ui/toast.tsx +0 -111
  77. package/src/components/ui/toaster.tsx +0 -24
  78. package/src/components/ui/toggle-group.tsx +0 -49
  79. package/src/components/ui/toggle.tsx +0 -37
  80. package/src/components/ui/tooltip.tsx +0 -28
  81. package/src/components/ui/use-toast.ts +0 -1
  82. package/src/components/ui/utils.ts +0 -9
  83. package/src/contexts/AgentSettingsContext.tsx +0 -24
  84. package/src/hooks/use-mobile.tsx +0 -19
  85. package/src/hooks/use-toast.ts +0 -186
  86. package/src/hooks/useKeyboardShortcuts.ts +0 -95
  87. package/src/hooks/useTheme.ts +0 -24
  88. package/src/index.css +0 -245
  89. package/src/lib/entityColors.ts +0 -18
  90. package/src/lib/stageConfig.ts +0 -32
  91. package/src/lib/utils.ts +0 -6
  92. package/src/main.tsx +0 -25
  93. package/src/pages/Accounts.tsx +0 -205
  94. package/src/pages/Activities.tsx +0 -251
  95. package/src/pages/Agent.tsx +0 -237
  96. package/src/pages/AgentSettings.tsx +0 -544
  97. package/src/pages/Assignments.tsx +0 -750
  98. package/src/pages/Contacts.tsx +0 -200
  99. package/src/pages/Dashboard.tsx +0 -143
  100. package/src/pages/Inbox.tsx +0 -615
  101. package/src/pages/NotFound.tsx +0 -24
  102. package/src/pages/Opportunities.tsx +0 -386
  103. package/src/pages/SearchResults.tsx +0 -49
  104. package/src/pages/Settings.tsx +0 -1884
  105. package/src/pages/UseCases.tsx +0 -396
  106. package/src/pages/auth/Login.tsx +0 -261
  107. package/src/pages/hitl/HITL.tsx +0 -101
  108. package/src/store/appStore.ts +0 -103
  109. package/src/vite-env.d.ts +0 -14
  110. package/tailwind.config.js +0 -121
  111. package/tsconfig.json +0 -24
  112. package/vite.config.ts +0 -27
  113. /package/{public → dist}/android-chrome-192x192.png +0 -0
  114. /package/{public → dist}/android-chrome-512x512.png +0 -0
  115. /package/{public → dist}/apple-touch-icon.png +0 -0
  116. /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
  117. /package/{public → dist}/favicon-16x16.png +0 -0
  118. /package/{public → dist}/favicon-32x32.png +0 -0
  119. /package/{public → dist}/favicon.ico +0 -0
  120. /package/{public → dist}/favicon.svg +0 -0
  121. /package/{public → dist}/site.webmanifest +0 -0
@@ -1,115 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { activityIcon } from './CrmWidgets';
5
- import { useAppStore } from '@/store/appStore';
6
- import { Bot } from 'lucide-react';
7
-
8
- const OUTCOME_COLORS: Record<string, string> = {
9
- connected: 'hsl(152, 55%, 42%)',
10
- positive: 'hsl(152, 55%, 42%)',
11
- voicemail: 'hsl(38, 92%, 50%)',
12
- neutral: 'hsl(38, 92%, 50%)',
13
- follow_up_needed: 'hsl(38, 92%, 50%)',
14
- negative: 'hsl(var(--destructive))',
15
- no_show: 'hsl(var(--destructive))',
16
- };
17
-
18
- type DrawerType = 'contact' | 'opportunity' | 'use-case' | 'account';
19
- const SUBJECT_TYPE_DRAWER: Record<string, DrawerType> = {
20
- contact: 'contact',
21
- account: 'account',
22
- opportunity: 'opportunity',
23
- use_case: 'use-case',
24
- };
25
-
26
- export interface TimelineActivity {
27
- id: string;
28
- type: string;
29
- subject?: string;
30
- description?: string;
31
- body?: string;
32
- note?: string;
33
- created_at?: string;
34
- occurred_at?: string;
35
- outcome?: string;
36
- performed_by?: string;
37
- performer_name?: string;
38
- subject_type?: string;
39
- subject_id?: string;
40
- contact_id?: string;
41
- }
42
-
43
- interface ActivityTimelineProps {
44
- activities: TimelineActivity[];
45
- emptyMessage?: string;
46
- }
47
-
48
- export function ActivityTimeline({ activities, emptyMessage = 'No activity yet.' }: ActivityTimelineProps) {
49
- const { openDrawer } = useAppStore();
50
-
51
- if (activities.length === 0) {
52
- return <p className="text-sm text-muted-foreground">{emptyMessage}</p>;
53
- }
54
-
55
- return (
56
- <div className="space-y-1">
57
- {activities.map((a, i) => {
58
- const desc = a.description ?? a.body ?? a.note ?? a.subject ?? '';
59
- const ts = a.occurred_at ?? a.created_at ?? '';
60
- const hasSubjectLink = a.subject_type && a.subject_id;
61
- const isClickable = hasSubjectLink || !!a.contact_id;
62
-
63
- const handleClick = () => {
64
- if (hasSubjectLink) {
65
- const drawerType = SUBJECT_TYPE_DRAWER[a.subject_type!];
66
- if (drawerType) openDrawer(drawerType, a.subject_id!);
67
- } else if (a.contact_id) {
68
- openDrawer('contact', a.contact_id);
69
- }
70
- };
71
-
72
- return (
73
- <div
74
- key={a.id ?? i}
75
- className={`flex gap-3 py-2 ${isClickable ? 'cursor-pointer hover:bg-muted/40 rounded-xl px-2 -mx-2 transition-colors' : ''}`}
76
- onClick={isClickable ? handleClick : undefined}
77
- >
78
- <div className="w-7 h-7 rounded-xl bg-muted flex items-center justify-center text-muted-foreground flex-shrink-0 mt-0.5">
79
- {activityIcon(a.type)}
80
- </div>
81
- <div className="flex-1 min-w-0">
82
- <div className="flex items-start gap-2">
83
- <p className="text-sm text-foreground flex-1 min-w-0 truncate">
84
- {desc || a.type}
85
- </p>
86
- {a.outcome && (
87
- <span
88
- className="inline-flex items-center px-1.5 py-0.5 rounded-md text-[10px] font-medium capitalize flex-shrink-0"
89
- style={{
90
- backgroundColor: (OUTCOME_COLORS[a.outcome] ?? 'hsl(var(--muted-foreground))') + '18',
91
- color: OUTCOME_COLORS[a.outcome] ?? 'hsl(var(--muted-foreground))',
92
- }}
93
- >
94
- {a.outcome.replace(/_/g, ' ')}
95
- </span>
96
- )}
97
- </div>
98
- <div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
99
- {a.performer_name && (
100
- <span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground">
101
- <Bot className="w-2.5 h-2.5" />
102
- {a.performer_name}
103
- </span>
104
- )}
105
- <span className="text-xs text-muted-foreground">
106
- {ts ? new Date(ts).toLocaleDateString() : ''}
107
- </span>
108
- </div>
109
- </div>
110
- </div>
111
- );
112
- })}
113
- </div>
114
- );
115
- }
@@ -1,396 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState } from 'react';
5
- import {
6
- useAssignment, useUpdateAssignment,
7
- useAcceptAssignment, useStartAssignment, useCompleteAssignment,
8
- useDeclineAssignment, useBlockAssignment, useCancelAssignment,
9
- useActor,
10
- } from '@/api/hooks';
11
- import { useAppStore } from '@/store/appStore';
12
- import {
13
- Pencil, ChevronLeft, ClipboardList,
14
- Play, CheckCircle2, XCircle, Ban, AlertOctagon,
15
- } from 'lucide-react';
16
- import { DatePicker } from '@/components/ui/date-picker';
17
- import { toast } from '@/components/ui/use-toast';
18
-
19
- 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';
20
- const labelClass = 'text-xs font-mono text-muted-foreground uppercase tracking-wider';
21
-
22
- const STATUS_COLORS: Record<string, string> = {
23
- pending: '#f59e0b',
24
- accepted: '#3b82f6',
25
- in_progress: '#8b5cf6',
26
- blocked: '#ef4444',
27
- completed: '#22c55e',
28
- declined: '#94a3b8',
29
- cancelled: '#94a3b8',
30
- };
31
-
32
- const PRIORITY_COLORS: Record<string, string> = {
33
- urgent: '#ef4444',
34
- high: '#f97316',
35
- normal: '#3b82f6',
36
- low: '#94a3b8',
37
- };
38
-
39
- const ASSIGNMENT_TYPES = ['call', 'draft', 'email', 'follow_up', 'research', 'review', 'send'];
40
- const PRIORITIES = ['urgent', 'high', 'normal', 'low'];
41
- const SUBJECT_TYPES = ['contact', 'account', 'opportunity', 'use_case'];
42
-
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- function ActorName({ id }: { id: string }) {
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- const { data } = useActor(id) as any;
47
- const actor = data?.actor;
48
- if (!actor) return <span className="text-muted-foreground">—</span>;
49
- return (
50
- <span className="text-sm text-foreground">
51
- {actor.display_name}
52
- <span className="text-xs text-muted-foreground ml-1.5 capitalize">({actor.actor_type})</span>
53
- </span>
54
- );
55
- }
56
-
57
- function AssignmentEditForm({
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- assignment,
60
- onSave,
61
- onCancel,
62
- isSaving,
63
- }: {
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- assignment: any;
66
- onSave: (data: Record<string, unknown>) => void;
67
- onCancel: () => void;
68
- isSaving: boolean;
69
- }) {
70
- const [fields, setFields] = useState({
71
- title: assignment.title ?? '',
72
- assignment_type: assignment.assignment_type ?? '',
73
- priority: assignment.priority ?? 'normal',
74
- due_at: assignment.due_at ? assignment.due_at.split('T')[0] : '',
75
- context: assignment.context ?? '',
76
- description: assignment.description ?? '',
77
- subject_type: assignment.subject_type ?? '',
78
- subject_id: assignment.subject_id ?? '',
79
- });
80
-
81
- const set = (key: string, val: string) => setFields(prev => ({ ...prev, [key]: val }));
82
-
83
- const handleSave = () => {
84
- const payload: Record<string, unknown> = { ...fields };
85
- if (!payload.due_at) delete payload.due_at;
86
- if (!payload.context) delete payload.context;
87
- if (!payload.description) delete payload.description;
88
- if (!payload.subject_type) { delete payload.subject_type; delete payload.subject_id; }
89
- else if (!payload.subject_id) delete payload.subject_id;
90
- if (payload.due_at) payload.due_at = new Date(payload.due_at as string).toISOString();
91
- onSave(payload);
92
- };
93
-
94
- return (
95
- <div className="flex flex-col h-full">
96
- <div className="flex items-center gap-2 px-5 py-3 border-b border-border">
97
- <button onClick={onCancel} className="flex items-center gap-1 text-xs text-accent hover:underline">
98
- <ChevronLeft className="w-3.5 h-3.5" /> Back
99
- </button>
100
- <span className="text-xs text-muted-foreground ml-auto">Editing assignment</span>
101
- </div>
102
- <div className="flex-1 overflow-y-auto p-5 space-y-4">
103
- <div className="space-y-1.5">
104
- <label className={labelClass}>Title <span className="text-destructive">*</span></label>
105
- <input
106
- type="text"
107
- value={fields.title}
108
- onChange={e => set('title', e.target.value)}
109
- placeholder="e.g. Follow up with Acme about contract"
110
- className={inputClass}
111
- />
112
- </div>
113
- <div className="space-y-1.5">
114
- <label className={labelClass}>Type <span className="text-destructive">*</span></label>
115
- <select value={fields.assignment_type} onChange={e => set('assignment_type', e.target.value)} className={`${inputClass} pr-3`}>
116
- <option value="">Select type…</option>
117
- {ASSIGNMENT_TYPES.map(t => (
118
- <option key={t} value={t}>{t.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</option>
119
- ))}
120
- </select>
121
- </div>
122
- <div className="space-y-1.5">
123
- <label className={labelClass}>Priority</label>
124
- <select value={fields.priority} onChange={e => set('priority', e.target.value)} className={`${inputClass} pr-3`}>
125
- {PRIORITIES.map(p => (
126
- <option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
127
- ))}
128
- </select>
129
- </div>
130
- <div className="space-y-1.5">
131
- <label className={labelClass}>Due Date</label>
132
- <DatePicker value={fields.due_at} onChange={val => set('due_at', val)} />
133
- </div>
134
- <div className="space-y-1.5">
135
- <label className={labelClass}>Linked To</label>
136
- <select value={fields.subject_type} onChange={e => { set('subject_type', e.target.value); set('subject_id', ''); }} className={`${inputClass} pr-3`}>
137
- <option value="">None (no link)</option>
138
- {SUBJECT_TYPES.map(t => (
139
- <option key={t} value={t}>{t.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</option>
140
- ))}
141
- </select>
142
- </div>
143
- {fields.subject_type && (
144
- <div className="space-y-1.5">
145
- <label className={labelClass}>Subject ID</label>
146
- <input
147
- type="text"
148
- value={fields.subject_id}
149
- onChange={e => set('subject_id', e.target.value)}
150
- placeholder="Record ID"
151
- className={inputClass}
152
- />
153
- </div>
154
- )}
155
- <div className="space-y-1.5">
156
- <label className={labelClass}>Context</label>
157
- <textarea
158
- value={fields.context}
159
- onChange={e => set('context', e.target.value)}
160
- placeholder="Brief context for the assignee"
161
- rows={3}
162
- className={`${inputClass} h-auto py-2 resize-none`}
163
- />
164
- </div>
165
- <div className="space-y-1.5">
166
- <label className={labelClass}>Description</label>
167
- <textarea
168
- value={fields.description}
169
- onChange={e => set('description', e.target.value)}
170
- placeholder="Additional details"
171
- rows={3}
172
- className={`${inputClass} h-auto py-2 resize-none`}
173
- />
174
- </div>
175
- <button
176
- onClick={handleSave}
177
- disabled={!fields.title.trim() || !fields.assignment_type || isSaving}
178
- 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"
179
- >
180
- {isSaving ? 'Saving…' : 'Save Changes'}
181
- </button>
182
- </div>
183
- </div>
184
- );
185
- }
186
-
187
- export function AssignmentDrawer() {
188
- const { drawerEntityId, openDrawer, closeDrawer } = useAppStore();
189
- const [editing, setEditing] = useState(false);
190
-
191
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
192
- const { data: assignmentData, isLoading } = useAssignment(drawerEntityId ?? '') as any;
193
- const updateAssignment = useUpdateAssignment(drawerEntityId ?? '');
194
- const acceptMutation = useAcceptAssignment();
195
- const startMutation = useStartAssignment();
196
- const completeMutation = useCompleteAssignment();
197
- const declineMutation = useDeclineAssignment();
198
- const blockMutation = useBlockAssignment();
199
- const cancelMutation = useCancelAssignment();
200
-
201
- const handleAction = async (action: string) => {
202
- if (!drawerEntityId) return;
203
- try {
204
- switch (action) {
205
- case 'accept': await acceptMutation.mutateAsync(drawerEntityId); break;
206
- case 'start': await startMutation.mutateAsync(drawerEntityId); break;
207
- case 'complete': await completeMutation.mutateAsync({ id: drawerEntityId }); break;
208
- case 'decline': await declineMutation.mutateAsync({ id: drawerEntityId }); break;
209
- case 'block': await blockMutation.mutateAsync({ id: drawerEntityId }); break;
210
- case 'cancel': await cancelMutation.mutateAsync({ id: drawerEntityId }); break;
211
- }
212
- toast({ title: `Assignment ${action}ed` });
213
- } catch (err) {
214
- toast({ title: `Failed to ${action} assignment`, description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
215
- }
216
- };
217
-
218
- if (isLoading) {
219
- return (
220
- <div className="flex flex-col gap-4 p-6 animate-pulse">
221
- <div className="space-y-2">
222
- <div className="h-4 bg-muted rounded w-1/3" />
223
- <div className="h-5 bg-muted rounded w-3/4" />
224
- </div>
225
- </div>
226
- );
227
- }
228
-
229
- const assignment = assignmentData?.assignment;
230
- if (!assignment) return <div className="p-4 text-muted-foreground">Assignment not found</div>;
231
-
232
- if (editing) {
233
- return (
234
- <AssignmentEditForm
235
- assignment={assignment}
236
- onSave={async (data) => {
237
- try {
238
- await updateAssignment.mutateAsync(data);
239
- setEditing(false);
240
- toast({ title: 'Assignment updated' });
241
- } catch (err) {
242
- toast({ title: 'Failed to update assignment', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
243
- }
244
- }}
245
- onCancel={() => setEditing(false)}
246
- isSaving={updateAssignment.isPending}
247
- />
248
- );
249
- }
250
-
251
- const statusColor = STATUS_COLORS[assignment.status] ?? '#94a3b8';
252
- const priorityColor = PRIORITY_COLORS[assignment.priority] ?? '#94a3b8';
253
- const isOverdue = assignment.due_at && new Date(assignment.due_at) < new Date() && !['completed', 'declined', 'cancelled'].includes(assignment.status);
254
-
255
- const actions: { label: string; action: string; icon: React.ReactNode; variant?: string }[] = [];
256
- switch (assignment.status) {
257
- case 'pending':
258
- actions.push({ label: 'Accept', action: 'accept', icon: <CheckCircle2 className="w-3.5 h-3.5" /> });
259
- actions.push({ label: 'Decline', action: 'decline', icon: <XCircle className="w-3.5 h-3.5" />, variant: 'muted' });
260
- break;
261
- case 'accepted':
262
- actions.push({ label: 'Start', action: 'start', icon: <Play className="w-3.5 h-3.5" /> });
263
- actions.push({ label: 'Block', action: 'block', icon: <AlertOctagon className="w-3.5 h-3.5" />, variant: 'warning' });
264
- break;
265
- case 'in_progress':
266
- actions.push({ label: 'Complete', action: 'complete', icon: <CheckCircle2 className="w-3.5 h-3.5" /> });
267
- actions.push({ label: 'Block', action: 'block', icon: <AlertOctagon className="w-3.5 h-3.5" />, variant: 'warning' });
268
- break;
269
- case 'blocked':
270
- actions.push({ label: 'Resume', action: 'start', icon: <Play className="w-3.5 h-3.5" /> });
271
- actions.push({ label: 'Cancel', action: 'cancel', icon: <Ban className="w-3.5 h-3.5" />, variant: 'destructive' });
272
- break;
273
- }
274
- if (!['completed', 'declined', 'cancelled', 'blocked'].includes(assignment.status)) {
275
- if (!actions.find(a => a.action === 'cancel')) {
276
- actions.push({ label: 'Cancel', action: 'cancel', icon: <Ban className="w-3.5 h-3.5" />, variant: 'destructive' });
277
- }
278
- }
279
-
280
- const SUBJECT_TYPE_DRAWER: Record<string, 'contact' | 'account' | 'opportunity' | 'use-case'> = {
281
- contact: 'contact', account: 'account', opportunity: 'opportunity', use_case: 'use-case',
282
- };
283
-
284
- return (
285
- <div className="flex flex-col">
286
- {/* Header */}
287
- <div className="p-6 border-b border-border">
288
- <div className="flex items-start gap-3 mb-3">
289
- <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0">
290
- <ClipboardList className="w-5 h-5 text-primary" />
291
- </div>
292
- <div className="flex-1 min-w-0">
293
- <div className="flex items-center gap-2 mb-1">
294
- <span
295
- className="px-2 py-0.5 rounded text-xs font-semibold capitalize"
296
- style={{ backgroundColor: statusColor + '18', color: statusColor }}
297
- >
298
- {assignment.status.replace(/_/g, ' ')}
299
- </span>
300
- <span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: priorityColor }} />
301
- <span className="text-xs text-muted-foreground capitalize">{assignment.priority}</span>
302
- </div>
303
- <h2 className="font-display font-extrabold text-xl text-foreground leading-snug">{assignment.title}</h2>
304
- {assignment.assignment_type && (
305
- <span className="text-xs text-muted-foreground capitalize mt-1 inline-block">
306
- {assignment.assignment_type.replace(/_/g, ' ')}
307
- </span>
308
- )}
309
- </div>
310
- </div>
311
-
312
- {/* Action buttons */}
313
- <div className="flex gap-2 flex-wrap">
314
- <button
315
- onClick={() => setEditing(true)}
316
- 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"
317
- >
318
- <Pencil className="w-3.5 h-3.5" /> Edit
319
- </button>
320
- {actions.map(act => (
321
- <button
322
- key={act.action}
323
- onClick={() => handleAction(act.action)}
324
- className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-sm font-medium transition-all press-scale ${
325
- act.variant === 'destructive'
326
- ? 'bg-destructive/10 text-destructive hover:bg-destructive/20'
327
- : act.variant === 'warning'
328
- ? 'bg-warning/10 text-warning hover:bg-warning/20'
329
- : act.variant === 'muted'
330
- ? 'bg-muted text-muted-foreground hover:bg-muted/80'
331
- : 'bg-primary/10 text-primary hover:bg-primary/20'
332
- }`}
333
- >
334
- {act.icon} {act.label}
335
- </button>
336
- ))}
337
- </div>
338
- </div>
339
-
340
- {/* Details */}
341
- <div className="p-4 mx-4 mt-4 space-y-3">
342
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide">Details</h3>
343
- <div className="flex items-center justify-between">
344
- <span className="text-xs text-muted-foreground">Assigned To</span>
345
- <ActorName id={assignment.assigned_to} />
346
- </div>
347
- {assignment.assigned_by && (
348
- <div className="flex items-center justify-between">
349
- <span className="text-xs text-muted-foreground">Assigned By</span>
350
- <ActorName id={assignment.assigned_by} />
351
- </div>
352
- )}
353
- {assignment.due_at && (
354
- <div className="flex items-center justify-between">
355
- <span className="text-xs text-muted-foreground">Due</span>
356
- <span className={`text-sm flex items-center gap-1 ${isOverdue ? 'text-destructive font-medium' : 'text-foreground'}`}>
357
- {isOverdue && <AlertOctagon className="w-3.5 h-3.5" />}
358
- {new Date(assignment.due_at).toLocaleDateString()}
359
- </span>
360
- </div>
361
- )}
362
- {assignment.subject_type && SUBJECT_TYPE_DRAWER[assignment.subject_type] && (
363
- <div className="flex items-center justify-between">
364
- <span className="text-xs text-muted-foreground capitalize">Linked {assignment.subject_type.replace(/_/g, ' ')}</span>
365
- <button
366
- onClick={() => { openDrawer(SUBJECT_TYPE_DRAWER[assignment.subject_type], assignment.subject_id); }}
367
- className="text-sm text-primary hover:underline"
368
- >
369
- View
370
- </button>
371
- </div>
372
- )}
373
- <div className="flex items-center justify-between">
374
- <span className="text-xs text-muted-foreground">Created</span>
375
- <span className="text-sm text-foreground">{new Date(assignment.created_at).toLocaleDateString()}</span>
376
- </div>
377
- </div>
378
-
379
- {/* Context */}
380
- {assignment.context && (
381
- <div className="p-4 mx-4 mt-2">
382
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide mb-2">Context</h3>
383
- <p className="text-sm text-foreground leading-relaxed italic">{assignment.context}</p>
384
- </div>
385
- )}
386
-
387
- {/* Description */}
388
- {assignment.description && (
389
- <div className="p-4 mx-4 mt-2 mb-6">
390
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide mb-2">Description</h3>
391
- <p className="text-sm text-foreground leading-relaxed">{assignment.description}</p>
392
- </div>
393
- )}
394
- </div>
395
- );
396
- }