@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
|
@@ -21,7 +21,6 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
|
21
21
|
import { Badge } from '@/components/ui/badge';
|
|
22
22
|
import { Button } from '@/components/ui/button';
|
|
23
23
|
import { Card, CardContent } from '@/components/ui/card';
|
|
24
|
-
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
25
24
|
import {
|
|
26
25
|
DropdownMenu,
|
|
27
26
|
DropdownMenuContent,
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
DropdownMenuSeparator,
|
|
30
29
|
DropdownMenuTrigger,
|
|
31
30
|
} from '@/components/ui/dropdown-menu';
|
|
31
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
32
32
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
33
33
|
import {
|
|
34
34
|
Table,
|
|
@@ -41,12 +41,12 @@ import {
|
|
|
41
41
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
42
42
|
import { formatDate } from '@/lib/format-date';
|
|
43
43
|
import { cn } from '@/lib/utils';
|
|
44
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
44
45
|
import {
|
|
45
46
|
type ColumnDef,
|
|
46
47
|
type SortingState,
|
|
47
48
|
flexRender,
|
|
48
49
|
getCoreRowModel,
|
|
49
|
-
getSortedRowModel,
|
|
50
50
|
useReactTable,
|
|
51
51
|
} from '@tanstack/react-table';
|
|
52
52
|
import {
|
|
@@ -65,18 +65,23 @@ import {
|
|
|
65
65
|
import { useTranslations } from 'next-intl';
|
|
66
66
|
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
|
67
67
|
import { toast } from 'sonner';
|
|
68
|
-
import { CrmNav } from '../_components/crm-nav';
|
|
69
|
-
import { crmMockAccounts, crmOwners } from '../_lib/crm-mocks';
|
|
70
68
|
import { AccountFormSheet } from './_components/account-form-sheet';
|
|
71
69
|
import type {
|
|
72
70
|
Account,
|
|
73
71
|
AccountFormValues,
|
|
74
72
|
AccountStats,
|
|
73
|
+
PaginatedResult,
|
|
74
|
+
UserOption,
|
|
75
75
|
} from './_components/account-types';
|
|
76
76
|
|
|
77
77
|
const ACCOUNT_VIEW_STORAGE_KEY = 'contact-account-view-mode';
|
|
78
|
+
|
|
78
79
|
type AccountViewMode = 'table' | 'cards';
|
|
79
80
|
|
|
81
|
+
type RequestError = {
|
|
82
|
+
message?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
80
85
|
function getAccountInitials(name: string) {
|
|
81
86
|
return name
|
|
82
87
|
.split(' ')
|
|
@@ -122,11 +127,7 @@ function AccountInfoTile({
|
|
|
122
127
|
)}
|
|
123
128
|
>
|
|
124
129
|
<div className="mb-2 flex items-center gap-2">
|
|
125
|
-
<div
|
|
126
|
-
className={cn(
|
|
127
|
-
'flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border bg-background text-muted-foreground'
|
|
128
|
-
)}
|
|
129
|
-
>
|
|
130
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border bg-background text-muted-foreground">
|
|
130
131
|
{icon}
|
|
131
132
|
</div>
|
|
132
133
|
<span className="truncate text-[10px] font-semibold tracking-[0.14em] text-muted-foreground uppercase">
|
|
@@ -149,6 +150,7 @@ function AccountInfoTile({
|
|
|
149
150
|
export default function AccountsPage() {
|
|
150
151
|
const t = useTranslations('contact.AccountsPage');
|
|
151
152
|
const crmT = useTranslations('contact.CrmMenu');
|
|
153
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
152
154
|
|
|
153
155
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
154
156
|
const [page, setPage] = useState(1);
|
|
@@ -162,25 +164,14 @@ export default function AccountsPage() {
|
|
|
162
164
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
163
165
|
const [accountToDelete, setAccountToDelete] = useState<Account | null>(null);
|
|
164
166
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
167
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
165
168
|
const [viewMode, setViewMode] = useState<AccountViewMode>('table');
|
|
166
|
-
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
167
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
168
|
-
|
|
169
|
-
// Simulated API calls with mock data
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
setIsLoading(true);
|
|
172
|
-
// Simulate API call delay
|
|
173
|
-
const timeout = setTimeout(() => {
|
|
174
|
-
setAccounts(crmMockAccounts);
|
|
175
|
-
setIsLoading(false);
|
|
176
|
-
}, 300);
|
|
177
|
-
return () => clearTimeout(timeout);
|
|
178
|
-
}, []);
|
|
179
169
|
|
|
180
170
|
useEffect(() => {
|
|
181
171
|
const timeout = setTimeout(() => {
|
|
182
172
|
setDebouncedSearch(searchInput.trim());
|
|
183
173
|
}, 300);
|
|
174
|
+
|
|
184
175
|
return () => clearTimeout(timeout);
|
|
185
176
|
}, [searchInput]);
|
|
186
177
|
|
|
@@ -193,62 +184,125 @@ export default function AccountsPage() {
|
|
|
193
184
|
setViewMode(savedViewMode);
|
|
194
185
|
}
|
|
195
186
|
} catch {
|
|
196
|
-
// Ignore storage read failures
|
|
187
|
+
// Ignore storage read failures.
|
|
197
188
|
}
|
|
198
189
|
}, []);
|
|
199
190
|
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (debouncedSearch) {
|
|
205
|
-
const search = debouncedSearch.toLowerCase();
|
|
206
|
-
result = result.filter(
|
|
207
|
-
(account) =>
|
|
208
|
-
account.name.toLowerCase().includes(search) ||
|
|
209
|
-
account.trade_name?.toLowerCase().includes(search) ||
|
|
210
|
-
account.email?.toLowerCase().includes(search) ||
|
|
211
|
-
account.city?.toLowerCase().includes(search)
|
|
212
|
-
);
|
|
213
|
-
}
|
|
191
|
+
const currentSort = sorting[0];
|
|
192
|
+
const sortField = currentSort?.id === 'created_at' ? 'created_at' : 'name';
|
|
193
|
+
const sortOrder = currentSort?.desc ? 'desc' : 'asc';
|
|
214
194
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
195
|
+
const {
|
|
196
|
+
data: stats = {
|
|
197
|
+
total: 0,
|
|
198
|
+
active: 0,
|
|
199
|
+
customers: 0,
|
|
200
|
+
prospects: 0,
|
|
201
|
+
},
|
|
202
|
+
refetch: refetchStats,
|
|
203
|
+
} = useQuery<AccountStats>({
|
|
204
|
+
queryKey: ['contact-accounts-stats', currentLocaleCode],
|
|
205
|
+
queryFn: async () => {
|
|
206
|
+
const response = await request<AccountStats>({
|
|
207
|
+
url: '/person/accounts/stats',
|
|
208
|
+
method: 'GET',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return response.data;
|
|
212
|
+
},
|
|
213
|
+
placeholderData: (previous) =>
|
|
214
|
+
previous ?? {
|
|
215
|
+
total: 0,
|
|
216
|
+
active: 0,
|
|
217
|
+
customers: 0,
|
|
218
|
+
prospects: 0,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
218
221
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const { data: owners = [] } = useQuery<UserOption[]>({
|
|
223
|
+
queryKey: ['contact-account-owner-options', currentLocaleCode],
|
|
224
|
+
queryFn: async () => {
|
|
225
|
+
const response = await request<UserOption[] | { data?: UserOption[] }>({
|
|
226
|
+
url: '/person/owner-options',
|
|
227
|
+
method: 'GET',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return Array.isArray(response.data)
|
|
231
|
+
? response.data
|
|
232
|
+
: response.data?.data || [];
|
|
233
|
+
},
|
|
234
|
+
placeholderData: (previous) => previous ?? [],
|
|
235
|
+
});
|
|
224
236
|
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
const {
|
|
238
|
+
data: paginate = {
|
|
239
|
+
data: [],
|
|
240
|
+
total: 0,
|
|
241
|
+
page: 1,
|
|
242
|
+
pageSize: 12,
|
|
243
|
+
lastPage: 1,
|
|
244
|
+
prev: null,
|
|
245
|
+
next: null,
|
|
246
|
+
},
|
|
247
|
+
isLoading,
|
|
248
|
+
refetch: refetchAccounts,
|
|
249
|
+
} = useQuery<PaginatedResult<Account>>({
|
|
250
|
+
queryKey: [
|
|
251
|
+
'contact-accounts',
|
|
252
|
+
page,
|
|
253
|
+
pageSize,
|
|
254
|
+
debouncedSearch,
|
|
255
|
+
statusFilter,
|
|
256
|
+
lifecycleFilter,
|
|
257
|
+
sortField,
|
|
258
|
+
sortOrder,
|
|
259
|
+
currentLocaleCode,
|
|
260
|
+
],
|
|
261
|
+
queryFn: async () => {
|
|
262
|
+
const params = new URLSearchParams();
|
|
263
|
+
params.set('page', String(page));
|
|
264
|
+
params.set('pageSize', String(pageSize));
|
|
265
|
+
params.set('sortField', sortField);
|
|
266
|
+
params.set('sortOrder', sortOrder);
|
|
267
|
+
if (debouncedSearch) {
|
|
268
|
+
params.set('search', debouncedSearch);
|
|
269
|
+
}
|
|
270
|
+
if (statusFilter !== 'all') {
|
|
271
|
+
params.set('status', statusFilter);
|
|
272
|
+
}
|
|
273
|
+
if (lifecycleFilter !== 'all') {
|
|
274
|
+
params.set('lifecycle_stage', lifecycleFilter);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const response = await request<PaginatedResult<Account>>({
|
|
278
|
+
url: `/person/accounts?${params.toString()}`,
|
|
279
|
+
method: 'GET',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return response.data;
|
|
283
|
+
},
|
|
284
|
+
placeholderData: (previous) =>
|
|
285
|
+
previous ?? {
|
|
286
|
+
data: [],
|
|
287
|
+
total: 0,
|
|
288
|
+
page: 1,
|
|
289
|
+
pageSize: 12,
|
|
290
|
+
lastPage: 1,
|
|
291
|
+
prev: null,
|
|
292
|
+
next: null,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
227
295
|
|
|
228
|
-
const
|
|
296
|
+
const totalPages = Math.max(
|
|
297
|
+
1,
|
|
298
|
+
paginate.lastPage ?? (Math.ceil((paginate.total || 0) / pageSize) || 1)
|
|
299
|
+
);
|
|
229
300
|
|
|
230
301
|
useEffect(() => {
|
|
231
|
-
if (page >
|
|
232
|
-
setPage(
|
|
302
|
+
if (page > totalPages) {
|
|
303
|
+
setPage(totalPages);
|
|
233
304
|
}
|
|
234
|
-
}, [page,
|
|
235
|
-
|
|
236
|
-
const paginatedAccounts = useMemo(() => {
|
|
237
|
-
const startIndex = (page - 1) * pageSize;
|
|
238
|
-
return filteredAccounts.slice(startIndex, startIndex + pageSize);
|
|
239
|
-
}, [filteredAccounts, page, pageSize]);
|
|
240
|
-
|
|
241
|
-
// Calculate stats
|
|
242
|
-
const stats: AccountStats = useMemo(() => {
|
|
243
|
-
return {
|
|
244
|
-
total: accounts.length,
|
|
245
|
-
active: accounts.filter((a) => a.status === 'active').length,
|
|
246
|
-
customers: accounts.filter((a) => a.lifecycle_stage === 'customer')
|
|
247
|
-
.length,
|
|
248
|
-
prospects: accounts.filter((a) => a.lifecycle_stage === 'prospect')
|
|
249
|
-
.length,
|
|
250
|
-
};
|
|
251
|
-
}, [accounts]);
|
|
305
|
+
}, [page, totalPages]);
|
|
252
306
|
|
|
253
307
|
const openCreateSheet = () => {
|
|
254
308
|
setAccountToEdit(null);
|
|
@@ -260,56 +314,79 @@ export default function AccountsPage() {
|
|
|
260
314
|
setFormSheetOpen(true);
|
|
261
315
|
};
|
|
262
316
|
|
|
263
|
-
const handleFormSubmit = (data: AccountFormValues) => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
317
|
+
const handleFormSubmit = async (data: AccountFormValues) => {
|
|
318
|
+
try {
|
|
319
|
+
setIsSubmitting(true);
|
|
320
|
+
|
|
321
|
+
if (accountToEdit) {
|
|
322
|
+
await request({
|
|
323
|
+
url: `/person/accounts/${accountToEdit.id}`,
|
|
324
|
+
method: 'PATCH',
|
|
325
|
+
data,
|
|
326
|
+
});
|
|
327
|
+
toast.success(t('editSuccess'));
|
|
328
|
+
} else {
|
|
329
|
+
await request({
|
|
330
|
+
url: '/person/accounts',
|
|
331
|
+
method: 'POST',
|
|
332
|
+
data,
|
|
333
|
+
});
|
|
334
|
+
toast.success(t('createSuccess'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setFormSheetOpen(false);
|
|
338
|
+
setAccountToEdit(null);
|
|
339
|
+
await Promise.all([refetchAccounts(), refetchStats()]);
|
|
340
|
+
} catch (error: unknown) {
|
|
341
|
+
const message =
|
|
342
|
+
typeof error === 'object' && error && 'message' in error
|
|
343
|
+
? (error as RequestError).message
|
|
344
|
+
: null;
|
|
345
|
+
toast.error(message || t('saveError'));
|
|
346
|
+
throw error;
|
|
347
|
+
} finally {
|
|
348
|
+
setIsSubmitting(false);
|
|
286
349
|
}
|
|
287
|
-
setFormSheetOpen(false);
|
|
288
350
|
};
|
|
289
351
|
|
|
290
|
-
const handleDelete = () => {
|
|
352
|
+
const handleDelete = async () => {
|
|
291
353
|
if (!accountToDelete) return;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
setIsDeleting(true);
|
|
357
|
+
await request({
|
|
358
|
+
url: '/person/accounts',
|
|
359
|
+
method: 'DELETE',
|
|
360
|
+
data: {
|
|
361
|
+
ids: [accountToDelete.id],
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
297
365
|
toast.success(t('deleteSuccess'));
|
|
298
366
|
setDeleteDialogOpen(false);
|
|
299
367
|
setAccountToDelete(null);
|
|
368
|
+
await Promise.all([refetchAccounts(), refetchStats()]);
|
|
369
|
+
} catch (error: unknown) {
|
|
370
|
+
const message =
|
|
371
|
+
typeof error === 'object' && error && 'message' in error
|
|
372
|
+
? (error as RequestError).message
|
|
373
|
+
: null;
|
|
374
|
+
toast.error(message || t('deleteError'));
|
|
375
|
+
} finally {
|
|
300
376
|
setIsDeleting(false);
|
|
301
|
-
}
|
|
377
|
+
}
|
|
302
378
|
};
|
|
303
379
|
|
|
304
380
|
const handleViewModeChange = (value: string) => {
|
|
305
381
|
if (value !== 'table' && value !== 'cards') {
|
|
306
382
|
return;
|
|
307
383
|
}
|
|
384
|
+
|
|
308
385
|
setViewMode(value);
|
|
309
386
|
try {
|
|
310
387
|
window.localStorage.setItem(ACCOUNT_VIEW_STORAGE_KEY, value);
|
|
311
388
|
} catch {
|
|
312
|
-
// Ignore storage write failures
|
|
389
|
+
// Ignore storage write failures.
|
|
313
390
|
}
|
|
314
391
|
};
|
|
315
392
|
|
|
@@ -320,7 +397,10 @@ export default function AccountsPage() {
|
|
|
320
397
|
header: ({ column }) => (
|
|
321
398
|
<Button
|
|
322
399
|
variant="ghost"
|
|
323
|
-
onClick={() =>
|
|
400
|
+
onClick={() => {
|
|
401
|
+
column.toggleSorting(column.getIsSorted() === 'asc');
|
|
402
|
+
setPage(1);
|
|
403
|
+
}}
|
|
324
404
|
className="-ml-4"
|
|
325
405
|
>
|
|
326
406
|
{t('columnName')}
|
|
@@ -332,7 +412,7 @@ export default function AccountsPage() {
|
|
|
332
412
|
return (
|
|
333
413
|
<div className="flex items-center gap-3">
|
|
334
414
|
<Avatar className="h-9 w-9 rounded-lg">
|
|
335
|
-
<AvatarFallback className="bg-slate-100
|
|
415
|
+
<AvatarFallback className="rounded-lg bg-slate-100 text-xs font-semibold uppercase text-slate-700">
|
|
336
416
|
{getAccountInitials(account.name)}
|
|
337
417
|
</AvatarFallback>
|
|
338
418
|
</Avatar>
|
|
@@ -353,7 +433,7 @@ export default function AccountsPage() {
|
|
|
353
433
|
header: t('columnIndustry'),
|
|
354
434
|
cell: ({ row }) => (
|
|
355
435
|
<span className="text-sm text-muted-foreground">
|
|
356
|
-
{row.getValue('industry') || '-'}
|
|
436
|
+
{(row.getValue('industry') as string | null) || '-'}
|
|
357
437
|
</span>
|
|
358
438
|
),
|
|
359
439
|
},
|
|
@@ -361,7 +441,7 @@ export default function AccountsPage() {
|
|
|
361
441
|
accessorKey: 'lifecycle_stage',
|
|
362
442
|
header: t('columnStage'),
|
|
363
443
|
cell: ({ row }) => {
|
|
364
|
-
const stage = row.getValue('lifecycle_stage') as string;
|
|
444
|
+
const stage = row.getValue('lifecycle_stage') as string | null;
|
|
365
445
|
return (
|
|
366
446
|
<Badge
|
|
367
447
|
variant="outline"
|
|
@@ -370,7 +450,7 @@ export default function AccountsPage() {
|
|
|
370
450
|
getLifecycleStageColor(stage)
|
|
371
451
|
)}
|
|
372
452
|
>
|
|
373
|
-
{t(`stage_${stage}` as
|
|
453
|
+
{stage ? t(`stage_${stage}` as never) : '-'}
|
|
374
454
|
</Badge>
|
|
375
455
|
);
|
|
376
456
|
},
|
|
@@ -390,7 +470,7 @@ export default function AccountsPage() {
|
|
|
390
470
|
: 'border-red-500/20 bg-red-500/10 text-red-600'
|
|
391
471
|
)}
|
|
392
472
|
>
|
|
393
|
-
{t(`status_${status}` as
|
|
473
|
+
{t(`status_${status}` as never)}
|
|
394
474
|
</Badge>
|
|
395
475
|
);
|
|
396
476
|
},
|
|
@@ -412,7 +492,7 @@ export default function AccountsPage() {
|
|
|
412
492
|
header: t('columnCity'),
|
|
413
493
|
cell: ({ row }) => (
|
|
414
494
|
<span className="text-sm text-muted-foreground">
|
|
415
|
-
{row.getValue('city') || '-'}
|
|
495
|
+
{(row.getValue('city') as string | null) || '-'}
|
|
416
496
|
</span>
|
|
417
497
|
),
|
|
418
498
|
},
|
|
@@ -421,7 +501,10 @@ export default function AccountsPage() {
|
|
|
421
501
|
header: ({ column }) => (
|
|
422
502
|
<Button
|
|
423
503
|
variant="ghost"
|
|
424
|
-
onClick={() =>
|
|
504
|
+
onClick={() => {
|
|
505
|
+
column.toggleSorting(column.getIsSorted() === 'asc');
|
|
506
|
+
setPage(1);
|
|
507
|
+
}}
|
|
425
508
|
className="-ml-4"
|
|
426
509
|
>
|
|
427
510
|
{t('columnCreatedAt')}
|
|
@@ -430,7 +513,11 @@ export default function AccountsPage() {
|
|
|
430
513
|
),
|
|
431
514
|
cell: ({ row }) => (
|
|
432
515
|
<span className="text-sm text-muted-foreground">
|
|
433
|
-
{formatDate(
|
|
516
|
+
{formatDate(
|
|
517
|
+
row.getValue('created_at') as string,
|
|
518
|
+
getSettingValue,
|
|
519
|
+
currentLocaleCode
|
|
520
|
+
)}
|
|
434
521
|
</span>
|
|
435
522
|
),
|
|
436
523
|
},
|
|
@@ -467,16 +554,16 @@ export default function AccountsPage() {
|
|
|
467
554
|
},
|
|
468
555
|
},
|
|
469
556
|
],
|
|
470
|
-
[t]
|
|
557
|
+
[currentLocaleCode, getSettingValue, t]
|
|
471
558
|
);
|
|
472
559
|
|
|
473
560
|
const table = useReactTable({
|
|
474
|
-
data:
|
|
561
|
+
data: paginate.data,
|
|
475
562
|
columns,
|
|
476
563
|
state: { sorting },
|
|
564
|
+
manualSorting: true,
|
|
477
565
|
onSortingChange: setSorting,
|
|
478
566
|
getCoreRowModel: getCoreRowModel(),
|
|
479
|
-
getSortedRowModel: getSortedRowModel(),
|
|
480
567
|
});
|
|
481
568
|
|
|
482
569
|
const accountsRows = table.getRowModel().rows;
|
|
@@ -519,7 +606,7 @@ export default function AccountsPage() {
|
|
|
519
606
|
const searchControls: SearchBarControl[] = [
|
|
520
607
|
{
|
|
521
608
|
id: 'lifecycle-filter',
|
|
522
|
-
type: 'select'
|
|
609
|
+
type: 'select',
|
|
523
610
|
value: lifecycleFilter,
|
|
524
611
|
onChange: (value: string) => {
|
|
525
612
|
setLifecycleFilter(value);
|
|
@@ -580,9 +667,7 @@ export default function AccountsPage() {
|
|
|
580
667
|
setSearchInput(value);
|
|
581
668
|
setPage(1);
|
|
582
669
|
}}
|
|
583
|
-
onSearch={() =>
|
|
584
|
-
setPage(1);
|
|
585
|
-
}}
|
|
670
|
+
onSearch={() => setPage(1)}
|
|
586
671
|
placeholder={t('searchPlaceholder')}
|
|
587
672
|
controls={searchControls}
|
|
588
673
|
/>
|
|
@@ -653,7 +738,7 @@ export default function AccountsPage() {
|
|
|
653
738
|
))}
|
|
654
739
|
</div>
|
|
655
740
|
)
|
|
656
|
-
) :
|
|
741
|
+
) : paginate.data.length === 0 ? (
|
|
657
742
|
<EmptyState
|
|
658
743
|
icon={<Building2 className="h-12 w-12" />}
|
|
659
744
|
title={t('emptyStateTitle')}
|
|
@@ -705,147 +790,139 @@ export default function AccountsPage() {
|
|
|
705
790
|
</div>
|
|
706
791
|
) : (
|
|
707
792
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
708
|
-
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
<Badge
|
|
731
|
-
variant="outline"
|
|
732
|
-
className={cn(
|
|
733
|
-
'border px-2 py-0.5 text-[11px] font-medium',
|
|
734
|
-
getLifecycleStageColor(
|
|
735
|
-
account.lifecycle_stage
|
|
736
|
-
)
|
|
737
|
-
)}
|
|
738
|
-
>
|
|
739
|
-
{t(`stage_${account.lifecycle_stage}` as any) ||
|
|
740
|
-
account.lifecycle_stage}
|
|
741
|
-
</Badge>
|
|
742
|
-
|
|
743
|
-
<Badge
|
|
744
|
-
variant="outline"
|
|
745
|
-
className={cn(
|
|
746
|
-
'border px-2 py-0.5 text-[11px] font-medium',
|
|
747
|
-
account.status === 'active'
|
|
748
|
-
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
749
|
-
: 'border-red-500/20 bg-red-500/10 text-red-600'
|
|
750
|
-
)}
|
|
751
|
-
>
|
|
752
|
-
{t(`status_${account.status}` as any) ||
|
|
753
|
-
account.status}
|
|
754
|
-
</Badge>
|
|
755
|
-
</div>
|
|
756
|
-
|
|
757
|
-
<div className="min-w-0">
|
|
758
|
-
<h3 className="line-clamp-2 text-sm font-semibold text-foreground">
|
|
759
|
-
{account.name}
|
|
760
|
-
</h3>
|
|
761
|
-
{account.trade_name && (
|
|
762
|
-
<p className="truncate text-xs text-muted-foreground">
|
|
763
|
-
{account.trade_name}
|
|
764
|
-
</p>
|
|
793
|
+
{paginate.data.map((account) => (
|
|
794
|
+
<Card
|
|
795
|
+
key={account.id}
|
|
796
|
+
className="group h-full overflow-hidden border-border/70 py-0 transition-colors hover:border-border hover:shadow-md"
|
|
797
|
+
onDoubleClick={() => openEditSheet(account)}
|
|
798
|
+
>
|
|
799
|
+
<CardContent className="flex h-full flex-col gap-3 p-4">
|
|
800
|
+
<div className="flex items-start justify-between gap-3">
|
|
801
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
802
|
+
<Avatar className="h-10 w-10 shrink-0 rounded-lg border border-slate-500/20">
|
|
803
|
+
<AvatarFallback className="rounded-lg bg-slate-500/8 text-sm font-semibold uppercase text-slate-700 dark:text-slate-200">
|
|
804
|
+
{getAccountInitials(account.name)}
|
|
805
|
+
</AvatarFallback>
|
|
806
|
+
</Avatar>
|
|
807
|
+
|
|
808
|
+
<div className="min-w-0 space-y-1.5">
|
|
809
|
+
<div className="flex flex-wrap gap-1.5">
|
|
810
|
+
<Badge
|
|
811
|
+
variant="outline"
|
|
812
|
+
className={cn(
|
|
813
|
+
'border px-2 py-0.5 text-[11px] font-medium',
|
|
814
|
+
getLifecycleStageColor(account.lifecycle_stage)
|
|
765
815
|
)}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
816
|
+
>
|
|
817
|
+
{account.lifecycle_stage
|
|
818
|
+
? t(`stage_${account.lifecycle_stage}` as never)
|
|
819
|
+
: '-'}
|
|
820
|
+
</Badge>
|
|
821
|
+
|
|
822
|
+
<Badge
|
|
823
|
+
variant="outline"
|
|
824
|
+
className={cn(
|
|
825
|
+
'border px-2 py-0.5 text-[11px] font-medium',
|
|
826
|
+
account.status === 'active'
|
|
827
|
+
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
828
|
+
: 'border-red-500/20 bg-red-500/10 text-red-600'
|
|
770
829
|
)}
|
|
771
|
-
|
|
830
|
+
>
|
|
831
|
+
{t(`status_${account.status}` as never)}
|
|
832
|
+
</Badge>
|
|
772
833
|
</div>
|
|
773
|
-
</div>
|
|
774
834
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
className="
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
</DropdownMenuItem>
|
|
792
|
-
<DropdownMenuSeparator />
|
|
793
|
-
<DropdownMenuItem
|
|
794
|
-
className="text-red-600"
|
|
795
|
-
onClick={() => {
|
|
796
|
-
setAccountToDelete(account);
|
|
797
|
-
setDeleteDialogOpen(true);
|
|
798
|
-
}}
|
|
799
|
-
>
|
|
800
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
801
|
-
{t('delete')}
|
|
802
|
-
</DropdownMenuItem>
|
|
803
|
-
</DropdownMenuContent>
|
|
804
|
-
</DropdownMenu>
|
|
835
|
+
<div className="min-w-0">
|
|
836
|
+
<h3 className="line-clamp-2 text-sm font-semibold text-foreground">
|
|
837
|
+
{account.name}
|
|
838
|
+
</h3>
|
|
839
|
+
{account.trade_name && (
|
|
840
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
841
|
+
{account.trade_name}
|
|
842
|
+
</p>
|
|
843
|
+
)}
|
|
844
|
+
{account.industry && (
|
|
845
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
846
|
+
{account.industry}
|
|
847
|
+
</p>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
805
851
|
</div>
|
|
806
852
|
|
|
807
|
-
<
|
|
808
|
-
<
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
853
|
+
<DropdownMenu>
|
|
854
|
+
<DropdownMenuTrigger asChild>
|
|
855
|
+
<Button
|
|
856
|
+
variant="ghost"
|
|
857
|
+
size="icon"
|
|
858
|
+
className="h-8 w-8 rounded-lg"
|
|
859
|
+
>
|
|
860
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
861
|
+
</Button>
|
|
862
|
+
</DropdownMenuTrigger>
|
|
863
|
+
<DropdownMenuContent align="end">
|
|
864
|
+
<DropdownMenuItem
|
|
865
|
+
onClick={() => openEditSheet(account)}
|
|
866
|
+
>
|
|
867
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
868
|
+
{t('edit')}
|
|
869
|
+
</DropdownMenuItem>
|
|
870
|
+
<DropdownMenuSeparator />
|
|
871
|
+
<DropdownMenuItem
|
|
872
|
+
className="text-red-600"
|
|
873
|
+
onClick={() => {
|
|
874
|
+
setAccountToDelete(account);
|
|
875
|
+
setDeleteDialogOpen(true);
|
|
876
|
+
}}
|
|
877
|
+
>
|
|
878
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
879
|
+
{t('delete')}
|
|
880
|
+
</DropdownMenuItem>
|
|
881
|
+
</DropdownMenuContent>
|
|
882
|
+
</DropdownMenu>
|
|
883
|
+
</div>
|
|
829
884
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
885
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
886
|
+
<AccountInfoTile
|
|
887
|
+
icon={<Mail className="h-3 w-3" />}
|
|
888
|
+
label={t('tileEmail')}
|
|
889
|
+
value={account.email || '-'}
|
|
890
|
+
/>
|
|
891
|
+
<AccountInfoTile
|
|
892
|
+
icon={<Phone className="h-3 w-3" />}
|
|
893
|
+
label={t('tilePhone')}
|
|
894
|
+
value={account.phone || '-'}
|
|
895
|
+
/>
|
|
896
|
+
<AccountInfoTile
|
|
897
|
+
icon={<Globe className="h-3 w-3" />}
|
|
898
|
+
label={t('tileWebsite')}
|
|
899
|
+
value={account.website || '-'}
|
|
900
|
+
/>
|
|
901
|
+
<AccountInfoTile
|
|
902
|
+
icon={<Building2 className="h-3 w-3" />}
|
|
903
|
+
label={t('tileIndustry')}
|
|
904
|
+
value={account.industry || '-'}
|
|
905
|
+
/>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<div className="border-t pt-2 text-xs text-muted-foreground">
|
|
909
|
+
<div className="flex items-center justify-between">
|
|
910
|
+
<span>
|
|
911
|
+
{t('owner')}:{' '}
|
|
912
|
+
{account.owner_user?.name || t('unassigned')}
|
|
913
|
+
</span>
|
|
837
914
|
</div>
|
|
838
|
-
</
|
|
839
|
-
</
|
|
840
|
-
|
|
841
|
-
|
|
915
|
+
</div>
|
|
916
|
+
</CardContent>
|
|
917
|
+
</Card>
|
|
918
|
+
))}
|
|
842
919
|
</div>
|
|
843
920
|
)}
|
|
844
921
|
|
|
845
922
|
<PaginationFooter
|
|
846
923
|
currentPage={page}
|
|
847
924
|
pageSize={pageSize}
|
|
848
|
-
totalItems={
|
|
925
|
+
totalItems={paginate.total}
|
|
849
926
|
onPageChange={setPage}
|
|
850
927
|
onPageSizeChange={(nextPageSize) => {
|
|
851
928
|
setPageSize(nextPageSize);
|
|
@@ -859,8 +936,9 @@ export default function AccountsPage() {
|
|
|
859
936
|
open={formSheetOpen}
|
|
860
937
|
onOpenChange={setFormSheetOpen}
|
|
861
938
|
account={accountToEdit}
|
|
862
|
-
owners={
|
|
939
|
+
owners={owners}
|
|
863
940
|
onSubmit={handleFormSubmit}
|
|
941
|
+
isLoading={isSubmitting}
|
|
864
942
|
/>
|
|
865
943
|
|
|
866
944
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|