@hed-hog/contact 0.0.279 → 0.0.285
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/README.md +2 -0
- package/dist/person/dto/create-followup.dto.d.ts +5 -0
- package/dist/person/dto/create-followup.dto.d.ts.map +1 -0
- package/dist/person/dto/create-followup.dto.js +31 -0
- package/dist/person/dto/create-followup.dto.js.map +1 -0
- package/dist/person/dto/create-interaction.dto.d.ts +12 -0
- package/dist/person/dto/create-interaction.dto.d.ts.map +1 -0
- package/dist/person/dto/create-interaction.dto.js +39 -0
- package/dist/person/dto/create-interaction.dto.js.map +1 -0
- package/dist/person/dto/create.dto.d.ts +24 -0
- package/dist/person/dto/create.dto.d.ts.map +1 -1
- package/dist/person/dto/create.dto.js +56 -1
- package/dist/person/dto/create.dto.js.map +1 -1
- package/dist/person/dto/duplicates-query.dto.d.ts +8 -0
- package/dist/person/dto/duplicates-query.dto.d.ts.map +1 -0
- package/dist/person/dto/duplicates-query.dto.js +45 -0
- package/dist/person/dto/duplicates-query.dto.js.map +1 -0
- package/dist/person/dto/merge.dto.d.ts +6 -0
- package/dist/person/dto/merge.dto.d.ts.map +1 -0
- package/dist/person/dto/merge.dto.js +35 -0
- package/dist/person/dto/merge.dto.js.map +1 -0
- package/dist/person/dto/update-lifecycle-stage.dto.d.ts +13 -0
- package/dist/person/dto/update-lifecycle-stage.dto.d.ts.map +1 -0
- package/dist/person/dto/update-lifecycle-stage.dto.js +34 -0
- package/dist/person/dto/update-lifecycle-stage.dto.js.map +1 -0
- package/dist/person/dto/update.dto.d.ts +8 -1
- package/dist/person/dto/update.dto.d.ts.map +1 -1
- package/dist/person/dto/update.dto.js +36 -0
- package/dist/person/dto/update.dto.js.map +1 -1
- package/dist/person/person.controller.d.ts +57 -1
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +85 -3
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +79 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +730 -9
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/data/route.yaml +18 -0
- package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +110 -110
- package/hedhog/frontend/app/_components/crm-nav.tsx.ejs +73 -73
- package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +498 -256
- package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +81 -81
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +477 -0
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +62 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +886 -15
- package/hedhog/frontend/app/activities/page.tsx.ejs +15 -15
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +105 -91
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +506 -573
- package/hedhog/frontend/app/document-type/page.tsx.ejs +105 -91
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +15 -15
- package/hedhog/frontend/app/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1440 -1103
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +4 -3
- package/hedhog/frontend/app/person/_components/person-types.ts.ejs +14 -0
- package/hedhog/frontend/app/person/page.tsx.ejs +108 -190
- package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +599 -0
- package/hedhog/frontend/app/pipeline/page.tsx.ejs +1074 -299
- package/hedhog/frontend/app/reports/page.tsx.ejs +15 -15
- package/hedhog/frontend/messages/en.json +107 -0
- package/hedhog/frontend/messages/pt.json +106 -0
- package/package.json +6 -6
- package/src/person/dto/create-followup.dto.ts +15 -0
- package/src/person/dto/create-interaction.dto.ts +23 -0
- package/src/person/dto/create.dto.ts +50 -0
- package/src/person/dto/duplicates-query.dto.ts +34 -0
- package/src/person/dto/merge.dto.ts +15 -0
- package/src/person/dto/update-lifecycle-stage.dto.ts +19 -0
- package/src/person/dto/update.dto.ts +31 -1
- package/src/person/person.controller.ts +63 -2
- package/src/person/person.service.ts +1096 -7
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Separator } from '@/components/ui/separator';
|
|
6
|
+
import {
|
|
7
|
+
Sheet,
|
|
8
|
+
SheetContent,
|
|
9
|
+
SheetDescription,
|
|
10
|
+
SheetHeader,
|
|
11
|
+
SheetTitle,
|
|
12
|
+
} from '@/components/ui/sheet';
|
|
13
|
+
import {
|
|
14
|
+
Tooltip,
|
|
15
|
+
TooltipContent,
|
|
16
|
+
TooltipTrigger,
|
|
17
|
+
} from '@/components/ui/tooltip';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
20
|
+
import {
|
|
21
|
+
Building2,
|
|
22
|
+
CalendarPlus2,
|
|
23
|
+
CalendarCheck,
|
|
24
|
+
CalendarClock,
|
|
25
|
+
CalendarX,
|
|
26
|
+
CircleDollarSign,
|
|
27
|
+
CircleUser,
|
|
28
|
+
ExternalLink,
|
|
29
|
+
Mail,
|
|
30
|
+
MessageSquare,
|
|
31
|
+
MoveRight,
|
|
32
|
+
Phone,
|
|
33
|
+
Star,
|
|
34
|
+
Tag,
|
|
35
|
+
TrendingUp,
|
|
36
|
+
User,
|
|
37
|
+
Users,
|
|
38
|
+
} from 'lucide-react';
|
|
39
|
+
import { useTranslations } from 'next-intl';
|
|
40
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
41
|
+
import { crmStageOrder, type CrmLead } from '../../_lib/crm-mocks';
|
|
42
|
+
import { PersonInteractionDialog } from '../../person/_components/person-interaction-dialog';
|
|
43
|
+
import type {
|
|
44
|
+
PersonInteraction,
|
|
45
|
+
PersonInteractionType,
|
|
46
|
+
PersonLifecycleStage,
|
|
47
|
+
} from '../../person/_components/person-types';
|
|
48
|
+
|
|
49
|
+
type LeadDetailSheetProps = {
|
|
50
|
+
lead: CrmLead | null;
|
|
51
|
+
open: boolean;
|
|
52
|
+
onOpenChange: (open: boolean) => void;
|
|
53
|
+
onEditCadastro: (lead: CrmLead) => void;
|
|
54
|
+
onMoveStage: (lead: CrmLead, stage: PersonLifecycleStage) => void;
|
|
55
|
+
onLeadUpdated: (lead: CrmLead) => Promise<void> | void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function formatCurrency(value: number) {
|
|
59
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
60
|
+
style: 'currency',
|
|
61
|
+
currency: 'BRL',
|
|
62
|
+
maximumFractionDigits: 0,
|
|
63
|
+
}).format(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatDate(iso: string) {
|
|
67
|
+
return new Date(iso).toLocaleDateString('pt-BR', {
|
|
68
|
+
day: '2-digit',
|
|
69
|
+
month: 'short',
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatDateTime(iso: string) {
|
|
75
|
+
return new Date(iso).toLocaleString('pt-BR', {
|
|
76
|
+
day: '2-digit',
|
|
77
|
+
month: 'short',
|
|
78
|
+
hour: '2-digit',
|
|
79
|
+
minute: '2-digit',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function daysSince(date: string) {
|
|
84
|
+
const diff =
|
|
85
|
+
new Date('2026-03-16T00:00:00.000Z').getTime() - new Date(date).getTime();
|
|
86
|
+
return Math.max(0, Math.floor(diff / (1000 * 60 * 60 * 24)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nextActionUrgency(nextActionAt: string | null | undefined) {
|
|
90
|
+
if (!nextActionAt) return 'none';
|
|
91
|
+
const now = new Date('2026-03-16T00:00:00.000Z');
|
|
92
|
+
const date = new Date(nextActionAt);
|
|
93
|
+
const diffDays = Math.floor(
|
|
94
|
+
(date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
95
|
+
);
|
|
96
|
+
if (diffDays < 0) return 'overdue';
|
|
97
|
+
if (diffDays <= 3) return 'soon';
|
|
98
|
+
return 'ok';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ScoreRing({ score }: { score: number }) {
|
|
102
|
+
const size = 44;
|
|
103
|
+
const radius = 17;
|
|
104
|
+
const circumference = 2 * Math.PI * radius;
|
|
105
|
+
const dashOffset =
|
|
106
|
+
circumference - (Math.min(score, 100) / 100) * circumference;
|
|
107
|
+
const color =
|
|
108
|
+
score >= 80
|
|
109
|
+
? 'text-emerald-500'
|
|
110
|
+
: score >= 50
|
|
111
|
+
? 'text-amber-500'
|
|
112
|
+
: 'text-red-500';
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="relative flex size-11 items-center justify-center">
|
|
116
|
+
<svg className="-rotate-90" width={size} height={size}>
|
|
117
|
+
<circle
|
|
118
|
+
cx={size / 2}
|
|
119
|
+
cy={size / 2}
|
|
120
|
+
r={radius}
|
|
121
|
+
strokeWidth="4"
|
|
122
|
+
className="stroke-muted"
|
|
123
|
+
fill="transparent"
|
|
124
|
+
/>
|
|
125
|
+
<circle
|
|
126
|
+
cx={size / 2}
|
|
127
|
+
cy={size / 2}
|
|
128
|
+
r={radius}
|
|
129
|
+
strokeWidth="4"
|
|
130
|
+
strokeLinecap="round"
|
|
131
|
+
className={cn('fill-transparent stroke-current', color)}
|
|
132
|
+
strokeDasharray={circumference}
|
|
133
|
+
strokeDashoffset={dashOffset}
|
|
134
|
+
/>
|
|
135
|
+
</svg>
|
|
136
|
+
<span className={cn('absolute text-[11px] font-semibold', color)}>
|
|
137
|
+
{score}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function MetricTile({
|
|
144
|
+
icon,
|
|
145
|
+
label,
|
|
146
|
+
value,
|
|
147
|
+
tooltip,
|
|
148
|
+
valueClassName,
|
|
149
|
+
}: {
|
|
150
|
+
icon: ReactNode;
|
|
151
|
+
label: string;
|
|
152
|
+
value: ReactNode;
|
|
153
|
+
tooltip?: string;
|
|
154
|
+
valueClassName?: string;
|
|
155
|
+
}) {
|
|
156
|
+
const tile = (
|
|
157
|
+
<div className="rounded-xl border border-border/60 bg-muted/20 p-3">
|
|
158
|
+
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
159
|
+
{icon}
|
|
160
|
+
<span>{label}</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
'mt-2 text-sm font-semibold text-foreground',
|
|
165
|
+
valueClassName
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{value}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (!tooltip) return tile;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Tooltip>
|
|
177
|
+
<TooltipTrigger asChild>{tile}</TooltipTrigger>
|
|
178
|
+
<TooltipContent side="top">{tooltip}</TooltipContent>
|
|
179
|
+
</Tooltip>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function StageProgress({
|
|
184
|
+
stage,
|
|
185
|
+
}: {
|
|
186
|
+
stage: PersonLifecycleStage | null | undefined;
|
|
187
|
+
}) {
|
|
188
|
+
const dashboardT = useTranslations('contact.CrmDashboard');
|
|
189
|
+
const currentStage = stage ?? 'new';
|
|
190
|
+
const currentIndex = crmStageOrder.indexOf(currentStage);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
<div className="grid grid-cols-7 gap-1.5">
|
|
195
|
+
{crmStageOrder.map((item, index) => {
|
|
196
|
+
const isLostStage = item === 'lost';
|
|
197
|
+
const isActive = currentStage === item;
|
|
198
|
+
const isCompleted =
|
|
199
|
+
!isLostStage && currentStage !== 'lost' && index <= currentIndex;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Tooltip key={item}>
|
|
203
|
+
<TooltipTrigger asChild>
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'h-2 rounded-full transition-colors',
|
|
207
|
+
isActive && currentStage === 'lost' && 'bg-red-500',
|
|
208
|
+
isLostStage && currentStage !== 'lost' && 'bg-muted',
|
|
209
|
+
!isLostStage && isCompleted && 'bg-primary',
|
|
210
|
+
!isActive && !isCompleted && !isLostStage && 'bg-muted',
|
|
211
|
+
isActive && currentStage !== 'lost' && 'bg-primary'
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
</TooltipTrigger>
|
|
215
|
+
<TooltipContent side="bottom" className="text-xs">
|
|
216
|
+
{dashboardT(`stageLabels.${item}`)}
|
|
217
|
+
</TooltipContent>
|
|
218
|
+
</Tooltip>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</div>
|
|
222
|
+
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
223
|
+
<span>{dashboardT('stageLabels.new')}</span>
|
|
224
|
+
<span>{dashboardT(`stageLabels.${currentStage}`)}</span>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function LeadDetailSheet({
|
|
231
|
+
lead,
|
|
232
|
+
open,
|
|
233
|
+
onOpenChange,
|
|
234
|
+
onEditCadastro,
|
|
235
|
+
onMoveStage,
|
|
236
|
+
onLeadUpdated,
|
|
237
|
+
}: LeadDetailSheetProps) {
|
|
238
|
+
const t = useTranslations('contact.CrmPipeline');
|
|
239
|
+
const dashboardT = useTranslations('contact.CrmDashboard');
|
|
240
|
+
const contactPageT = useTranslations('contact.ContactPage');
|
|
241
|
+
const { request, currentLocaleCode } = useApp();
|
|
242
|
+
const [interactionDialogOpen, setInteractionDialogOpen] = useState(false);
|
|
243
|
+
const [interactionMode, setInteractionMode] = useState<
|
|
244
|
+
'interaction' | 'followup'
|
|
245
|
+
>('interaction');
|
|
246
|
+
const {
|
|
247
|
+
data: interactions = [],
|
|
248
|
+
isLoading: isInteractionsLoading,
|
|
249
|
+
refetch: refetchInteractions,
|
|
250
|
+
} = useQuery<PersonInteraction[]>({
|
|
251
|
+
queryKey: ['contact-person-interactions', lead?.id, currentLocaleCode],
|
|
252
|
+
enabled: open && !!lead?.id,
|
|
253
|
+
queryFn: async () => {
|
|
254
|
+
if (!lead?.id) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const response = await request<PersonInteraction[]>({
|
|
259
|
+
url: `/person/${lead.id}/interaction`,
|
|
260
|
+
method: 'GET',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return response.data;
|
|
264
|
+
},
|
|
265
|
+
placeholderData: (previous) => previous ?? [],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (!open) {
|
|
270
|
+
setInteractionDialogOpen(false);
|
|
271
|
+
setInteractionMode('interaction');
|
|
272
|
+
}
|
|
273
|
+
}, [open]);
|
|
274
|
+
|
|
275
|
+
if (!lead) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const urgency = nextActionUrgency(lead.next_action_at);
|
|
280
|
+
const age = daysSince(lead.created_at);
|
|
281
|
+
const currentStageIndex = crmStageOrder.indexOf(
|
|
282
|
+
lead.lifecycle_stage ?? 'new'
|
|
283
|
+
);
|
|
284
|
+
const nextStage =
|
|
285
|
+
lead.lifecycle_stage &&
|
|
286
|
+
lead.lifecycle_stage !== 'customer' &&
|
|
287
|
+
lead.lifecycle_stage !== 'lost'
|
|
288
|
+
? crmStageOrder[currentStageIndex + 1]
|
|
289
|
+
: null;
|
|
290
|
+
|
|
291
|
+
const interactionIconMap: Record<PersonInteractionType, ReactNode> = {
|
|
292
|
+
call: <Phone className="size-3.5" />,
|
|
293
|
+
email: <Mail className="size-3.5" />,
|
|
294
|
+
whatsapp: <MessageSquare className="size-3.5" />,
|
|
295
|
+
meeting: <Users className="size-3.5" />,
|
|
296
|
+
note: <Tag className="size-3.5" />,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
301
|
+
<SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-lg">
|
|
302
|
+
<SheetHeader className="shrink-0 border-b px-5 py-4 text-left">
|
|
303
|
+
<div className="flex items-start gap-3 pr-6">
|
|
304
|
+
<div className="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
|
305
|
+
{lead.type === 'company' ? (
|
|
306
|
+
<Building2 className="size-5" />
|
|
307
|
+
) : (
|
|
308
|
+
<User className="size-5" />
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
313
|
+
<div className="space-y-1">
|
|
314
|
+
<SheetTitle className="truncate text-base font-semibold">
|
|
315
|
+
{lead.name}
|
|
316
|
+
</SheetTitle>
|
|
317
|
+
<SheetDescription className="truncate text-xs">
|
|
318
|
+
{lead.trade_name ||
|
|
319
|
+
lead.companyLabel ||
|
|
320
|
+
dashboardT(`sourceLabels.${lead.source ?? 'other'}`)}
|
|
321
|
+
</SheetDescription>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
325
|
+
<Badge
|
|
326
|
+
variant="outline"
|
|
327
|
+
className={cn(
|
|
328
|
+
'text-[10px]',
|
|
329
|
+
lead.status === 'active'
|
|
330
|
+
? 'border-green-500/30 bg-green-500/10 text-green-600'
|
|
331
|
+
: 'border-red-500/30 bg-red-500/10 text-red-600'
|
|
332
|
+
)}
|
|
333
|
+
>
|
|
334
|
+
{lead.status === 'active' ? t('active') : t('inactive')}
|
|
335
|
+
</Badge>
|
|
336
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
337
|
+
{dashboardT(`stageLabels.${lead.lifecycle_stage ?? 'new'}`)}
|
|
338
|
+
</Badge>
|
|
339
|
+
{lead.lifecycle_stage === 'lost' && (
|
|
340
|
+
<Badge
|
|
341
|
+
variant="outline"
|
|
342
|
+
className="border-red-500/30 bg-red-500/10 text-[10px] text-red-600"
|
|
343
|
+
>
|
|
344
|
+
{t('detail.lost')}
|
|
345
|
+
</Badge>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</SheetHeader>
|
|
351
|
+
|
|
352
|
+
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
353
|
+
<div className="space-y-5">
|
|
354
|
+
<div className="space-y-2">
|
|
355
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
356
|
+
{t('detail.title')}
|
|
357
|
+
</p>
|
|
358
|
+
<StageProgress stage={lead.lifecycle_stage} />
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div className="grid grid-cols-2 gap-2.5">
|
|
362
|
+
<MetricTile
|
|
363
|
+
icon={
|
|
364
|
+
<CircleDollarSign className="size-3.5 text-emerald-500" />
|
|
365
|
+
}
|
|
366
|
+
label={t('card.dealValue')}
|
|
367
|
+
value={formatCurrency(lead.dealValue)}
|
|
368
|
+
tooltip={t('tooltips.dealValue')}
|
|
369
|
+
/>
|
|
370
|
+
<MetricTile
|
|
371
|
+
icon={<Star className="size-3.5 text-amber-400" />}
|
|
372
|
+
label={t('card.score')}
|
|
373
|
+
value={<ScoreRing score={lead.score} />}
|
|
374
|
+
tooltip={t('tooltips.score')}
|
|
375
|
+
/>
|
|
376
|
+
<MetricTile
|
|
377
|
+
icon={<TrendingUp className="size-3.5 text-violet-400" />}
|
|
378
|
+
label={t('card.age')}
|
|
379
|
+
value={t('card.days', { count: age })}
|
|
380
|
+
tooltip={t('tooltips.age')}
|
|
381
|
+
/>
|
|
382
|
+
<MetricTile
|
|
383
|
+
icon={
|
|
384
|
+
urgency === 'overdue' ? (
|
|
385
|
+
<CalendarX className="size-3.5 text-red-500" />
|
|
386
|
+
) : urgency === 'soon' ? (
|
|
387
|
+
<CalendarClock className="size-3.5 text-amber-500" />
|
|
388
|
+
) : (
|
|
389
|
+
<CalendarCheck className="size-3.5 text-blue-500" />
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
label={t('card.nextAction')}
|
|
393
|
+
value={
|
|
394
|
+
lead.next_action_at
|
|
395
|
+
? new Date(lead.next_action_at).toLocaleDateString('pt-BR')
|
|
396
|
+
: t('card.noFollowup')
|
|
397
|
+
}
|
|
398
|
+
valueClassName={cn(
|
|
399
|
+
urgency === 'overdue' && 'text-red-500',
|
|
400
|
+
urgency === 'soon' && 'text-amber-500',
|
|
401
|
+
urgency === 'ok' && 'text-blue-500'
|
|
402
|
+
)}
|
|
403
|
+
tooltip={
|
|
404
|
+
urgency === 'overdue'
|
|
405
|
+
? t('tooltips.nextActionOverdue')
|
|
406
|
+
: urgency === 'soon'
|
|
407
|
+
? t('tooltips.nextActionSoon')
|
|
408
|
+
: urgency === 'ok'
|
|
409
|
+
? t('tooltips.nextActionOk')
|
|
410
|
+
: t('tooltips.nextActionNone')
|
|
411
|
+
}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
416
|
+
<div className="flex items-center gap-1.5 rounded-lg bg-muted/60 px-2.5 py-1.5 text-xs">
|
|
417
|
+
<CircleUser className="size-3.5 shrink-0 text-muted-foreground" />
|
|
418
|
+
<span className="text-muted-foreground">
|
|
419
|
+
{t('detail.owner')}:
|
|
420
|
+
</span>
|
|
421
|
+
<span className="font-medium text-foreground">
|
|
422
|
+
{lead.owner_user?.name || dashboardT('common.unassigned')}
|
|
423
|
+
</span>
|
|
424
|
+
</div>
|
|
425
|
+
<div className="flex items-center gap-1.5 rounded-lg bg-muted/60 px-2.5 py-1.5 text-xs">
|
|
426
|
+
<Tag className="size-3.5 shrink-0 text-sky-400" />
|
|
427
|
+
<span className="text-muted-foreground">
|
|
428
|
+
{t('detail.source')}:
|
|
429
|
+
</span>
|
|
430
|
+
<span className="font-medium text-foreground">
|
|
431
|
+
{dashboardT(`sourceLabels.${lead.source ?? 'other'}`)}
|
|
432
|
+
</span>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
{lead.tags.length > 0 && (
|
|
437
|
+
<div className="flex flex-wrap gap-1.5">
|
|
438
|
+
{lead.tags.map((tag) => (
|
|
439
|
+
<Badge key={tag} variant="outline" className="text-[11px]">
|
|
440
|
+
{tag}
|
|
441
|
+
</Badge>
|
|
442
|
+
))}
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
|
|
446
|
+
{nextStage && (
|
|
447
|
+
<>
|
|
448
|
+
<Separator />
|
|
449
|
+
<div className="space-y-2">
|
|
450
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
451
|
+
{t('detail.advanceStage')}
|
|
452
|
+
</p>
|
|
453
|
+
<Button
|
|
454
|
+
variant="outline"
|
|
455
|
+
className="w-full gap-2"
|
|
456
|
+
onClick={() => onMoveStage(lead, nextStage)}
|
|
457
|
+
>
|
|
458
|
+
<MoveRight className="size-4" />
|
|
459
|
+
{t('detail.moveTo', {
|
|
460
|
+
stage: dashboardT(`stageLabels.${nextStage}`),
|
|
461
|
+
})}
|
|
462
|
+
</Button>
|
|
463
|
+
</div>
|
|
464
|
+
</>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
<Separator />
|
|
468
|
+
|
|
469
|
+
<div className="space-y-2">
|
|
470
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
471
|
+
{contactPageT('registerInteraction')}
|
|
472
|
+
</p>
|
|
473
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
474
|
+
<Button
|
|
475
|
+
variant="outline"
|
|
476
|
+
className="w-full gap-2"
|
|
477
|
+
onClick={() => {
|
|
478
|
+
setInteractionMode('interaction');
|
|
479
|
+
setInteractionDialogOpen(true);
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
<Phone className="size-4" />
|
|
483
|
+
{contactPageT('registerInteraction')}
|
|
484
|
+
</Button>
|
|
485
|
+
<Button
|
|
486
|
+
variant="outline"
|
|
487
|
+
className="w-full gap-2"
|
|
488
|
+
onClick={() => {
|
|
489
|
+
setInteractionMode('followup');
|
|
490
|
+
setInteractionDialogOpen(true);
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<CalendarPlus2 className="size-4" />
|
|
494
|
+
{contactPageT('scheduleFollowup')}
|
|
495
|
+
</Button>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<div className="space-y-3">
|
|
500
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
501
|
+
{t('detail.timeline')}
|
|
502
|
+
</p>
|
|
503
|
+
|
|
504
|
+
{interactions.length === 0 ? (
|
|
505
|
+
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
|
|
506
|
+
{isInteractionsLoading
|
|
507
|
+
? t('detail.loadingTimeline')
|
|
508
|
+
: t('detail.noInteractions')}
|
|
509
|
+
</div>
|
|
510
|
+
) : (
|
|
511
|
+
<div className="space-y-3">
|
|
512
|
+
{interactions.map((interaction) => (
|
|
513
|
+
<div
|
|
514
|
+
key={`${lead.id}-${interaction.id}`}
|
|
515
|
+
className="flex gap-3 rounded-xl border border-border/50 bg-muted/15 p-3"
|
|
516
|
+
>
|
|
517
|
+
<div
|
|
518
|
+
className={cn(
|
|
519
|
+
'mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full border text-xs',
|
|
520
|
+
interaction.type === 'call' &&
|
|
521
|
+
'border-blue-500/25 bg-blue-500/10 text-blue-600',
|
|
522
|
+
interaction.type === 'email' &&
|
|
523
|
+
'border-violet-500/25 bg-violet-500/10 text-violet-600',
|
|
524
|
+
interaction.type === 'whatsapp' &&
|
|
525
|
+
'border-emerald-500/25 bg-emerald-500/10 text-emerald-600',
|
|
526
|
+
interaction.type === 'meeting' &&
|
|
527
|
+
'border-amber-500/25 bg-amber-500/10 text-amber-600',
|
|
528
|
+
interaction.type === 'note' &&
|
|
529
|
+
'border-slate-500/25 bg-slate-500/10 text-slate-600'
|
|
530
|
+
)}
|
|
531
|
+
>
|
|
532
|
+
{interactionIconMap[interaction.type]}
|
|
533
|
+
</div>
|
|
534
|
+
<div className="min-w-0 flex-1">
|
|
535
|
+
<div className="flex items-start justify-between gap-3">
|
|
536
|
+
<p className="text-xs font-semibold text-foreground">
|
|
537
|
+
{t(`detail.interactionType.${interaction.type}`)}
|
|
538
|
+
</p>
|
|
539
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
540
|
+
{formatDateTime(interaction.created_at)}
|
|
541
|
+
</span>
|
|
542
|
+
</div>
|
|
543
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
544
|
+
{interaction.notes}
|
|
545
|
+
</p>
|
|
546
|
+
<p className="mt-1 text-[11px] font-medium text-foreground">
|
|
547
|
+
{interaction.user_name}
|
|
548
|
+
</p>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
))}
|
|
552
|
+
</div>
|
|
553
|
+
)}
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div className="rounded-xl border border-border/50 bg-muted/20 p-3 text-xs">
|
|
557
|
+
<div className="flex items-center justify-between gap-2">
|
|
558
|
+
<span className="text-muted-foreground">
|
|
559
|
+
{t('detail.createdAt')}
|
|
560
|
+
</span>
|
|
561
|
+
<span className="font-medium text-foreground">
|
|
562
|
+
{formatDate(lead.created_at)}
|
|
563
|
+
</span>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="mt-2 flex items-center justify-between gap-2">
|
|
566
|
+
<span className="text-muted-foreground">
|
|
567
|
+
{t('detail.lastInteraction')}
|
|
568
|
+
</span>
|
|
569
|
+
<span className="font-medium text-foreground">
|
|
570
|
+
{formatDate(lead.lastInteractionAt)}
|
|
571
|
+
</span>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<div className="shrink-0 border-t px-5 py-3">
|
|
578
|
+
<Button className="w-full gap-2" onClick={() => onEditCadastro(lead)}>
|
|
579
|
+
<ExternalLink className="size-4" />
|
|
580
|
+
{t('detail.editCadastro')}
|
|
581
|
+
</Button>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<PersonInteractionDialog
|
|
585
|
+
open={interactionDialogOpen}
|
|
586
|
+
person={lead}
|
|
587
|
+
mode={interactionMode}
|
|
588
|
+
onOpenChange={setInteractionDialogOpen}
|
|
589
|
+
onSuccess={async () => {
|
|
590
|
+
await Promise.all([
|
|
591
|
+
refetchInteractions(),
|
|
592
|
+
Promise.resolve(onLeadUpdated(lead)),
|
|
593
|
+
]);
|
|
594
|
+
}}
|
|
595
|
+
/>
|
|
596
|
+
</SheetContent>
|
|
597
|
+
</Sheet>
|
|
598
|
+
);
|
|
599
|
+
}
|