@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
|
@@ -1,15 +1,886 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EmptyState,
|
|
5
|
+
Page,
|
|
6
|
+
PageHeader,
|
|
7
|
+
PaginationFooter,
|
|
8
|
+
SearchBar,
|
|
9
|
+
type SearchBarControl,
|
|
10
|
+
StatsCards,
|
|
11
|
+
} from '@/components/entity-list';
|
|
12
|
+
import {
|
|
13
|
+
AlertDialog,
|
|
14
|
+
AlertDialogAction,
|
|
15
|
+
AlertDialogCancel,
|
|
16
|
+
AlertDialogContent,
|
|
17
|
+
AlertDialogDescription,
|
|
18
|
+
AlertDialogHeader,
|
|
19
|
+
AlertDialogTitle,
|
|
20
|
+
} from '@/components/ui/alert-dialog';
|
|
21
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
22
|
+
import { Badge } from '@/components/ui/badge';
|
|
23
|
+
import { Button } from '@/components/ui/button';
|
|
24
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
25
|
+
import {
|
|
26
|
+
DropdownMenu,
|
|
27
|
+
DropdownMenuContent,
|
|
28
|
+
DropdownMenuItem,
|
|
29
|
+
DropdownMenuSeparator,
|
|
30
|
+
DropdownMenuTrigger,
|
|
31
|
+
} from '@/components/ui/dropdown-menu';
|
|
32
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
33
|
+
import {
|
|
34
|
+
Table,
|
|
35
|
+
TableBody,
|
|
36
|
+
TableCell,
|
|
37
|
+
TableHead,
|
|
38
|
+
TableHeader,
|
|
39
|
+
TableRow,
|
|
40
|
+
} from '@/components/ui/table';
|
|
41
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
42
|
+
import { formatDate } from '@/lib/format-date';
|
|
43
|
+
import { cn } from '@/lib/utils';
|
|
44
|
+
import {
|
|
45
|
+
type ColumnDef,
|
|
46
|
+
type SortingState,
|
|
47
|
+
flexRender,
|
|
48
|
+
getCoreRowModel,
|
|
49
|
+
getSortedRowModel,
|
|
50
|
+
useReactTable,
|
|
51
|
+
} from '@tanstack/react-table';
|
|
52
|
+
import {
|
|
53
|
+
ArrowUpDown,
|
|
54
|
+
Building2,
|
|
55
|
+
Globe,
|
|
56
|
+
LayoutGrid,
|
|
57
|
+
List,
|
|
58
|
+
Mail,
|
|
59
|
+
MoreHorizontal,
|
|
60
|
+
Pencil,
|
|
61
|
+
Phone,
|
|
62
|
+
Plus,
|
|
63
|
+
Trash2,
|
|
64
|
+
} from 'lucide-react';
|
|
65
|
+
import { useTranslations } from 'next-intl';
|
|
66
|
+
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
|
67
|
+
import { toast } from 'sonner';
|
|
68
|
+
import { CrmNav } from '../_components/crm-nav';
|
|
69
|
+
import { crmMockAccounts, crmOwners } from '../_lib/crm-mocks';
|
|
70
|
+
import { AccountFormSheet } from './_components/account-form-sheet';
|
|
71
|
+
import type {
|
|
72
|
+
Account,
|
|
73
|
+
AccountFormValues,
|
|
74
|
+
AccountStats,
|
|
75
|
+
} from './_components/account-types';
|
|
76
|
+
|
|
77
|
+
const ACCOUNT_VIEW_STORAGE_KEY = 'contact-account-view-mode';
|
|
78
|
+
type AccountViewMode = 'table' | 'cards';
|
|
79
|
+
|
|
80
|
+
function getAccountInitials(name: string) {
|
|
81
|
+
return name
|
|
82
|
+
.split(' ')
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.slice(0, 2)
|
|
85
|
+
.map((part) => part[0]?.toUpperCase() || '')
|
|
86
|
+
.join('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getLifecycleStageColor(stage?: string | null) {
|
|
90
|
+
switch (stage) {
|
|
91
|
+
case 'customer':
|
|
92
|
+
return 'border-green-500/20 bg-green-500/10 text-green-600';
|
|
93
|
+
case 'prospect':
|
|
94
|
+
return 'border-blue-500/20 bg-blue-500/10 text-blue-600';
|
|
95
|
+
case 'churned':
|
|
96
|
+
return 'border-red-500/20 bg-red-500/10 text-red-600';
|
|
97
|
+
case 'inactive':
|
|
98
|
+
return 'border-gray-500/20 bg-gray-500/10 text-gray-600';
|
|
99
|
+
default:
|
|
100
|
+
return 'border-slate-500/20 bg-slate-500/10 text-slate-600';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function AccountInfoTile({
|
|
105
|
+
icon,
|
|
106
|
+
label,
|
|
107
|
+
value,
|
|
108
|
+
}: {
|
|
109
|
+
icon: ReactNode;
|
|
110
|
+
label: string;
|
|
111
|
+
value: string;
|
|
112
|
+
}) {
|
|
113
|
+
const hasValue = value.trim() !== '-' && value.trim() !== '';
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
className={cn(
|
|
118
|
+
'rounded-xl border border-border/70 bg-muted/25 p-2.5 transition-colors',
|
|
119
|
+
hasValue
|
|
120
|
+
? 'text-foreground'
|
|
121
|
+
: 'border-dashed bg-muted/40 text-muted-foreground'
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<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
|
+
{icon}
|
|
131
|
+
</div>
|
|
132
|
+
<span className="truncate text-[10px] font-semibold tracking-[0.14em] text-muted-foreground uppercase">
|
|
133
|
+
{label}
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div
|
|
138
|
+
className={cn(
|
|
139
|
+
'line-clamp-2 text-sm font-medium',
|
|
140
|
+
hasValue ? 'text-foreground' : 'text-muted-foreground'
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{hasValue ? value : '-'}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default function AccountsPage() {
|
|
150
|
+
const t = useTranslations('contact.AccountsPage');
|
|
151
|
+
const crmT = useTranslations('contact.CrmMenu');
|
|
152
|
+
|
|
153
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
154
|
+
const [page, setPage] = useState(1);
|
|
155
|
+
const [pageSize, setPageSize] = useState(12);
|
|
156
|
+
const [searchInput, setSearchInput] = useState('');
|
|
157
|
+
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
158
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
159
|
+
const [lifecycleFilter, setLifecycleFilter] = useState('all');
|
|
160
|
+
const [formSheetOpen, setFormSheetOpen] = useState(false);
|
|
161
|
+
const [accountToEdit, setAccountToEdit] = useState<Account | null>(null);
|
|
162
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
163
|
+
const [accountToDelete, setAccountToDelete] = useState<Account | null>(null);
|
|
164
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
165
|
+
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
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const timeout = setTimeout(() => {
|
|
182
|
+
setDebouncedSearch(searchInput.trim());
|
|
183
|
+
}, 300);
|
|
184
|
+
return () => clearTimeout(timeout);
|
|
185
|
+
}, [searchInput]);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
try {
|
|
189
|
+
const savedViewMode = window.localStorage.getItem(
|
|
190
|
+
ACCOUNT_VIEW_STORAGE_KEY
|
|
191
|
+
);
|
|
192
|
+
if (savedViewMode === 'table' || savedViewMode === 'cards') {
|
|
193
|
+
setViewMode(savedViewMode);
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Ignore storage read failures
|
|
197
|
+
}
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
// Filter and paginate data
|
|
201
|
+
const filteredAccounts = useMemo(() => {
|
|
202
|
+
let result = accounts;
|
|
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
|
+
}
|
|
214
|
+
|
|
215
|
+
if (statusFilter !== 'all') {
|
|
216
|
+
result = result.filter((account) => account.status === statusFilter);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (lifecycleFilter !== 'all') {
|
|
220
|
+
result = result.filter(
|
|
221
|
+
(account) => account.lifecycle_stage === lifecycleFilter
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}, [accounts, debouncedSearch, statusFilter, lifecycleFilter]);
|
|
227
|
+
|
|
228
|
+
const pageCount = Math.max(1, Math.ceil(filteredAccounts.length / pageSize));
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (page > pageCount) {
|
|
232
|
+
setPage(pageCount);
|
|
233
|
+
}
|
|
234
|
+
}, [page, pageCount]);
|
|
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]);
|
|
252
|
+
|
|
253
|
+
const openCreateSheet = () => {
|
|
254
|
+
setAccountToEdit(null);
|
|
255
|
+
setFormSheetOpen(true);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const openEditSheet = (account: Account) => {
|
|
259
|
+
setAccountToEdit(account);
|
|
260
|
+
setFormSheetOpen(true);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleFormSubmit = (data: AccountFormValues) => {
|
|
264
|
+
if (accountToEdit) {
|
|
265
|
+
// Edit mode - update existing account
|
|
266
|
+
setAccounts((prev) =>
|
|
267
|
+
prev.map((acc) =>
|
|
268
|
+
acc.id === accountToEdit.id
|
|
269
|
+
? {
|
|
270
|
+
...acc,
|
|
271
|
+
...data,
|
|
272
|
+
}
|
|
273
|
+
: acc
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
toast.success(t('editSuccess'));
|
|
277
|
+
} else {
|
|
278
|
+
// Create mode - add new account
|
|
279
|
+
const newAccount: Account = {
|
|
280
|
+
id: Math.max(...accounts.map((a) => a.id), 0) + 1,
|
|
281
|
+
...data,
|
|
282
|
+
created_at: new Date().toISOString(),
|
|
283
|
+
};
|
|
284
|
+
setAccounts((prev) => [newAccount, ...prev]);
|
|
285
|
+
toast.success(t('createSuccess'));
|
|
286
|
+
}
|
|
287
|
+
setFormSheetOpen(false);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleDelete = () => {
|
|
291
|
+
if (!accountToDelete) return;
|
|
292
|
+
setIsDeleting(true);
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
setAccounts((prev) =>
|
|
295
|
+
prev.filter((acc) => acc.id !== accountToDelete.id)
|
|
296
|
+
);
|
|
297
|
+
toast.success(t('deleteSuccess'));
|
|
298
|
+
setDeleteDialogOpen(false);
|
|
299
|
+
setAccountToDelete(null);
|
|
300
|
+
setIsDeleting(false);
|
|
301
|
+
}, 300);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const handleViewModeChange = (value: string) => {
|
|
305
|
+
if (value !== 'table' && value !== 'cards') {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
setViewMode(value);
|
|
309
|
+
try {
|
|
310
|
+
window.localStorage.setItem(ACCOUNT_VIEW_STORAGE_KEY, value);
|
|
311
|
+
} catch {
|
|
312
|
+
// Ignore storage write failures
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const columns = useMemo<ColumnDef<Account>[]>(
|
|
317
|
+
() => [
|
|
318
|
+
{
|
|
319
|
+
accessorKey: 'name',
|
|
320
|
+
header: ({ column }) => (
|
|
321
|
+
<Button
|
|
322
|
+
variant="ghost"
|
|
323
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
324
|
+
className="-ml-4"
|
|
325
|
+
>
|
|
326
|
+
{t('columnName')}
|
|
327
|
+
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
328
|
+
</Button>
|
|
329
|
+
),
|
|
330
|
+
cell: ({ row }) => {
|
|
331
|
+
const account = row.original;
|
|
332
|
+
return (
|
|
333
|
+
<div className="flex items-center gap-3">
|
|
334
|
+
<Avatar className="h-9 w-9 rounded-lg">
|
|
335
|
+
<AvatarFallback className="bg-slate-100 rounded-lg text-xs font-semibold uppercase text-slate-700">
|
|
336
|
+
{getAccountInitials(account.name)}
|
|
337
|
+
</AvatarFallback>
|
|
338
|
+
</Avatar>
|
|
339
|
+
<div className="min-w-0">
|
|
340
|
+
<div className="truncate font-medium">{account.name}</div>
|
|
341
|
+
{account.trade_name && (
|
|
342
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
343
|
+
{account.trade_name}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
accessorKey: 'industry',
|
|
353
|
+
header: t('columnIndustry'),
|
|
354
|
+
cell: ({ row }) => (
|
|
355
|
+
<span className="text-sm text-muted-foreground">
|
|
356
|
+
{row.getValue('industry') || '-'}
|
|
357
|
+
</span>
|
|
358
|
+
),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
accessorKey: 'lifecycle_stage',
|
|
362
|
+
header: t('columnStage'),
|
|
363
|
+
cell: ({ row }) => {
|
|
364
|
+
const stage = row.getValue('lifecycle_stage') as string;
|
|
365
|
+
return (
|
|
366
|
+
<Badge
|
|
367
|
+
variant="outline"
|
|
368
|
+
className={cn(
|
|
369
|
+
'border px-2.5 py-1 text-xs font-medium',
|
|
370
|
+
getLifecycleStageColor(stage)
|
|
371
|
+
)}
|
|
372
|
+
>
|
|
373
|
+
{t(`stage_${stage}` as any) || stage}
|
|
374
|
+
</Badge>
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
accessorKey: 'status',
|
|
380
|
+
header: t('columnStatus'),
|
|
381
|
+
cell: ({ row }) => {
|
|
382
|
+
const status = row.getValue('status') as Account['status'];
|
|
383
|
+
return (
|
|
384
|
+
<Badge
|
|
385
|
+
variant="outline"
|
|
386
|
+
className={cn(
|
|
387
|
+
'border px-2.5 py-1 text-xs font-medium',
|
|
388
|
+
status === 'active'
|
|
389
|
+
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
390
|
+
: 'border-red-500/20 bg-red-500/10 text-red-600'
|
|
391
|
+
)}
|
|
392
|
+
>
|
|
393
|
+
{t(`status_${status}` as any) || status}
|
|
394
|
+
</Badge>
|
|
395
|
+
);
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
accessorKey: 'owner_user',
|
|
400
|
+
header: t('columnOwner'),
|
|
401
|
+
cell: ({ row }) => {
|
|
402
|
+
const account = row.original;
|
|
403
|
+
return (
|
|
404
|
+
<span className="text-sm">
|
|
405
|
+
{account.owner_user?.name || t('unassigned')}
|
|
406
|
+
</span>
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
accessorKey: 'city',
|
|
412
|
+
header: t('columnCity'),
|
|
413
|
+
cell: ({ row }) => (
|
|
414
|
+
<span className="text-sm text-muted-foreground">
|
|
415
|
+
{row.getValue('city') || '-'}
|
|
416
|
+
</span>
|
|
417
|
+
),
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
accessorKey: 'created_at',
|
|
421
|
+
header: ({ column }) => (
|
|
422
|
+
<Button
|
|
423
|
+
variant="ghost"
|
|
424
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
425
|
+
className="-ml-4"
|
|
426
|
+
>
|
|
427
|
+
{t('columnCreatedAt')}
|
|
428
|
+
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
429
|
+
</Button>
|
|
430
|
+
),
|
|
431
|
+
cell: ({ row }) => (
|
|
432
|
+
<span className="text-sm text-muted-foreground">
|
|
433
|
+
{formatDate(row.getValue('created_at') as string, () => {}, 'en')}
|
|
434
|
+
</span>
|
|
435
|
+
),
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: 'actions',
|
|
439
|
+
cell: ({ row }) => {
|
|
440
|
+
const account = row.original;
|
|
441
|
+
return (
|
|
442
|
+
<DropdownMenu>
|
|
443
|
+
<DropdownMenuTrigger asChild>
|
|
444
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
445
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
446
|
+
</Button>
|
|
447
|
+
</DropdownMenuTrigger>
|
|
448
|
+
<DropdownMenuContent align="end">
|
|
449
|
+
<DropdownMenuItem onClick={() => openEditSheet(account)}>
|
|
450
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
451
|
+
{t('edit')}
|
|
452
|
+
</DropdownMenuItem>
|
|
453
|
+
<DropdownMenuSeparator />
|
|
454
|
+
<DropdownMenuItem
|
|
455
|
+
className="text-red-600"
|
|
456
|
+
onClick={() => {
|
|
457
|
+
setAccountToDelete(account);
|
|
458
|
+
setDeleteDialogOpen(true);
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
462
|
+
{t('delete')}
|
|
463
|
+
</DropdownMenuItem>
|
|
464
|
+
</DropdownMenuContent>
|
|
465
|
+
</DropdownMenu>
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
[t]
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const table = useReactTable({
|
|
474
|
+
data: paginatedAccounts,
|
|
475
|
+
columns,
|
|
476
|
+
state: { sorting },
|
|
477
|
+
onSortingChange: setSorting,
|
|
478
|
+
getCoreRowModel: getCoreRowModel(),
|
|
479
|
+
getSortedRowModel: getSortedRowModel(),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const accountsRows = table.getRowModel().rows;
|
|
483
|
+
|
|
484
|
+
const statsCards = [
|
|
485
|
+
{
|
|
486
|
+
title: t('statsTotal'),
|
|
487
|
+
value: stats.total,
|
|
488
|
+
icon: <Building2 className="h-5 w-5" />,
|
|
489
|
+
iconBgColor: 'bg-slate-100',
|
|
490
|
+
iconColor: 'text-slate-700',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
title: t('statsActive'),
|
|
494
|
+
value: stats.active,
|
|
495
|
+
icon: <Building2 className="h-5 w-5" />,
|
|
496
|
+
iconBgColor: 'bg-green-50',
|
|
497
|
+
iconColor: 'text-green-600',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
title: t('statsCustomers'),
|
|
501
|
+
value: stats.customers,
|
|
502
|
+
icon: <Building2 className="h-5 w-5" />,
|
|
503
|
+
iconBgColor: 'bg-blue-50',
|
|
504
|
+
iconColor: 'text-blue-600',
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
title: t('statsProspects'),
|
|
508
|
+
value: stats.prospects,
|
|
509
|
+
icon: <Building2 className="h-5 w-5" />,
|
|
510
|
+
iconBgColor: 'bg-amber-50',
|
|
511
|
+
iconColor: 'text-amber-600',
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const searchControls: SearchBarControl[] = [
|
|
516
|
+
{
|
|
517
|
+
id: 'lifecycle-filter',
|
|
518
|
+
type: 'select' as const,
|
|
519
|
+
value: lifecycleFilter,
|
|
520
|
+
onChange: (value: string) => {
|
|
521
|
+
setLifecycleFilter(value);
|
|
522
|
+
setPage(1);
|
|
523
|
+
},
|
|
524
|
+
placeholder: t('filterByStage'),
|
|
525
|
+
options: [
|
|
526
|
+
{ value: 'all', label: t('allStages') },
|
|
527
|
+
{ value: 'prospect', label: t('stage_prospect') },
|
|
528
|
+
{ value: 'customer', label: t('stage_customer') },
|
|
529
|
+
{ value: 'churned', label: t('stage_churned') },
|
|
530
|
+
{ value: 'inactive', label: t('stage_inactive') },
|
|
531
|
+
],
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: 'status-filter',
|
|
535
|
+
type: 'select',
|
|
536
|
+
value: statusFilter,
|
|
537
|
+
onChange: (value: string) => {
|
|
538
|
+
setStatusFilter(value);
|
|
539
|
+
setPage(1);
|
|
540
|
+
},
|
|
541
|
+
placeholder: t('filterByStatus'),
|
|
542
|
+
options: [
|
|
543
|
+
{ value: 'all', label: t('allStatuses') },
|
|
544
|
+
{ value: 'active', label: t('status_active') },
|
|
545
|
+
{ value: 'inactive', label: t('status_inactive') },
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<Page>
|
|
552
|
+
<PageHeader
|
|
553
|
+
breadcrumbs={[
|
|
554
|
+
{ label: 'Home', href: '/' },
|
|
555
|
+
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
556
|
+
{ label: t('title') },
|
|
557
|
+
]}
|
|
558
|
+
title={t('title')}
|
|
559
|
+
description={t('description')}
|
|
560
|
+
actions={[
|
|
561
|
+
{
|
|
562
|
+
label: t('newAccount'),
|
|
563
|
+
onClick: openCreateSheet,
|
|
564
|
+
icon: <Plus className="h-4 w-4" />,
|
|
565
|
+
},
|
|
566
|
+
]}
|
|
567
|
+
/>
|
|
568
|
+
|
|
569
|
+
<StatsCards stats={statsCards} />
|
|
570
|
+
|
|
571
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-center">
|
|
572
|
+
<div className="flex-1">
|
|
573
|
+
<SearchBar
|
|
574
|
+
searchQuery={searchInput}
|
|
575
|
+
onSearchChange={(value) => {
|
|
576
|
+
setSearchInput(value);
|
|
577
|
+
setPage(1);
|
|
578
|
+
}}
|
|
579
|
+
onSearch={() => {
|
|
580
|
+
setPage(1);
|
|
581
|
+
}}
|
|
582
|
+
placeholder={t('searchPlaceholder')}
|
|
583
|
+
controls={searchControls}
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap xl:justify-end">
|
|
588
|
+
<div className="flex items-center justify-between gap-3 sm:justify-start">
|
|
589
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
590
|
+
{t('viewMode')}
|
|
591
|
+
</span>
|
|
592
|
+
<ToggleGroup
|
|
593
|
+
type="single"
|
|
594
|
+
value={viewMode}
|
|
595
|
+
onValueChange={handleViewModeChange}
|
|
596
|
+
variant="outline"
|
|
597
|
+
size="sm"
|
|
598
|
+
aria-label={t('viewMode')}
|
|
599
|
+
>
|
|
600
|
+
<ToggleGroupItem
|
|
601
|
+
value="table"
|
|
602
|
+
className="gap-1.5 px-2.5"
|
|
603
|
+
aria-label={t('viewModeTable')}
|
|
604
|
+
>
|
|
605
|
+
<List className="h-4 w-4" />
|
|
606
|
+
<span className="hidden sm:inline">{t('viewModeTable')}</span>
|
|
607
|
+
</ToggleGroupItem>
|
|
608
|
+
<ToggleGroupItem
|
|
609
|
+
value="cards"
|
|
610
|
+
className="gap-1.5 px-2.5"
|
|
611
|
+
aria-label={t('viewModeCards')}
|
|
612
|
+
>
|
|
613
|
+
<LayoutGrid className="h-4 w-4" />
|
|
614
|
+
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
615
|
+
</ToggleGroupItem>
|
|
616
|
+
</ToggleGroup>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
{isLoading ? (
|
|
622
|
+
viewMode === 'cards' ? (
|
|
623
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
624
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
625
|
+
<Card key={index} className="overflow-hidden py-0">
|
|
626
|
+
<CardContent className="space-y-3 p-4">
|
|
627
|
+
<div className="flex items-center gap-2.5">
|
|
628
|
+
<Skeleton className="h-10 w-10 rounded-xl" />
|
|
629
|
+
<div className="flex-1 space-y-2">
|
|
630
|
+
<Skeleton className="h-3 w-20" />
|
|
631
|
+
<Skeleton className="h-4 w-2/3" />
|
|
632
|
+
<Skeleton className="h-3 w-1/2" />
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
636
|
+
{Array.from({ length: 4 }).map((__, detailIndex) => (
|
|
637
|
+
<Skeleton key={detailIndex} className="h-20 rounded-xl" />
|
|
638
|
+
))}
|
|
639
|
+
</div>
|
|
640
|
+
<Skeleton className="h-10 rounded-xl" />
|
|
641
|
+
</CardContent>
|
|
642
|
+
</Card>
|
|
643
|
+
))}
|
|
644
|
+
</div>
|
|
645
|
+
) : (
|
|
646
|
+
<div className="space-y-3 p-4">
|
|
647
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
648
|
+
<Skeleton key={index} className="h-14 w-full" />
|
|
649
|
+
))}
|
|
650
|
+
</div>
|
|
651
|
+
)
|
|
652
|
+
) : paginatedAccounts.length === 0 ? (
|
|
653
|
+
<EmptyState
|
|
654
|
+
icon={<Building2 className="h-12 w-12" />}
|
|
655
|
+
title={t('emptyStateTitle')}
|
|
656
|
+
description={t('emptyStateDescription')}
|
|
657
|
+
actionLabel={t('newAccount')}
|
|
658
|
+
actionIcon={<Plus className="mr-2 h-4 w-4" />}
|
|
659
|
+
onAction={openCreateSheet}
|
|
660
|
+
/>
|
|
661
|
+
) : (
|
|
662
|
+
<>
|
|
663
|
+
{viewMode === 'table' ? (
|
|
664
|
+
<div className="overflow-x-auto">
|
|
665
|
+
<Table>
|
|
666
|
+
<TableHeader>
|
|
667
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
668
|
+
<TableRow key={headerGroup.id}>
|
|
669
|
+
{headerGroup.headers.map((header) => (
|
|
670
|
+
<TableHead key={header.id}>
|
|
671
|
+
{header.isPlaceholder
|
|
672
|
+
? null
|
|
673
|
+
: flexRender(
|
|
674
|
+
header.column.columnDef.header,
|
|
675
|
+
header.getContext()
|
|
676
|
+
)}
|
|
677
|
+
</TableHead>
|
|
678
|
+
))}
|
|
679
|
+
</TableRow>
|
|
680
|
+
))}
|
|
681
|
+
</TableHeader>
|
|
682
|
+
<TableBody>
|
|
683
|
+
{accountsRows.map((row) => (
|
|
684
|
+
<TableRow
|
|
685
|
+
key={row.id}
|
|
686
|
+
className="cursor-pointer"
|
|
687
|
+
onDoubleClick={() => openEditSheet(row.original)}
|
|
688
|
+
>
|
|
689
|
+
{row.getVisibleCells().map((cell) => (
|
|
690
|
+
<TableCell key={cell.id}>
|
|
691
|
+
{flexRender(
|
|
692
|
+
cell.column.columnDef.cell,
|
|
693
|
+
cell.getContext()
|
|
694
|
+
)}
|
|
695
|
+
</TableCell>
|
|
696
|
+
))}
|
|
697
|
+
</TableRow>
|
|
698
|
+
))}
|
|
699
|
+
</TableBody>
|
|
700
|
+
</Table>
|
|
701
|
+
</div>
|
|
702
|
+
) : (
|
|
703
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
|
704
|
+
{accountsRows.map((row) => {
|
|
705
|
+
const account = row.original;
|
|
706
|
+
|
|
707
|
+
return (
|
|
708
|
+
<Card
|
|
709
|
+
key={account.id}
|
|
710
|
+
className={cn(
|
|
711
|
+
'group h-full overflow-hidden border-border/70 py-0 transition-colors hover:border-border hover:shadow-md'
|
|
712
|
+
)}
|
|
713
|
+
onDoubleClick={() => openEditSheet(account)}
|
|
714
|
+
>
|
|
715
|
+
<CardContent className="flex h-full flex-col gap-3 p-4">
|
|
716
|
+
<div className="flex items-start justify-between gap-3">
|
|
717
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
718
|
+
<Avatar className="h-10 w-10 shrink-0 rounded-lg border border-slate-500/20">
|
|
719
|
+
<AvatarFallback className="rounded-lg bg-slate-500/8 text-sm font-semibold uppercase text-slate-700 dark:text-slate-200">
|
|
720
|
+
{getAccountInitials(account.name)}
|
|
721
|
+
</AvatarFallback>
|
|
722
|
+
</Avatar>
|
|
723
|
+
|
|
724
|
+
<div className="min-w-0 space-y-1.5">
|
|
725
|
+
<div className="flex flex-wrap gap-1.5">
|
|
726
|
+
<Badge
|
|
727
|
+
variant="outline"
|
|
728
|
+
className={cn(
|
|
729
|
+
'border px-2 py-0.5 text-[11px] font-medium',
|
|
730
|
+
getLifecycleStageColor(
|
|
731
|
+
account.lifecycle_stage
|
|
732
|
+
)
|
|
733
|
+
)}
|
|
734
|
+
>
|
|
735
|
+
{t(`stage_${account.lifecycle_stage}` as any) ||
|
|
736
|
+
account.lifecycle_stage}
|
|
737
|
+
</Badge>
|
|
738
|
+
|
|
739
|
+
<Badge
|
|
740
|
+
variant="outline"
|
|
741
|
+
className={cn(
|
|
742
|
+
'border px-2 py-0.5 text-[11px] font-medium',
|
|
743
|
+
account.status === 'active'
|
|
744
|
+
? 'border-green-500/20 bg-green-500/10 text-green-600'
|
|
745
|
+
: 'border-red-500/20 bg-red-500/10 text-red-600'
|
|
746
|
+
)}
|
|
747
|
+
>
|
|
748
|
+
{t(`status_${account.status}` as any) ||
|
|
749
|
+
account.status}
|
|
750
|
+
</Badge>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div className="min-w-0">
|
|
754
|
+
<h3 className="line-clamp-2 text-sm font-semibold text-foreground">
|
|
755
|
+
{account.name}
|
|
756
|
+
</h3>
|
|
757
|
+
{account.trade_name && (
|
|
758
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
759
|
+
{account.trade_name}
|
|
760
|
+
</p>
|
|
761
|
+
)}
|
|
762
|
+
{account.industry && (
|
|
763
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
764
|
+
{account.industry}
|
|
765
|
+
</p>
|
|
766
|
+
)}
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<DropdownMenu>
|
|
772
|
+
<DropdownMenuTrigger asChild>
|
|
773
|
+
<Button
|
|
774
|
+
variant="ghost"
|
|
775
|
+
size="icon"
|
|
776
|
+
className="h-8 w-8 rounded-lg"
|
|
777
|
+
>
|
|
778
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
779
|
+
</Button>
|
|
780
|
+
</DropdownMenuTrigger>
|
|
781
|
+
<DropdownMenuContent align="end">
|
|
782
|
+
<DropdownMenuItem
|
|
783
|
+
onClick={() => openEditSheet(account)}
|
|
784
|
+
>
|
|
785
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
786
|
+
{t('edit')}
|
|
787
|
+
</DropdownMenuItem>
|
|
788
|
+
<DropdownMenuSeparator />
|
|
789
|
+
<DropdownMenuItem
|
|
790
|
+
className="text-red-600"
|
|
791
|
+
onClick={() => {
|
|
792
|
+
setAccountToDelete(account);
|
|
793
|
+
setDeleteDialogOpen(true);
|
|
794
|
+
}}
|
|
795
|
+
>
|
|
796
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
797
|
+
{t('delete')}
|
|
798
|
+
</DropdownMenuItem>
|
|
799
|
+
</DropdownMenuContent>
|
|
800
|
+
</DropdownMenu>
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
804
|
+
<AccountInfoTile
|
|
805
|
+
icon={<Mail className="h-3 w-3" />}
|
|
806
|
+
label={t('tileEmail')}
|
|
807
|
+
value={account.email || '-'}
|
|
808
|
+
/>
|
|
809
|
+
<AccountInfoTile
|
|
810
|
+
icon={<Phone className="h-3 w-3" />}
|
|
811
|
+
label={t('tilePhone')}
|
|
812
|
+
value={account.phone || '-'}
|
|
813
|
+
/>
|
|
814
|
+
<AccountInfoTile
|
|
815
|
+
icon={<Globe className="h-3 w-3" />}
|
|
816
|
+
label={t('tileWebsite')}
|
|
817
|
+
value={account.website || '-'}
|
|
818
|
+
/>
|
|
819
|
+
<AccountInfoTile
|
|
820
|
+
icon={<Building2 className="h-3 w-3" />}
|
|
821
|
+
label={t('tileIndustry')}
|
|
822
|
+
value={account.industry || '-'}
|
|
823
|
+
/>
|
|
824
|
+
</div>
|
|
825
|
+
|
|
826
|
+
<div className="border-t pt-2 text-xs text-muted-foreground">
|
|
827
|
+
<div className="flex items-center justify-between">
|
|
828
|
+
<span>
|
|
829
|
+
{t('owner')}:{' '}
|
|
830
|
+
{account.owner_user?.name || t('unassigned')}
|
|
831
|
+
</span>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
</CardContent>
|
|
835
|
+
</Card>
|
|
836
|
+
);
|
|
837
|
+
})}
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
840
|
+
|
|
841
|
+
<PaginationFooter
|
|
842
|
+
table={table}
|
|
843
|
+
pageSize={pageSize}
|
|
844
|
+
setPageSize={setPageSize}
|
|
845
|
+
currentPage={page}
|
|
846
|
+
totalPages={pageCount}
|
|
847
|
+
onPageChange={setPage}
|
|
848
|
+
/>
|
|
849
|
+
</>
|
|
850
|
+
)}
|
|
851
|
+
|
|
852
|
+
<AccountFormSheet
|
|
853
|
+
open={formSheetOpen}
|
|
854
|
+
onOpenChange={setFormSheetOpen}
|
|
855
|
+
account={accountToEdit}
|
|
856
|
+
owners={crmOwners}
|
|
857
|
+
onSubmit={handleFormSubmit}
|
|
858
|
+
/>
|
|
859
|
+
|
|
860
|
+
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
861
|
+
<AlertDialogContent>
|
|
862
|
+
<AlertDialogHeader>
|
|
863
|
+
<AlertDialogTitle>{t('deleteTitle')}</AlertDialogTitle>
|
|
864
|
+
<AlertDialogDescription>
|
|
865
|
+
{t('deleteDescription', {
|
|
866
|
+
name: accountToDelete?.name || '',
|
|
867
|
+
})}
|
|
868
|
+
</AlertDialogDescription>
|
|
869
|
+
</AlertDialogHeader>
|
|
870
|
+
<div className="flex justify-end gap-3">
|
|
871
|
+
<AlertDialogCancel disabled={isDeleting}>
|
|
872
|
+
{t('cancel')}
|
|
873
|
+
</AlertDialogCancel>
|
|
874
|
+
<AlertDialogAction
|
|
875
|
+
onClick={handleDelete}
|
|
876
|
+
disabled={isDeleting}
|
|
877
|
+
className="bg-red-600 hover:bg-red-700"
|
|
878
|
+
>
|
|
879
|
+
{isDeleting ? t('deleting') : t('delete')}
|
|
880
|
+
</AlertDialogAction>
|
|
881
|
+
</div>
|
|
882
|
+
</AlertDialogContent>
|
|
883
|
+
</AlertDialog>
|
|
884
|
+
</Page>
|
|
885
|
+
);
|
|
886
|
+
}
|