@hed-hog/contact 0.0.293 → 0.0.295
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/person/dto/account.dto.d.ts +28 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -0
- package/dist/person/dto/account.dto.js +123 -0
- package/dist/person/dto/account.dto.js.map +1 -0
- package/dist/person/dto/activity.dto.d.ts +15 -0
- package/dist/person/dto/activity.dto.d.ts.map +1 -0
- package/dist/person/dto/activity.dto.js +65 -0
- package/dist/person/dto/activity.dto.js.map +1 -0
- package/dist/person/dto/dashboard-query.dto.d.ts +9 -0
- package/dist/person/dto/dashboard-query.dto.d.ts.map +1 -0
- package/dist/person/dto/dashboard-query.dto.js +40 -0
- package/dist/person/dto/dashboard-query.dto.js.map +1 -0
- package/dist/person/dto/followup-query.dto.d.ts +10 -0
- package/dist/person/dto/followup-query.dto.d.ts.map +1 -0
- package/dist/person/dto/followup-query.dto.js +45 -0
- package/dist/person/dto/followup-query.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +204 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +138 -0
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +234 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +1367 -0
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/data/menu.yaml +163 -163
- package/hedhog/data/route.yaml +41 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +210 -114
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +3 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +323 -245
- package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -0
- package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -0
- package/hedhog/frontend/app/activities/page.tsx.ejs +165 -517
- package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +504 -356
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +242 -153
- package/hedhog/frontend/messages/en.json +91 -6
- package/hedhog/frontend/messages/pt.json +91 -6
- package/hedhog/table/crm_activity.yaml +68 -0
- package/hedhog/table/person_company.yaml +22 -0
- package/package.json +5 -5
- package/src/person/dto/account.dto.ts +100 -0
- package/src/person/dto/activity.dto.ts +54 -0
- package/src/person/dto/dashboard-query.dto.ts +25 -0
- package/src/person/dto/followup-query.dto.ts +25 -0
- package/src/person/person.controller.ts +116 -0
- package/src/person/person.service.ts +2139 -77
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
} from '@/components/entity-list';
|
|
11
11
|
import { Badge } from '@/components/ui/badge';
|
|
12
12
|
import { Button } from '@/components/ui/button';
|
|
13
|
+
import {
|
|
14
|
+
Command,
|
|
15
|
+
CommandEmpty,
|
|
16
|
+
CommandGroup,
|
|
17
|
+
CommandInput,
|
|
18
|
+
CommandItem,
|
|
19
|
+
CommandList,
|
|
20
|
+
} from '@/components/ui/command';
|
|
13
21
|
import {
|
|
14
22
|
Form,
|
|
15
23
|
FormControl,
|
|
@@ -21,12 +29,10 @@ import {
|
|
|
21
29
|
import { Input } from '@/components/ui/input';
|
|
22
30
|
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
23
31
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
SelectValue,
|
|
29
|
-
} from '@/components/ui/select';
|
|
32
|
+
Popover,
|
|
33
|
+
PopoverContent,
|
|
34
|
+
PopoverTrigger,
|
|
35
|
+
} from '@/components/ui/popover';
|
|
30
36
|
import {
|
|
31
37
|
Sheet,
|
|
32
38
|
SheetContent,
|
|
@@ -52,7 +58,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
52
58
|
import {
|
|
53
59
|
CalendarClock,
|
|
54
60
|
CalendarDays,
|
|
61
|
+
Check,
|
|
55
62
|
CheckCircle2,
|
|
63
|
+
ChevronsUpDown,
|
|
56
64
|
Clock3,
|
|
57
65
|
Loader2,
|
|
58
66
|
Plus,
|
|
@@ -71,6 +79,9 @@ type PaginatedResult<T> = {
|
|
|
71
79
|
total: number;
|
|
72
80
|
page: number;
|
|
73
81
|
pageSize: number;
|
|
82
|
+
lastPage?: number;
|
|
83
|
+
prev?: number | null;
|
|
84
|
+
next?: number | null;
|
|
74
85
|
};
|
|
75
86
|
|
|
76
87
|
type Person = {
|
|
@@ -85,15 +96,26 @@ type Person = {
|
|
|
85
96
|
last_interaction_at?: string | null;
|
|
86
97
|
};
|
|
87
98
|
|
|
99
|
+
type PersonOption = {
|
|
100
|
+
id: number;
|
|
101
|
+
name: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
88
104
|
type FollowupStatus = 'today' | 'upcoming' | 'overdue';
|
|
89
105
|
|
|
90
|
-
type
|
|
106
|
+
type FollowupListItem = {
|
|
91
107
|
person: Person;
|
|
92
|
-
|
|
108
|
+
next_action_at: string;
|
|
109
|
+
last_interaction_at?: string | null;
|
|
93
110
|
status: FollowupStatus;
|
|
94
111
|
};
|
|
95
112
|
|
|
96
|
-
|
|
113
|
+
type FollowupStats = {
|
|
114
|
+
total: number;
|
|
115
|
+
today: number;
|
|
116
|
+
overdue: number;
|
|
117
|
+
upcoming: number;
|
|
118
|
+
};
|
|
97
119
|
|
|
98
120
|
function toInputDateTimeValue(value?: string | null) {
|
|
99
121
|
if (!value) {
|
|
@@ -115,37 +137,6 @@ function toInputDateTimeValue(value?: string | null) {
|
|
|
115
137
|
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
116
138
|
}
|
|
117
139
|
|
|
118
|
-
function startOfDay(date: Date) {
|
|
119
|
-
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function getFollowupStatus(nextActionAt: string): FollowupStatus {
|
|
123
|
-
const target = new Date(nextActionAt);
|
|
124
|
-
const now = new Date();
|
|
125
|
-
const todayStart = startOfDay(now);
|
|
126
|
-
const tomorrowStart = new Date(todayStart);
|
|
127
|
-
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
|
128
|
-
|
|
129
|
-
if (target < todayStart) {
|
|
130
|
-
return 'overdue';
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (target >= todayStart && target < tomorrowStart) {
|
|
134
|
-
return 'today';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return 'upcoming';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isValidDateString(value?: string | null) {
|
|
141
|
-
if (!value) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const date = new Date(value);
|
|
146
|
-
return !Number.isNaN(date.getTime());
|
|
147
|
-
}
|
|
148
|
-
|
|
149
140
|
function getStatusBadgeClass(status: FollowupStatus) {
|
|
150
141
|
if (status === 'overdue') {
|
|
151
142
|
return 'border-red-500/25 bg-red-500/10 text-red-700';
|
|
@@ -186,12 +177,16 @@ export default function CrmFollowupsPage() {
|
|
|
186
177
|
|
|
187
178
|
const [searchInput, setSearchInput] = useState('');
|
|
188
179
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
180
|
+
const [personSearch, setPersonSearch] = useState('');
|
|
181
|
+
const [debouncedPersonSearch, setDebouncedPersonSearch] = useState('');
|
|
182
|
+
const [selectedPersonLabel, setSelectedPersonLabel] = useState('');
|
|
189
183
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
190
184
|
const [dateFrom, setDateFrom] = useState('');
|
|
191
185
|
const [dateTo, setDateTo] = useState('');
|
|
192
186
|
const [page, setPage] = useState(1);
|
|
193
187
|
const [pageSize, setPageSize] = useState(12);
|
|
194
188
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
189
|
+
const [personPickerOpen, setPersonPickerOpen] = useState(false);
|
|
195
190
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
196
191
|
|
|
197
192
|
useEffect(() => {
|
|
@@ -202,22 +197,35 @@ export default function CrmFollowupsPage() {
|
|
|
202
197
|
return () => clearTimeout(timeout);
|
|
203
198
|
}, [searchInput]);
|
|
204
199
|
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const timeout = setTimeout(() => {
|
|
202
|
+
setDebouncedPersonSearch(personSearch.trim());
|
|
203
|
+
}, 300);
|
|
204
|
+
|
|
205
|
+
return () => clearTimeout(timeout);
|
|
206
|
+
}, [personSearch]);
|
|
207
|
+
|
|
205
208
|
const {
|
|
206
|
-
data:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
data: stats = {
|
|
210
|
+
total: 0,
|
|
211
|
+
today: 0,
|
|
212
|
+
overdue: 0,
|
|
213
|
+
upcoming: 0,
|
|
214
|
+
},
|
|
215
|
+
refetch: refetchStats,
|
|
216
|
+
} = useQuery<FollowupStats>({
|
|
217
|
+
queryKey: ['contact-followups-stats', debouncedSearch, currentLocaleCode],
|
|
211
218
|
queryFn: async () => {
|
|
212
219
|
const params = new URLSearchParams();
|
|
213
|
-
params.set('page', '1');
|
|
214
|
-
params.set('pageSize', String(SOURCE_PAGE_SIZE));
|
|
215
220
|
if (debouncedSearch) {
|
|
216
221
|
params.set('search', debouncedSearch);
|
|
217
222
|
}
|
|
218
223
|
|
|
219
|
-
const
|
|
220
|
-
|
|
224
|
+
const queryString = params.toString();
|
|
225
|
+
const response = await request<FollowupStats>({
|
|
226
|
+
url: queryString
|
|
227
|
+
? `/person/followups/stats?${queryString}`
|
|
228
|
+
: '/person/followups/stats',
|
|
221
229
|
method: 'GET',
|
|
222
230
|
});
|
|
223
231
|
|
|
@@ -225,62 +233,97 @@ export default function CrmFollowupsPage() {
|
|
|
225
233
|
},
|
|
226
234
|
placeholderData: (previous) =>
|
|
227
235
|
previous ?? {
|
|
228
|
-
data: [],
|
|
229
236
|
total: 0,
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
today: 0,
|
|
238
|
+
overdue: 0,
|
|
239
|
+
upcoming: 0,
|
|
232
240
|
},
|
|
233
241
|
});
|
|
234
242
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
243
|
+
const {
|
|
244
|
+
data: paginate = {
|
|
245
|
+
data: [],
|
|
246
|
+
total: 0,
|
|
247
|
+
page: 1,
|
|
248
|
+
pageSize: 12,
|
|
249
|
+
lastPage: 1,
|
|
250
|
+
},
|
|
251
|
+
isLoading,
|
|
252
|
+
refetch: refetchFollowups,
|
|
253
|
+
} = useQuery<PaginatedResult<FollowupListItem>>({
|
|
254
|
+
queryKey: [
|
|
255
|
+
'contact-followups',
|
|
256
|
+
page,
|
|
257
|
+
pageSize,
|
|
258
|
+
debouncedSearch,
|
|
259
|
+
statusFilter,
|
|
260
|
+
dateFrom,
|
|
261
|
+
dateTo,
|
|
262
|
+
currentLocaleCode,
|
|
263
|
+
],
|
|
264
|
+
queryFn: async () => {
|
|
265
|
+
const params = new URLSearchParams();
|
|
266
|
+
params.set('page', String(page));
|
|
267
|
+
params.set('pageSize', String(pageSize));
|
|
268
|
+
if (debouncedSearch) {
|
|
269
|
+
params.set('search', debouncedSearch);
|
|
270
|
+
}
|
|
271
|
+
if (statusFilter !== 'all') {
|
|
272
|
+
params.set('status', statusFilter);
|
|
258
273
|
}
|
|
259
|
-
|
|
260
|
-
const targetDate = new Date(item.nextActionAt);
|
|
261
|
-
|
|
262
274
|
if (dateFrom) {
|
|
263
|
-
|
|
264
|
-
if (targetDate < min) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
275
|
+
params.set('date_from', dateFrom);
|
|
267
276
|
}
|
|
268
|
-
|
|
269
277
|
if (dateTo) {
|
|
270
|
-
|
|
271
|
-
max.setDate(max.getDate() + 1);
|
|
272
|
-
if (targetDate >= max) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
278
|
+
params.set('date_to', dateTo);
|
|
275
279
|
}
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
const response = await request<PaginatedResult<FollowupListItem>>({
|
|
282
|
+
url: `/person/followups?${params.toString()}`,
|
|
283
|
+
method: 'GET',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return response.data;
|
|
287
|
+
},
|
|
288
|
+
placeholderData: (previous) =>
|
|
289
|
+
previous ?? {
|
|
290
|
+
data: [],
|
|
291
|
+
total: 0,
|
|
292
|
+
page: 1,
|
|
293
|
+
pageSize: 12,
|
|
294
|
+
lastPage: 1,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const { data: personOptions = [], isLoading: isLoadingPersons } = useQuery<
|
|
299
|
+
PersonOption[]
|
|
300
|
+
>({
|
|
301
|
+
queryKey: [
|
|
302
|
+
'contact-followup-person-options',
|
|
303
|
+
debouncedPersonSearch,
|
|
304
|
+
currentLocaleCode,
|
|
305
|
+
],
|
|
306
|
+
queryFn: async () => {
|
|
307
|
+
const params = new URLSearchParams();
|
|
308
|
+
params.set('page', '1');
|
|
309
|
+
params.set('pageSize', '20');
|
|
310
|
+
if (debouncedPersonSearch) {
|
|
311
|
+
params.set('search', debouncedPersonSearch);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const response = await request<PaginatedResult<PersonOption>>({
|
|
315
|
+
url: `/person?${params.toString()}`,
|
|
316
|
+
method: 'GET',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return response.data.data || [];
|
|
320
|
+
},
|
|
321
|
+
placeholderData: (previous) => previous ?? [],
|
|
322
|
+
});
|
|
280
323
|
|
|
281
324
|
const totalPages = Math.max(
|
|
282
325
|
1,
|
|
283
|
-
Math.ceil(
|
|
326
|
+
paginate.lastPage ?? (Math.ceil((paginate.total || 0) / pageSize) || 1)
|
|
284
327
|
);
|
|
285
328
|
|
|
286
329
|
useEffect(() => {
|
|
@@ -289,40 +332,6 @@ export default function CrmFollowupsPage() {
|
|
|
289
332
|
}
|
|
290
333
|
}, [page, totalPages]);
|
|
291
334
|
|
|
292
|
-
const pageData = useMemo(() => {
|
|
293
|
-
const start = (page - 1) * pageSize;
|
|
294
|
-
const end = start + pageSize;
|
|
295
|
-
|
|
296
|
-
return filteredFollowups.slice(start, end);
|
|
297
|
-
}, [filteredFollowups, page, pageSize]);
|
|
298
|
-
|
|
299
|
-
const stats = useMemo(() => {
|
|
300
|
-
const total = allFollowups.length;
|
|
301
|
-
const today = allFollowups.filter((item) => item.status === 'today').length;
|
|
302
|
-
const overdue = allFollowups.filter(
|
|
303
|
-
(item) => item.status === 'overdue'
|
|
304
|
-
).length;
|
|
305
|
-
const upcoming = allFollowups.filter(
|
|
306
|
-
(item) => item.status === 'upcoming'
|
|
307
|
-
).length;
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
total,
|
|
311
|
-
today,
|
|
312
|
-
overdue,
|
|
313
|
-
upcoming,
|
|
314
|
-
};
|
|
315
|
-
}, [allFollowups]);
|
|
316
|
-
|
|
317
|
-
const personOptions = useMemo(
|
|
318
|
-
() =>
|
|
319
|
-
source.data.map((person) => ({
|
|
320
|
-
value: String(person.id),
|
|
321
|
-
label: person.name,
|
|
322
|
-
})),
|
|
323
|
-
[source.data]
|
|
324
|
-
);
|
|
325
|
-
|
|
326
335
|
const searchControls: SearchBarControl[] = [
|
|
327
336
|
{
|
|
328
337
|
id: 'followup-status',
|
|
@@ -363,6 +372,10 @@ export default function CrmFollowupsPage() {
|
|
|
363
372
|
];
|
|
364
373
|
|
|
365
374
|
const openCreateSheet = () => {
|
|
375
|
+
setSelectedPersonLabel('');
|
|
376
|
+
setPersonSearch('');
|
|
377
|
+
setDebouncedPersonSearch('');
|
|
378
|
+
setPersonPickerOpen(false);
|
|
366
379
|
form.reset({
|
|
367
380
|
personId: '',
|
|
368
381
|
next_action_at: '',
|
|
@@ -371,10 +384,14 @@ export default function CrmFollowupsPage() {
|
|
|
371
384
|
setSheetOpen(true);
|
|
372
385
|
};
|
|
373
386
|
|
|
374
|
-
const openRescheduleSheet = (row:
|
|
387
|
+
const openRescheduleSheet = (row: FollowupListItem) => {
|
|
388
|
+
setSelectedPersonLabel(row.person.name);
|
|
389
|
+
setPersonSearch(row.person.name);
|
|
390
|
+
setDebouncedPersonSearch(row.person.name);
|
|
391
|
+
setPersonPickerOpen(false);
|
|
375
392
|
form.reset({
|
|
376
393
|
personId: String(row.person.id),
|
|
377
|
-
next_action_at: toInputDateTimeValue(row.
|
|
394
|
+
next_action_at: toInputDateTimeValue(row.next_action_at),
|
|
378
395
|
notes: '',
|
|
379
396
|
});
|
|
380
397
|
setSheetOpen(true);
|
|
@@ -407,7 +424,8 @@ export default function CrmFollowupsPage() {
|
|
|
407
424
|
|
|
408
425
|
toast.success(t('toasts.scheduleSuccess'));
|
|
409
426
|
setSheetOpen(false);
|
|
410
|
-
|
|
427
|
+
setPersonPickerOpen(false);
|
|
428
|
+
await Promise.all([refetchFollowups(), refetchStats()]);
|
|
411
429
|
} catch {
|
|
412
430
|
toast.error(t('toasts.scheduleError'));
|
|
413
431
|
} finally {
|
|
@@ -415,6 +433,13 @@ export default function CrmFollowupsPage() {
|
|
|
415
433
|
}
|
|
416
434
|
};
|
|
417
435
|
|
|
436
|
+
const selectedPersonName =
|
|
437
|
+
personOptions.find(
|
|
438
|
+
(option) => String(option.id) === String(form.watch('personId') || '')
|
|
439
|
+
)?.name ||
|
|
440
|
+
selectedPersonLabel ||
|
|
441
|
+
'';
|
|
442
|
+
|
|
418
443
|
const statsCards = [
|
|
419
444
|
{
|
|
420
445
|
key: 'total',
|
|
@@ -479,8 +504,9 @@ export default function CrmFollowupsPage() {
|
|
|
479
504
|
setPage(1);
|
|
480
505
|
}}
|
|
481
506
|
onSearch={() => {
|
|
507
|
+
setDebouncedSearch(searchInput.trim());
|
|
482
508
|
setPage(1);
|
|
483
|
-
void
|
|
509
|
+
void Promise.all([refetchFollowups(), refetchStats()]);
|
|
484
510
|
}}
|
|
485
511
|
placeholder={t('filters.searchPlaceholder')}
|
|
486
512
|
controls={searchControls}
|
|
@@ -492,7 +518,7 @@ export default function CrmFollowupsPage() {
|
|
|
492
518
|
<Skeleton key={index} className="h-12 w-full" />
|
|
493
519
|
))}
|
|
494
520
|
</div>
|
|
495
|
-
) :
|
|
521
|
+
) : paginate.data.length === 0 ? (
|
|
496
522
|
<EmptyState
|
|
497
523
|
icon={<CalendarClock className="h-12 w-12" />}
|
|
498
524
|
title={t('empty.title')}
|
|
@@ -517,8 +543,8 @@ export default function CrmFollowupsPage() {
|
|
|
517
543
|
</TableRow>
|
|
518
544
|
</TableHeader>
|
|
519
545
|
<TableBody>
|
|
520
|
-
{
|
|
521
|
-
<TableRow key={row.person.id}>
|
|
546
|
+
{paginate.data.map((row) => (
|
|
547
|
+
<TableRow key={`${row.person.id}-${row.next_action_at}`}>
|
|
522
548
|
<TableCell>
|
|
523
549
|
<div className="min-w-[180px]">
|
|
524
550
|
<div className="font-medium">{row.person.name}</div>
|
|
@@ -537,15 +563,15 @@ export default function CrmFollowupsPage() {
|
|
|
537
563
|
</TableCell>
|
|
538
564
|
<TableCell>
|
|
539
565
|
{formatDateTime(
|
|
540
|
-
row.
|
|
566
|
+
row.next_action_at,
|
|
541
567
|
getSettingValue,
|
|
542
568
|
currentLocaleCode
|
|
543
569
|
)}
|
|
544
570
|
</TableCell>
|
|
545
571
|
<TableCell>
|
|
546
|
-
{row.
|
|
572
|
+
{row.last_interaction_at
|
|
547
573
|
? formatDateTime(
|
|
548
|
-
row.
|
|
574
|
+
row.last_interaction_at,
|
|
549
575
|
getSettingValue,
|
|
550
576
|
currentLocaleCode
|
|
551
577
|
)
|
|
@@ -584,7 +610,7 @@ export default function CrmFollowupsPage() {
|
|
|
584
610
|
<PaginationFooter
|
|
585
611
|
currentPage={page}
|
|
586
612
|
pageSize={pageSize}
|
|
587
|
-
totalItems={
|
|
613
|
+
totalItems={paginate.total}
|
|
588
614
|
onPageChange={setPage}
|
|
589
615
|
onPageSizeChange={(nextPageSize) => {
|
|
590
616
|
setPageSize(nextPageSize);
|
|
@@ -599,6 +625,9 @@ export default function CrmFollowupsPage() {
|
|
|
599
625
|
onOpenChange={(open) => {
|
|
600
626
|
if (!isSubmitting) {
|
|
601
627
|
setSheetOpen(open);
|
|
628
|
+
if (!open) {
|
|
629
|
+
setPersonPickerOpen(false);
|
|
630
|
+
}
|
|
602
631
|
}
|
|
603
632
|
}}
|
|
604
633
|
>
|
|
@@ -617,24 +646,84 @@ export default function CrmFollowupsPage() {
|
|
|
617
646
|
control={form.control}
|
|
618
647
|
name="personId"
|
|
619
648
|
render={({ field }) => (
|
|
620
|
-
<FormItem>
|
|
649
|
+
<FormItem className="flex flex-col">
|
|
621
650
|
<FormLabel>{t('form.person')}</FormLabel>
|
|
622
|
-
<
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
651
|
+
<Popover
|
|
652
|
+
open={personPickerOpen}
|
|
653
|
+
onOpenChange={setPersonPickerOpen}
|
|
654
|
+
>
|
|
655
|
+
<PopoverTrigger asChild>
|
|
656
|
+
<FormControl>
|
|
657
|
+
<Button
|
|
658
|
+
type="button"
|
|
659
|
+
variant="outline"
|
|
660
|
+
role="combobox"
|
|
661
|
+
className={cn(
|
|
662
|
+
'w-full justify-between',
|
|
663
|
+
!field.value && 'text-muted-foreground'
|
|
664
|
+
)}
|
|
665
|
+
>
|
|
666
|
+
<span className="truncate">
|
|
667
|
+
{field.value
|
|
668
|
+
? selectedPersonName ||
|
|
669
|
+
`#${String(field.value)}`
|
|
670
|
+
: t('form.personPlaceholder')}
|
|
671
|
+
</span>
|
|
672
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
673
|
+
</Button>
|
|
674
|
+
</FormControl>
|
|
675
|
+
</PopoverTrigger>
|
|
676
|
+
<PopoverContent
|
|
677
|
+
className="p-0"
|
|
678
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
679
|
+
align="start"
|
|
680
|
+
>
|
|
681
|
+
<Command shouldFilter={false}>
|
|
682
|
+
<CommandInput
|
|
683
|
+
placeholder={t('form.personSearchPlaceholder')}
|
|
684
|
+
value={personSearch}
|
|
685
|
+
onValueChange={setPersonSearch}
|
|
627
686
|
/>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
687
|
+
<CommandList>
|
|
688
|
+
<CommandEmpty>
|
|
689
|
+
{isLoadingPersons
|
|
690
|
+
? t('form.personLoading')
|
|
691
|
+
: t('form.personEmpty')}
|
|
692
|
+
</CommandEmpty>
|
|
693
|
+
<CommandGroup>
|
|
694
|
+
{personOptions.map((option) => {
|
|
695
|
+
const optionValue = String(option.id);
|
|
696
|
+
const isSelected = field.value === optionValue;
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<CommandItem
|
|
700
|
+
key={optionValue}
|
|
701
|
+
value={`${option.name} ${optionValue}`}
|
|
702
|
+
onSelect={() => {
|
|
703
|
+
field.onChange(optionValue);
|
|
704
|
+
setSelectedPersonLabel(option.name);
|
|
705
|
+
setPersonPickerOpen(false);
|
|
706
|
+
}}
|
|
707
|
+
>
|
|
708
|
+
<Check
|
|
709
|
+
className={cn(
|
|
710
|
+
'mr-2 h-4 w-4',
|
|
711
|
+
isSelected
|
|
712
|
+
? 'opacity-100'
|
|
713
|
+
: 'opacity-0'
|
|
714
|
+
)}
|
|
715
|
+
/>
|
|
716
|
+
<span className="truncate">
|
|
717
|
+
{option.name}
|
|
718
|
+
</span>
|
|
719
|
+
</CommandItem>
|
|
720
|
+
);
|
|
721
|
+
})}
|
|
722
|
+
</CommandGroup>
|
|
723
|
+
</CommandList>
|
|
724
|
+
</Command>
|
|
725
|
+
</PopoverContent>
|
|
726
|
+
</Popover>
|
|
638
727
|
<FormMessage />
|
|
639
728
|
</FormItem>
|
|
640
729
|
)}
|