@crmy/web 0.5.5 → 0.5.9
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,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
|
-
}
|