@hed-hog/core 0.0.303 → 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.
Files changed (118) hide show
  1. package/README.md +84 -36
  2. package/dist/auth/auth.controller.d.ts +0 -3
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +4 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +6 -3
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
  22. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
  23. package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
  24. package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
  25. package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
  26. package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
  27. package/dist/mail-sent/mail-sent.controller.js +5 -3
  28. package/dist/mail-sent/mail-sent.controller.js.map +1 -1
  29. package/dist/mail-sent/mail-sent.service.d.ts +28 -3
  30. package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
  31. package/dist/mail-sent/mail-sent.service.js +108 -3
  32. package/dist/mail-sent/mail-sent.service.js.map +1 -1
  33. package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
  34. package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
  35. package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
  36. package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
  37. package/dist/oauth/oauth.controller.d.ts +4 -2
  38. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  39. package/dist/oauth/oauth.controller.js +39 -11
  40. package/dist/oauth/oauth.controller.js.map +1 -1
  41. package/dist/oauth/oauth.errors.d.ts +16 -0
  42. package/dist/oauth/oauth.errors.d.ts.map +1 -0
  43. package/dist/oauth/oauth.errors.js +30 -0
  44. package/dist/oauth/oauth.errors.js.map +1 -0
  45. package/dist/oauth/oauth.module.d.ts.map +1 -1
  46. package/dist/oauth/oauth.module.js +10 -1
  47. package/dist/oauth/oauth.module.js.map +1 -1
  48. package/dist/oauth/oauth.service.d.ts +9 -1
  49. package/dist/oauth/oauth.service.d.ts.map +1 -1
  50. package/dist/oauth/oauth.service.js +97 -24
  51. package/dist/oauth/oauth.service.js.map +1 -1
  52. package/dist/oauth/providers/abstract.provider.d.ts +6 -0
  53. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  54. package/dist/oauth/providers/abstract.provider.js +34 -0
  55. package/dist/oauth/providers/abstract.provider.js.map +1 -1
  56. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
  57. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  58. package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
  59. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  60. package/dist/security/security.service.d.ts +1 -0
  61. package/dist/security/security.service.d.ts.map +1 -1
  62. package/dist/security/security.service.js +9 -10
  63. package/dist/security/security.service.js.map +1 -1
  64. package/dist/session/session.service.d.ts +5 -1
  65. package/dist/session/session.service.d.ts.map +1 -1
  66. package/dist/session/session.service.js +76 -10
  67. package/dist/session/session.service.js.map +1 -1
  68. package/dist/setting/setting.service.js +4 -4
  69. package/dist/setting/setting.service.js.map +1 -1
  70. package/dist/token/token.service.d.ts +1 -0
  71. package/dist/token/token.service.d.ts.map +1 -1
  72. package/dist/token/token.service.js +23 -12
  73. package/dist/token/token.service.js.map +1 -1
  74. package/dist/user/user.service.d.ts +1 -1
  75. package/dist/user/user.service.d.ts.map +1 -1
  76. package/dist/user/user.service.js +3 -0
  77. package/dist/user/user.service.js.map +1 -1
  78. package/hedhog/data/dashboard.yaml +2 -2
  79. package/hedhog/data/mail.yaml +147 -0
  80. package/hedhog/data/route.yaml +8 -0
  81. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
  82. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
  83. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
  84. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  85. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
  86. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
  87. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
  88. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
  89. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
  90. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
  91. package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
  92. package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
  93. package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
  94. package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
  95. package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
  96. package/hedhog/frontend/messages/en.json +45 -6
  97. package/hedhog/frontend/messages/pt.json +45 -6
  98. package/package.json +4 -4
  99. package/src/auth/auth.controller.ts +15 -19
  100. package/src/auth/auth.service.ts +13 -8
  101. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
  102. package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
  103. package/src/index.ts +10 -7
  104. package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
  105. package/src/mail-sent/mail-sent.controller.ts +14 -2
  106. package/src/mail-sent/mail-sent.service.ts +159 -4
  107. package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
  108. package/src/oauth/oauth.controller.ts +58 -1
  109. package/src/oauth/oauth.errors.ts +32 -0
  110. package/src/oauth/oauth.module.ts +10 -1
  111. package/src/oauth/oauth.service.ts +178 -28
  112. package/src/oauth/providers/abstract.provider.ts +64 -0
  113. package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
  114. package/src/security/security.service.ts +12 -10
  115. package/src/session/session.service.ts +84 -9
  116. package/src/setting/setting.service.ts +10 -10
  117. package/src/token/token.service.ts +29 -11
  118. 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
- Dialog,
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 { Clock, Mail, User } from 'lucide-react';
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 [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
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 { data: logsResult, refetch: refetchLogs } = useQuery<
59
- PaginationResult<MailSent>
60
- >({
61
- queryKey: ['mail-sent', debouncedSearch, page, pageSize],
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
- setIsDetailDialogOpen(true);
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
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
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
- <SearchBar
139
- searchQuery={searchTerm}
140
- onSearchChange={handleSearchChange}
141
- onSearch={() => setPage(1)}
142
- placeholder={t('searchPlaceholder')}
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={searchTerm ? t('adjustSearch') : t('noEmailsSent')}
151
- actionLabel={searchTerm ? t('clearSearch') : t('refreshList')}
413
+ description={
414
+ hasActiveFilters ? t('adjustSearch') : t('noEmailsSent')
415
+ }
416
+ actionLabel={
417
+ hasActiveFilters ? t('clearFilters') : t('refreshList')
418
+ }
152
419
  onAction={() => {
153
- if (searchTerm) {
154
- setSearchTerm('');
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
- <Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
228
- <DialogContent className="max-w-2xl max-h-[80vh]">
229
- <DialogHeader>
230
- <DialogTitle>{t('emailDetails')}</DialogTitle>
231
- <DialogDescription>{t('detailsDescription')}</DialogDescription>
232
- </DialogHeader>
233
- <ScrollArea className="max-h-[60vh]">
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
- </DialogContent>
297
- </Dialog>
707
+ </SheetContent>
708
+ </Sheet>
298
709
  )}
299
710
  </Page>
300
711
  );