@hed-hog/core 0.0.304 → 0.0.305
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 +84 -36
- package/dist/auth/auth.controller.d.ts +0 -3
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +4 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +6 -3
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
- package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
- package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.controller.js +5 -3
- package/dist/mail-sent/mail-sent.controller.js.map +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts +28 -3
- package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.js +108 -3
- package/dist/mail-sent/mail-sent.service.js.map +1 -1
- package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
- package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
- package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
- package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
- package/dist/oauth/oauth.controller.d.ts +4 -2
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +39 -11
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.errors.d.ts +16 -0
- package/dist/oauth/oauth.errors.d.ts.map +1 -0
- package/dist/oauth/oauth.errors.js +30 -0
- package/dist/oauth/oauth.errors.js.map +1 -0
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +10 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +9 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +97 -24
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/abstract.provider.d.ts +6 -0
- package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
- package/dist/oauth/providers/abstract.provider.js +34 -0
- package/dist/oauth/providers/abstract.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/security/security.service.d.ts +1 -0
- package/dist/security/security.service.d.ts.map +1 -1
- package/dist/security/security.service.js +9 -10
- package/dist/security/security.service.js.map +1 -1
- package/dist/session/session.service.d.ts +5 -1
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +76 -10
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.service.js +4 -4
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +23 -12
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.service.d.ts +1 -1
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +3 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard.yaml +2 -2
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
- package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
- package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
- package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
- package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
- package/hedhog/frontend/messages/en.json +45 -6
- package/hedhog/frontend/messages/pt.json +45 -6
- package/package.json +5 -5
- package/src/auth/auth.controller.ts +15 -19
- package/src/auth/auth.service.ts +13 -8
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
- package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
- package/src/mail-sent/mail-sent.controller.ts +14 -2
- package/src/mail-sent/mail-sent.service.ts +159 -4
- package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
- package/src/oauth/oauth.controller.ts +58 -1
- package/src/oauth/oauth.errors.ts +32 -0
- package/src/oauth/oauth.module.ts +10 -1
- package/src/oauth/oauth.service.ts +178 -28
- package/src/oauth/providers/abstract.provider.ts +64 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
- package/src/security/security.service.ts +12 -10
- package/src/session/session.service.ts +84 -9
- package/src/setting/setting.service.ts +10 -10
- package/src/token/token.service.ts +29 -11
- package/src/user/user.service.ts +10 -6
|
@@ -6,24 +6,47 @@ import {
|
|
|
6
6
|
PageHeader,
|
|
7
7
|
PaginationFooter,
|
|
8
8
|
SearchBar,
|
|
9
|
+
type SearchBarControl,
|
|
9
10
|
} from '@/components/entity-list';
|
|
10
11
|
import { Badge } from '@/components/ui/badge';
|
|
11
12
|
import { Button } from '@/components/ui/button';
|
|
12
13
|
import { Card, CardContent } from '@/components/ui/card';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
DialogContent,
|
|
16
|
-
DialogDescription,
|
|
17
|
-
DialogHeader,
|
|
18
|
-
DialogTitle,
|
|
19
|
-
} from '@/components/ui/dialog';
|
|
14
|
+
import { Input } from '@/components/ui/input';
|
|
15
|
+
import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
|
|
20
16
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
17
|
+
import {
|
|
18
|
+
Sheet,
|
|
19
|
+
SheetContent,
|
|
20
|
+
SheetDescription,
|
|
21
|
+
SheetHeader,
|
|
22
|
+
SheetTitle,
|
|
23
|
+
} from '@/components/ui/sheet';
|
|
24
|
+
import {
|
|
25
|
+
Table,
|
|
26
|
+
TableBody,
|
|
27
|
+
TableCell,
|
|
28
|
+
TableHead,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
|
+
} from '@/components/ui/table';
|
|
32
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
21
33
|
import { useDebounce } from '@/hooks/use-debounce';
|
|
22
34
|
import { formatDate } from '@/lib/format-date';
|
|
23
35
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
24
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
AlertCircle,
|
|
38
|
+
CheckCircle2,
|
|
39
|
+
Clock,
|
|
40
|
+
Eye,
|
|
41
|
+
Inbox,
|
|
42
|
+
LayoutGrid,
|
|
43
|
+
List,
|
|
44
|
+
Mail,
|
|
45
|
+
TriangleAlert,
|
|
46
|
+
User,
|
|
47
|
+
} from 'lucide-react';
|
|
25
48
|
import { useTranslations } from 'next-intl';
|
|
26
|
-
import { useState } from 'react';
|
|
49
|
+
import { useMemo, useState } from 'react';
|
|
27
50
|
|
|
28
51
|
type PaginationResult<T> = {
|
|
29
52
|
data: T[];
|
|
@@ -43,27 +66,79 @@ type MailSent = {
|
|
|
43
66
|
body: string;
|
|
44
67
|
created_at: string;
|
|
45
68
|
updated_at: string;
|
|
69
|
+
mail_sent_user?: MailSentRecipientLog[];
|
|
70
|
+
deliverySummary?: MailDeliverySummary;
|
|
46
71
|
};
|
|
47
72
|
|
|
73
|
+
type MailSentRecipientLog = {
|
|
74
|
+
id: number;
|
|
75
|
+
recipient_email: string;
|
|
76
|
+
status: 'received' | 'read' | 'error';
|
|
77
|
+
read_at?: string | null;
|
|
78
|
+
error_code?: string | null;
|
|
79
|
+
error_message?: string | null;
|
|
80
|
+
user_id: number;
|
|
81
|
+
user_identifier_id?: number | null;
|
|
82
|
+
created_at: string;
|
|
83
|
+
updated_at: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type MailDeliverySummary = {
|
|
87
|
+
totalRecipients: number;
|
|
88
|
+
receivedCount: number;
|
|
89
|
+
readCount: number;
|
|
90
|
+
errorCount: number;
|
|
91
|
+
hasError: boolean;
|
|
92
|
+
lastReadAt?: string | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type DeliveryStatusFilter = 'all' | 'received' | 'read' | 'error';
|
|
96
|
+
type HasErrorFilter = 'all' | 'true' | 'false';
|
|
97
|
+
type ViewMode = 'cards' | 'table';
|
|
98
|
+
|
|
48
99
|
export default function MailLogPage() {
|
|
49
100
|
const t = useTranslations('core.MailLog');
|
|
50
101
|
const [selectedLog, setSelectedLog] = useState<MailSent | null>(null);
|
|
51
|
-
const [
|
|
102
|
+
const [isDetailSheetOpen, setIsDetailSheetOpen] = useState(false);
|
|
52
103
|
const [searchTerm, setSearchTerm] = useState('');
|
|
104
|
+
const [recipientEmail, setRecipientEmail] = useState('');
|
|
105
|
+
const [statusFilter, setStatusFilter] = useState<DeliveryStatusFilter>('all');
|
|
106
|
+
const [hasErrorFilter, setHasErrorFilter] = useState<HasErrorFilter>('all');
|
|
107
|
+
const [createdAtFrom, setCreatedAtFrom] = useState('');
|
|
108
|
+
const [createdAtTo, setCreatedAtTo] = useState('');
|
|
109
|
+
const [viewMode, setViewMode] = useState<ViewMode>('cards');
|
|
53
110
|
const debouncedSearch = useDebounce(searchTerm);
|
|
111
|
+
const debouncedRecipientEmail = useDebounce(recipientEmail);
|
|
54
112
|
const [page, setPage] = useState(1);
|
|
55
113
|
const [pageSize, setPageSize] = useState(10);
|
|
56
114
|
const { request, getSettingValue } = useApp();
|
|
57
115
|
|
|
58
|
-
const {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
const {
|
|
117
|
+
data: logsResult,
|
|
118
|
+
refetch: refetchLogs,
|
|
119
|
+
isLoading,
|
|
120
|
+
} = useQuery<PaginationResult<MailSent>>({
|
|
121
|
+
queryKey: [
|
|
122
|
+
'mail-sent',
|
|
123
|
+
debouncedSearch,
|
|
124
|
+
debouncedRecipientEmail,
|
|
125
|
+
statusFilter,
|
|
126
|
+
hasErrorFilter,
|
|
127
|
+
createdAtFrom,
|
|
128
|
+
createdAtTo,
|
|
129
|
+
page,
|
|
130
|
+
pageSize,
|
|
131
|
+
],
|
|
62
132
|
queryFn: async () => {
|
|
63
133
|
const response = await request({
|
|
64
134
|
url: '/mail-sent',
|
|
65
135
|
params: {
|
|
66
136
|
search: debouncedSearch,
|
|
137
|
+
recipientEmail: debouncedRecipientEmail || undefined,
|
|
138
|
+
status: statusFilter !== 'all' ? statusFilter : undefined,
|
|
139
|
+
hasError: hasErrorFilter !== 'all' ? hasErrorFilter : undefined,
|
|
140
|
+
createdAtFrom: createdAtFrom || undefined,
|
|
141
|
+
createdAtTo: createdAtTo || undefined,
|
|
67
142
|
page,
|
|
68
143
|
pageSize,
|
|
69
144
|
},
|
|
@@ -75,7 +150,7 @@ export default function MailLogPage() {
|
|
|
75
150
|
|
|
76
151
|
const handleViewDetails = (log: MailSent): void => {
|
|
77
152
|
setSelectedLog(log);
|
|
78
|
-
|
|
153
|
+
setIsDetailSheetOpen(true);
|
|
79
154
|
};
|
|
80
155
|
|
|
81
156
|
const handleSearchChange = (value: string): void => {
|
|
@@ -83,6 +158,147 @@ export default function MailLogPage() {
|
|
|
83
158
|
setPage(1);
|
|
84
159
|
};
|
|
85
160
|
|
|
161
|
+
const handleRecipientEmailChange = (value: string): void => {
|
|
162
|
+
setRecipientEmail(value);
|
|
163
|
+
setPage(1);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const hasActiveFilters =
|
|
167
|
+
searchTerm.length > 0 ||
|
|
168
|
+
recipientEmail.length > 0 ||
|
|
169
|
+
statusFilter !== 'all' ||
|
|
170
|
+
hasErrorFilter !== 'all' ||
|
|
171
|
+
createdAtFrom.length > 0 ||
|
|
172
|
+
createdAtTo.length > 0;
|
|
173
|
+
|
|
174
|
+
const clearFilters = (): void => {
|
|
175
|
+
setSearchTerm('');
|
|
176
|
+
setRecipientEmail('');
|
|
177
|
+
setStatusFilter('all');
|
|
178
|
+
setHasErrorFilter('all');
|
|
179
|
+
setCreatedAtFrom('');
|
|
180
|
+
setCreatedAtTo('');
|
|
181
|
+
setPage(1);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const searchControls = useMemo<SearchBarControl[]>(
|
|
185
|
+
() => [
|
|
186
|
+
{
|
|
187
|
+
id: 'status',
|
|
188
|
+
type: 'select',
|
|
189
|
+
value: statusFilter,
|
|
190
|
+
onChange: (value) => {
|
|
191
|
+
setStatusFilter(value as DeliveryStatusFilter);
|
|
192
|
+
setPage(1);
|
|
193
|
+
},
|
|
194
|
+
placeholder: t('filterByStatus'),
|
|
195
|
+
options: [
|
|
196
|
+
{ value: 'all', label: t('statusAll') },
|
|
197
|
+
{ value: 'received', label: t('statusReceived') },
|
|
198
|
+
{ value: 'read', label: t('statusRead') },
|
|
199
|
+
{ value: 'error', label: t('statusError') },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'has-error',
|
|
204
|
+
type: 'select',
|
|
205
|
+
value: hasErrorFilter,
|
|
206
|
+
onChange: (value) => {
|
|
207
|
+
setHasErrorFilter(value as HasErrorFilter);
|
|
208
|
+
setPage(1);
|
|
209
|
+
},
|
|
210
|
+
placeholder: t('filterByError'),
|
|
211
|
+
options: [
|
|
212
|
+
{ value: 'all', label: t('errorAll') },
|
|
213
|
+
{ value: 'true', label: t('errorOnly') },
|
|
214
|
+
{ value: 'false', label: t('errorNone') },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'created-at-from',
|
|
219
|
+
type: 'date',
|
|
220
|
+
value: createdAtFrom,
|
|
221
|
+
onChange: (value) => {
|
|
222
|
+
setCreatedAtFrom(value);
|
|
223
|
+
setPage(1);
|
|
224
|
+
},
|
|
225
|
+
max: createdAtTo || undefined,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'created-at-to',
|
|
229
|
+
type: 'date',
|
|
230
|
+
value: createdAtTo,
|
|
231
|
+
onChange: (value) => {
|
|
232
|
+
setCreatedAtTo(value);
|
|
233
|
+
setPage(1);
|
|
234
|
+
},
|
|
235
|
+
min: createdAtFrom || undefined,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
[createdAtFrom, createdAtTo, hasErrorFilter, statusFilter, t]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const summary = useMemo(() => {
|
|
242
|
+
return logs.reduce(
|
|
243
|
+
(acc, log) => {
|
|
244
|
+
const deliverySummary = log.deliverySummary;
|
|
245
|
+
acc.totalRecipients += deliverySummary?.totalRecipients ?? 0;
|
|
246
|
+
acc.readCount += deliverySummary?.readCount ?? 0;
|
|
247
|
+
acc.errorCount += deliverySummary?.errorCount ?? 0;
|
|
248
|
+
if (deliverySummary?.hasError) {
|
|
249
|
+
acc.emailsWithError += 1;
|
|
250
|
+
}
|
|
251
|
+
return acc;
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
totalRecipients: 0,
|
|
255
|
+
readCount: 0,
|
|
256
|
+
errorCount: 0,
|
|
257
|
+
emailsWithError: 0,
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
}, [logs]);
|
|
261
|
+
|
|
262
|
+
const kpiCards = useMemo<KpiCardItem[]>(
|
|
263
|
+
() => [
|
|
264
|
+
{
|
|
265
|
+
key: 'totalEmails',
|
|
266
|
+
title: t('totalEmails'),
|
|
267
|
+
value: total,
|
|
268
|
+
icon: Mail,
|
|
269
|
+
layout: 'compact',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: 'totalRecipients',
|
|
273
|
+
title: t('totalRecipientsCurrentPage'),
|
|
274
|
+
value: summary.totalRecipients,
|
|
275
|
+
icon: Inbox,
|
|
276
|
+
layout: 'compact',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
key: 'readRecipients',
|
|
280
|
+
title: t('readRecipientsCurrentPage'),
|
|
281
|
+
value: summary.readCount,
|
|
282
|
+
icon: Eye,
|
|
283
|
+
layout: 'compact',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
key: 'emailsWithError',
|
|
287
|
+
title: t('emailsWithErrorCurrentPage'),
|
|
288
|
+
value: summary.emailsWithError,
|
|
289
|
+
icon: TriangleAlert,
|
|
290
|
+
layout: 'compact',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
summary.emailsWithError,
|
|
295
|
+
summary.readCount,
|
|
296
|
+
summary.totalRecipients,
|
|
297
|
+
t,
|
|
298
|
+
total,
|
|
299
|
+
]
|
|
300
|
+
);
|
|
301
|
+
|
|
86
302
|
const truncateText = (text: string, maxLength: number = 100): string => {
|
|
87
303
|
if (!text) return '';
|
|
88
304
|
return text.length > maxLength
|
|
@@ -90,6 +306,28 @@ export default function MailLogPage() {
|
|
|
90
306
|
: text;
|
|
91
307
|
};
|
|
92
308
|
|
|
309
|
+
const renderDeliveryBadges = (deliverySummary?: MailDeliverySummary) => {
|
|
310
|
+
if (!deliverySummary) {
|
|
311
|
+
return <Badge variant="outline">{t('statusUnavailable')}</Badge>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
316
|
+
<Badge variant="outline">
|
|
317
|
+
{t('statusReceivedShort', { count: deliverySummary.receivedCount })}
|
|
318
|
+
</Badge>
|
|
319
|
+
<Badge variant="outline">
|
|
320
|
+
{t('statusReadShort', { count: deliverySummary.readCount })}
|
|
321
|
+
</Badge>
|
|
322
|
+
<Badge
|
|
323
|
+
variant={deliverySummary.errorCount > 0 ? 'destructive' : 'outline'}
|
|
324
|
+
>
|
|
325
|
+
{t('statusErrorShort', { count: deliverySummary.errorCount })}
|
|
326
|
+
</Badge>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
93
331
|
return (
|
|
94
332
|
<Page>
|
|
95
333
|
<PageHeader
|
|
@@ -101,64 +339,154 @@ export default function MailLogPage() {
|
|
|
101
339
|
description={t('description')}
|
|
102
340
|
/>
|
|
103
341
|
|
|
104
|
-
<
|
|
105
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
106
|
-
<CardContent className="p-4">
|
|
107
|
-
<div className="flex items-center space-x-3">
|
|
108
|
-
<div className="rounded-full bg-blue-100 p-2 dark:bg-blue-900">
|
|
109
|
-
<Mail className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
|
110
|
-
</div>
|
|
111
|
-
<div>
|
|
112
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
113
|
-
{t('totalEmails')}
|
|
114
|
-
</p>
|
|
115
|
-
<p className="text-2xl font-bold">{total || 0}</p>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</CardContent>
|
|
119
|
-
</Card>
|
|
120
|
-
|
|
121
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
122
|
-
<CardContent className="p-4">
|
|
123
|
-
<div className="flex items-center space-x-3">
|
|
124
|
-
<div className="rounded-full bg-green-100 p-2 dark:bg-green-900">
|
|
125
|
-
<Clock className="h-6 w-6 text-green-600 dark:text-green-400" />
|
|
126
|
-
</div>
|
|
127
|
-
<div>
|
|
128
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
129
|
-
{t('onThisPage')}
|
|
130
|
-
</p>
|
|
131
|
-
<p className="text-2xl font-bold">{logs.length}</p>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
</CardContent>
|
|
135
|
-
</Card>
|
|
136
|
-
</div>
|
|
342
|
+
<KpiCardsGrid items={kpiCards} className="mb-1" />
|
|
137
343
|
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
344
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-center">
|
|
345
|
+
<div className="flex-1">
|
|
346
|
+
<SearchBar
|
|
347
|
+
searchQuery={searchTerm}
|
|
348
|
+
onSearchChange={handleSearchChange}
|
|
349
|
+
onSearch={() => setPage(1)}
|
|
350
|
+
placeholder={t('searchPlaceholder')}
|
|
351
|
+
controls={searchControls}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap xl:justify-end">
|
|
356
|
+
<Input
|
|
357
|
+
value={recipientEmail}
|
|
358
|
+
onChange={(event) => handleRecipientEmailChange(event.target.value)}
|
|
359
|
+
placeholder={t('recipientPlaceholder')}
|
|
360
|
+
className="sm:w-64"
|
|
361
|
+
/>
|
|
362
|
+
|
|
363
|
+
<Button
|
|
364
|
+
variant="outline"
|
|
365
|
+
onClick={clearFilters}
|
|
366
|
+
disabled={!hasActiveFilters}
|
|
367
|
+
>
|
|
368
|
+
{t('clearFilters')}
|
|
369
|
+
</Button>
|
|
370
|
+
|
|
371
|
+
<div className="flex items-center justify-between gap-3 sm:justify-start">
|
|
372
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
373
|
+
{t('viewMode')}
|
|
374
|
+
</span>
|
|
375
|
+
<ToggleGroup
|
|
376
|
+
type="single"
|
|
377
|
+
value={viewMode}
|
|
378
|
+
onValueChange={(value) => {
|
|
379
|
+
if (value === 'cards' || value === 'table') {
|
|
380
|
+
setViewMode(value);
|
|
381
|
+
}
|
|
382
|
+
}}
|
|
383
|
+
variant="outline"
|
|
384
|
+
size="sm"
|
|
385
|
+
aria-label={t('viewMode')}
|
|
386
|
+
>
|
|
387
|
+
<ToggleGroupItem
|
|
388
|
+
value="table"
|
|
389
|
+
className="gap-1.5 px-2.5"
|
|
390
|
+
aria-label={t('viewModeTable')}
|
|
391
|
+
>
|
|
392
|
+
<List className="h-4 w-4" />
|
|
393
|
+
<span className="hidden sm:inline">{t('viewModeTable')}</span>
|
|
394
|
+
</ToggleGroupItem>
|
|
395
|
+
<ToggleGroupItem
|
|
396
|
+
value="cards"
|
|
397
|
+
className="gap-1.5 px-2.5"
|
|
398
|
+
aria-label={t('viewModeCards')}
|
|
399
|
+
>
|
|
400
|
+
<LayoutGrid className="h-4 w-4" />
|
|
401
|
+
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
402
|
+
</ToggleGroupItem>
|
|
403
|
+
</ToggleGroup>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
144
407
|
|
|
145
408
|
<div className="space-y-3 mb-4">
|
|
146
|
-
{logs.length === 0 ? (
|
|
409
|
+
{!isLoading && logs.length === 0 ? (
|
|
147
410
|
<EmptyState
|
|
148
411
|
icon={<Mail className="h-12 w-12" />}
|
|
149
412
|
title={t('noLogsFound')}
|
|
150
|
-
description={
|
|
151
|
-
|
|
413
|
+
description={
|
|
414
|
+
hasActiveFilters ? t('adjustSearch') : t('noEmailsSent')
|
|
415
|
+
}
|
|
416
|
+
actionLabel={
|
|
417
|
+
hasActiveFilters ? t('clearFilters') : t('refreshList')
|
|
418
|
+
}
|
|
152
419
|
onAction={() => {
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
setPage(1);
|
|
420
|
+
if (hasActiveFilters) {
|
|
421
|
+
clearFilters();
|
|
156
422
|
return;
|
|
157
423
|
}
|
|
158
424
|
|
|
159
425
|
refetchLogs();
|
|
160
426
|
}}
|
|
161
427
|
/>
|
|
428
|
+
) : isLoading ? (
|
|
429
|
+
<Card className="py-0">
|
|
430
|
+
<CardContent className="flex items-center justify-center p-8 text-sm text-muted-foreground">
|
|
431
|
+
{t('loadingLogs')}
|
|
432
|
+
</CardContent>
|
|
433
|
+
</Card>
|
|
434
|
+
) : viewMode === 'table' ? (
|
|
435
|
+
<div className="overflow-x-auto">
|
|
436
|
+
<Table>
|
|
437
|
+
<TableHeader>
|
|
438
|
+
<TableRow>
|
|
439
|
+
<TableHead>#{t('id')}</TableHead>
|
|
440
|
+
<TableHead>{t('subject')}</TableHead>
|
|
441
|
+
<TableHead>{t('from')}</TableHead>
|
|
442
|
+
<TableHead>{t('to')}</TableHead>
|
|
443
|
+
<TableHead>{t('delivery')}</TableHead>
|
|
444
|
+
<TableHead>{t('createdAt')}</TableHead>
|
|
445
|
+
<TableHead className="w-24 text-right">
|
|
446
|
+
{t('actions')}
|
|
447
|
+
</TableHead>
|
|
448
|
+
</TableRow>
|
|
449
|
+
</TableHeader>
|
|
450
|
+
<TableBody>
|
|
451
|
+
{logs.map((log) => (
|
|
452
|
+
<TableRow
|
|
453
|
+
key={log.id}
|
|
454
|
+
className="cursor-pointer"
|
|
455
|
+
onDoubleClick={() => handleViewDetails(log)}
|
|
456
|
+
>
|
|
457
|
+
<TableCell>#{log.id}</TableCell>
|
|
458
|
+
<TableCell className="max-w-72 truncate font-medium">
|
|
459
|
+
{log.subject}
|
|
460
|
+
</TableCell>
|
|
461
|
+
<TableCell className="max-w-52 truncate">
|
|
462
|
+
{log.from}
|
|
463
|
+
</TableCell>
|
|
464
|
+
<TableCell className="max-w-64 truncate">
|
|
465
|
+
{log.to}
|
|
466
|
+
</TableCell>
|
|
467
|
+
<TableCell>
|
|
468
|
+
{renderDeliveryBadges(log.deliverySummary)}
|
|
469
|
+
</TableCell>
|
|
470
|
+
<TableCell>
|
|
471
|
+
{formatDate(log.created_at, getSettingValue)}
|
|
472
|
+
</TableCell>
|
|
473
|
+
<TableCell className="text-right">
|
|
474
|
+
<Button
|
|
475
|
+
variant="outline"
|
|
476
|
+
size="sm"
|
|
477
|
+
onClick={(event) => {
|
|
478
|
+
event.stopPropagation();
|
|
479
|
+
handleViewDetails(log);
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
{t('viewDetails')}
|
|
483
|
+
</Button>
|
|
484
|
+
</TableCell>
|
|
485
|
+
</TableRow>
|
|
486
|
+
))}
|
|
487
|
+
</TableBody>
|
|
488
|
+
</Table>
|
|
489
|
+
</div>
|
|
162
490
|
) : (
|
|
163
491
|
logs.map((log) => (
|
|
164
492
|
<Card
|
|
@@ -172,6 +500,11 @@ export default function MailLogPage() {
|
|
|
172
500
|
<div className="flex items-center gap-2">
|
|
173
501
|
<h3 className="text-lg font-semibold">{log.subject}</h3>
|
|
174
502
|
<Badge variant="outline">#{log.id}</Badge>
|
|
503
|
+
{log.deliverySummary?.hasError ? (
|
|
504
|
+
<Badge variant="destructive">{t('hasError')}</Badge>
|
|
505
|
+
) : (
|
|
506
|
+
<Badge variant="outline">{t('withoutError')}</Badge>
|
|
507
|
+
)}
|
|
175
508
|
</div>
|
|
176
509
|
<div className="flex flex-col gap-1 text-sm text-muted-foreground">
|
|
177
510
|
<div className="flex items-center gap-2">
|
|
@@ -193,6 +526,7 @@ export default function MailLogPage() {
|
|
|
193
526
|
</span>
|
|
194
527
|
</div>
|
|
195
528
|
</div>
|
|
529
|
+
{renderDeliveryBadges(log.deliverySummary)}
|
|
196
530
|
</div>
|
|
197
531
|
<Button
|
|
198
532
|
variant="outline"
|
|
@@ -224,14 +558,14 @@ export default function MailLogPage() {
|
|
|
224
558
|
/>
|
|
225
559
|
|
|
226
560
|
{selectedLog && (
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
<
|
|
232
|
-
</
|
|
233
|
-
<ScrollArea className="
|
|
234
|
-
<div className="space-y-4">
|
|
561
|
+
<Sheet open={isDetailSheetOpen} onOpenChange={setIsDetailSheetOpen}>
|
|
562
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
|
|
563
|
+
<SheetHeader>
|
|
564
|
+
<SheetTitle>{t('emailDetails')}</SheetTitle>
|
|
565
|
+
<SheetDescription>{t('detailsDescription')}</SheetDescription>
|
|
566
|
+
</SheetHeader>
|
|
567
|
+
<ScrollArea className="h-[calc(100vh-9rem)] px-4 pb-4">
|
|
568
|
+
<div className="space-y-4 py-2">
|
|
235
569
|
<div>
|
|
236
570
|
<h4 className="mb-2 text-sm font-semibold">{t('subject')}</h4>
|
|
237
571
|
<p className="rounded-md bg-muted p-3 text-sm">
|
|
@@ -273,6 +607,83 @@ export default function MailLogPage() {
|
|
|
273
607
|
dangerouslySetInnerHTML={{ __html: selectedLog.body }}
|
|
274
608
|
/>
|
|
275
609
|
</div>
|
|
610
|
+
|
|
611
|
+
<div>
|
|
612
|
+
<h4 className="mb-2 text-sm font-semibold">
|
|
613
|
+
{t('delivery')}
|
|
614
|
+
</h4>
|
|
615
|
+
<div className="rounded-md border p-3">
|
|
616
|
+
<div className="mb-3 flex flex-wrap gap-2">
|
|
617
|
+
{renderDeliveryBadges(selectedLog.deliverySummary)}
|
|
618
|
+
<Badge variant="outline">
|
|
619
|
+
{t('totalRecipientsLabel', {
|
|
620
|
+
count:
|
|
621
|
+
selectedLog.deliverySummary?.totalRecipients ?? 0,
|
|
622
|
+
})}
|
|
623
|
+
</Badge>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{selectedLog.mail_sent_user &&
|
|
627
|
+
selectedLog.mail_sent_user.length > 0 ? (
|
|
628
|
+
<div className="space-y-2">
|
|
629
|
+
{selectedLog.mail_sent_user.map((recipient) => (
|
|
630
|
+
<div
|
|
631
|
+
key={recipient.id}
|
|
632
|
+
className="rounded-md border border-border/60 p-3"
|
|
633
|
+
>
|
|
634
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
635
|
+
<div className="text-sm font-medium">
|
|
636
|
+
{recipient.recipient_email}
|
|
637
|
+
</div>
|
|
638
|
+
<Badge
|
|
639
|
+
variant={
|
|
640
|
+
recipient.status === 'error'
|
|
641
|
+
? 'destructive'
|
|
642
|
+
: 'outline'
|
|
643
|
+
}
|
|
644
|
+
>
|
|
645
|
+
{t(`status_${recipient.status}` as never)}
|
|
646
|
+
</Badge>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
|
|
650
|
+
{recipient.read_at ? (
|
|
651
|
+
<div className="flex items-center gap-1.5">
|
|
652
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
653
|
+
<span>
|
|
654
|
+
{t('readAt')}:{' '}
|
|
655
|
+
{formatDate(
|
|
656
|
+
recipient.read_at,
|
|
657
|
+
getSettingValue
|
|
658
|
+
)}
|
|
659
|
+
</span>
|
|
660
|
+
</div>
|
|
661
|
+
) : null}
|
|
662
|
+
|
|
663
|
+
{recipient.error_message ? (
|
|
664
|
+
<div className="flex items-start gap-1.5 text-red-600 dark:text-red-400">
|
|
665
|
+
<AlertCircle className="mt-0.5 h-3.5 w-3.5" />
|
|
666
|
+
<span>
|
|
667
|
+
{t('errorMessage')}:{' '}
|
|
668
|
+
{recipient.error_message}
|
|
669
|
+
{recipient.error_code
|
|
670
|
+
? ` (${recipient.error_code})`
|
|
671
|
+
: ''}
|
|
672
|
+
</span>
|
|
673
|
+
</div>
|
|
674
|
+
) : null}
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
))}
|
|
678
|
+
</div>
|
|
679
|
+
) : (
|
|
680
|
+
<p className="text-sm text-muted-foreground">
|
|
681
|
+
{t('noRecipientStatus')}
|
|
682
|
+
</p>
|
|
683
|
+
)}
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
276
687
|
<div className="grid grid-cols-2 gap-4">
|
|
277
688
|
<div>
|
|
278
689
|
<h4 className="mb-2 text-sm font-semibold">
|
|
@@ -293,8 +704,8 @@ export default function MailLogPage() {
|
|
|
293
704
|
</div>
|
|
294
705
|
</div>
|
|
295
706
|
</ScrollArea>
|
|
296
|
-
</
|
|
297
|
-
</
|
|
707
|
+
</SheetContent>
|
|
708
|
+
</Sheet>
|
|
298
709
|
)}
|
|
299
710
|
</Page>
|
|
300
711
|
);
|