@hed-hog/contact-us 0.0.278 → 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 +3 -1
- package/hedhog/frontend/app/page.tsx.ejs +144 -147
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
```markdown
|
|
1
2
|
# @hed-hog/contact-us
|
|
2
3
|
|
|
3
4
|
## 1. Visão geral do módulo
|
|
@@ -58,7 +59,7 @@ O módulo `@hed-hog/contact-us` é responsável pela gestão das mensagens envia
|
|
|
58
59
|
- **Descrição:** Retorna os detalhes de uma mensagem de contato pelo seu ID.
|
|
59
60
|
- **Parâmetros:**
|
|
60
61
|
- `id` (path): número inteiro identificador da mensagem.
|
|
61
|
-
- **Resposta:**
|
|
62
|
+
- **Resposta:**
|
|
62
63
|
```json
|
|
63
64
|
{
|
|
64
65
|
"id": number,
|
|
@@ -417,3 +418,4 @@ Resposta: objeto da mensagem excluída.
|
|
|
417
418
|
---
|
|
418
419
|
|
|
419
420
|
Este módulo integra-se com os módulos `@hed-hog/api-locale`, `@hed-hog/api-prisma`, `@hed-hog/api-pagination` e `@hed-hog/core` para funcionalidades de localização, acesso ao banco de dados, paginação e envio de e-mails.
|
|
421
|
+
```
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
EmptyState,
|
|
5
|
+
Page,
|
|
6
|
+
PageHeader,
|
|
7
|
+
PaginationFooter,
|
|
8
|
+
SearchBar,
|
|
9
|
+
StatsCards,
|
|
10
|
+
} from '@/components/entity-list';
|
|
4
11
|
import {
|
|
5
12
|
AlertDialog,
|
|
6
13
|
AlertDialogAction,
|
|
@@ -31,6 +38,14 @@ import {
|
|
|
31
38
|
SelectTrigger,
|
|
32
39
|
SelectValue,
|
|
33
40
|
} from '@/components/ui/select';
|
|
41
|
+
import {
|
|
42
|
+
Sheet,
|
|
43
|
+
SheetContent,
|
|
44
|
+
SheetDescription,
|
|
45
|
+
SheetFooter,
|
|
46
|
+
SheetHeader,
|
|
47
|
+
SheetTitle,
|
|
48
|
+
} from '@/components/ui/sheet';
|
|
34
49
|
import { Textarea } from '@/components/ui/textarea';
|
|
35
50
|
import { useDebounce } from '@/hooks/use-debounce';
|
|
36
51
|
import { formatDateTime } from '@/lib/format-date';
|
|
@@ -94,7 +109,7 @@ interface ContactUs {
|
|
|
94
109
|
user_contact_us_user_idTouser?: User;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
export default function
|
|
112
|
+
export default function ContactUsPage() {
|
|
98
113
|
const t = useTranslations('contact-us.ContactUs');
|
|
99
114
|
|
|
100
115
|
const statusOptions = [
|
|
@@ -113,6 +128,8 @@ export default function Page() {
|
|
|
113
128
|
const [searchTerm, setSearchTerm] = useState('');
|
|
114
129
|
const debouncedSearch = useDebounce(searchTerm);
|
|
115
130
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
131
|
+
const [page, setPage] = useState(1);
|
|
132
|
+
const [pageSize, setPageSize] = useState(10);
|
|
116
133
|
const [loadingRespond, setLoadingRespond] = useState(false);
|
|
117
134
|
|
|
118
135
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
@@ -130,27 +147,29 @@ export default function Page() {
|
|
|
130
147
|
const { data: contacts, refetch: refetchList } = useQuery<
|
|
131
148
|
PaginationResult<ContactUs>
|
|
132
149
|
>({
|
|
133
|
-
queryKey: ['contact-us'],
|
|
150
|
+
queryKey: ['contact-us', debouncedSearch, statusFilter, page, pageSize],
|
|
134
151
|
queryFn: async () => {
|
|
135
152
|
const response = await request({
|
|
136
153
|
url: '/contact-us',
|
|
137
154
|
params: {
|
|
155
|
+
page,
|
|
156
|
+
pageSize,
|
|
138
157
|
search: debouncedSearch,
|
|
139
|
-
status: statusFilter,
|
|
158
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
140
159
|
},
|
|
141
160
|
});
|
|
142
161
|
return response.data as PaginationResult<ContactUs>;
|
|
143
162
|
},
|
|
144
|
-
initialData: {
|
|
145
|
-
data: [],
|
|
146
|
-
total: 0,
|
|
147
|
-
page: 1,
|
|
148
|
-
pageSize: 10,
|
|
149
|
-
lastPage: 1,
|
|
150
|
-
prev: null,
|
|
151
|
-
next: null,
|
|
152
|
-
},
|
|
153
163
|
});
|
|
164
|
+
const contactList = contacts ?? {
|
|
165
|
+
data: [],
|
|
166
|
+
total: 0,
|
|
167
|
+
page: 1,
|
|
168
|
+
pageSize: 10,
|
|
169
|
+
lastPage: 1,
|
|
170
|
+
prev: null,
|
|
171
|
+
next: null,
|
|
172
|
+
};
|
|
154
173
|
|
|
155
174
|
const getStatusBadge = (status: ContactUsStatusEnum) => {
|
|
156
175
|
const statusOption = statusOptions.find((s) => s.value === status);
|
|
@@ -219,105 +238,89 @@ export default function Page() {
|
|
|
219
238
|
|
|
220
239
|
useEffect(() => {
|
|
221
240
|
refetchList();
|
|
222
|
-
}, [debouncedSearch, statusFilter]);
|
|
241
|
+
}, [debouncedSearch, statusFilter, page, pageSize]);
|
|
242
|
+
|
|
243
|
+
const statsCards = [
|
|
244
|
+
{
|
|
245
|
+
title: t('totalContacts'),
|
|
246
|
+
value: stats?.total ?? 0,
|
|
247
|
+
icon: <MessageSquare className="h-5 w-5" />,
|
|
248
|
+
iconBgColor: 'bg-blue-50',
|
|
249
|
+
iconColor: 'text-blue-600',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
title: t('new'),
|
|
253
|
+
value: stats?.totalNew ?? 0,
|
|
254
|
+
icon: <Clock className="h-5 w-5" />,
|
|
255
|
+
iconBgColor: 'bg-orange-50',
|
|
256
|
+
iconColor: 'text-orange-600',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
title: t('responded'),
|
|
260
|
+
value: stats?.totalResponded ?? 0,
|
|
261
|
+
icon: <Reply className="h-5 w-5" />,
|
|
262
|
+
iconBgColor: 'bg-green-50',
|
|
263
|
+
iconColor: 'text-green-600',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
title: t('inProgress'),
|
|
267
|
+
value: stats?.totalProgress ?? 0,
|
|
268
|
+
icon: <UserIcon className="h-5 w-5" />,
|
|
269
|
+
iconBgColor: 'bg-purple-50',
|
|
270
|
+
iconColor: 'text-purple-600',
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const searchControls = [
|
|
275
|
+
{
|
|
276
|
+
id: 'status',
|
|
277
|
+
type: 'select' as const,
|
|
278
|
+
value: statusFilter,
|
|
279
|
+
onChange: (value: string) => {
|
|
280
|
+
setStatusFilter(value);
|
|
281
|
+
setPage(1);
|
|
282
|
+
},
|
|
283
|
+
placeholder: t('status'),
|
|
284
|
+
options: [
|
|
285
|
+
{ value: 'all', label: t('allStatus') },
|
|
286
|
+
...statusOptions.map((status) => ({
|
|
287
|
+
value: status.value,
|
|
288
|
+
label: status.label,
|
|
289
|
+
})),
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
];
|
|
223
293
|
|
|
224
294
|
return (
|
|
225
|
-
<
|
|
295
|
+
<Page>
|
|
226
296
|
<PageHeader
|
|
227
|
-
breadcrumbs={[
|
|
228
|
-
{ label: 'Home', href: '/' },
|
|
229
|
-
{ label: t('description') },
|
|
230
|
-
]}
|
|
297
|
+
breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
|
|
231
298
|
title={t('title')}
|
|
232
299
|
description={t('description')}
|
|
233
300
|
/>
|
|
234
301
|
|
|
235
|
-
<
|
|
236
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
237
|
-
<CardContent className="p-4">
|
|
238
|
-
<div className="flex items-center space-x-3">
|
|
239
|
-
<MessageSquare className="h-10 w-10 text-blue-500" />
|
|
240
|
-
<div>
|
|
241
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
242
|
-
{t('totalContacts')}
|
|
243
|
-
</p>
|
|
244
|
-
<p className="text-2xl font-bold">{stats?.total}</p>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
</CardContent>
|
|
248
|
-
</Card>
|
|
249
|
-
|
|
250
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
251
|
-
<CardContent className="p-4">
|
|
252
|
-
<div className="flex items-center space-x-3">
|
|
253
|
-
<Clock className="h-10 w-10 text-orange-500" />
|
|
254
|
-
<div>
|
|
255
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
256
|
-
{t('new')}
|
|
257
|
-
</p>
|
|
258
|
-
<p className="text-2xl font-bold">{stats?.totalNew}</p>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
</CardContent>
|
|
262
|
-
</Card>
|
|
263
|
-
|
|
264
|
-
<Card className="transition-shadow hover:shadow-md p-0">
|
|
265
|
-
<CardContent className="p-4">
|
|
266
|
-
<div className="flex items-center space-x-3">
|
|
267
|
-
<Reply className="h-10 w-10 text-green-500" />
|
|
268
|
-
<div>
|
|
269
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
270
|
-
{t('responded')}
|
|
271
|
-
</p>
|
|
272
|
-
<p className="text-2xl font-bold">{stats?.totalResponded}</p>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
</CardContent>
|
|
276
|
-
</Card>
|
|
302
|
+
<StatsCards stats={statsCards} />
|
|
277
303
|
|
|
278
|
-
|
|
279
|
-
<CardContent className="p-4">
|
|
280
|
-
<div className="flex items-center space-x-3">
|
|
281
|
-
<UserIcon className="h-10 w-10 text-purple-500" />
|
|
282
|
-
<div>
|
|
283
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
284
|
-
{t('inProgress')}
|
|
285
|
-
</p>
|
|
286
|
-
<p className="text-2xl font-bold">{stats?.totalProgress}</p>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
</CardContent>
|
|
290
|
-
</Card>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
<div className="mb-4 flex flex-col gap-4 md:flex-row mt-4">
|
|
304
|
+
<div className="mb-2 mt-2">
|
|
294
305
|
<SearchBar
|
|
295
306
|
searchQuery={searchTerm}
|
|
296
|
-
onSearchChange={
|
|
297
|
-
|
|
307
|
+
onSearchChange={(value) => {
|
|
308
|
+
setSearchTerm(value);
|
|
309
|
+
setPage(1);
|
|
310
|
+
}}
|
|
311
|
+
onSearch={() => {
|
|
312
|
+
setPage(1);
|
|
313
|
+
refetchList();
|
|
314
|
+
}}
|
|
298
315
|
placeholder={t('searchPlaceholder')}
|
|
316
|
+
controls={searchControls}
|
|
299
317
|
/>
|
|
300
|
-
<div className="flex gap-2 ">
|
|
301
|
-
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
302
|
-
<SelectTrigger className="w-40">
|
|
303
|
-
<SelectValue placeholder={t('status')} />
|
|
304
|
-
</SelectTrigger>
|
|
305
|
-
<SelectContent>
|
|
306
|
-
<SelectItem value="all">{t('allStatus')}</SelectItem>
|
|
307
|
-
{statusOptions.map((status) => (
|
|
308
|
-
<SelectItem key={status.value} value={status.value}>
|
|
309
|
-
{status.label}
|
|
310
|
-
</SelectItem>
|
|
311
|
-
))}
|
|
312
|
-
</SelectContent>
|
|
313
|
-
</Select>
|
|
314
|
-
</div>
|
|
315
318
|
</div>
|
|
316
319
|
|
|
317
320
|
<div className="space-y-4">
|
|
318
|
-
{
|
|
321
|
+
{contactList.data.length > 0 ? (
|
|
319
322
|
<div className="grid gap-4">
|
|
320
|
-
{
|
|
323
|
+
{contactList.data.map((contact) => (
|
|
321
324
|
<Card
|
|
322
325
|
key={contact.id}
|
|
323
326
|
className="overflow-hidden transition-all duration-200 hover:border-primary/20 hover:shadow-md"
|
|
@@ -462,34 +465,30 @@ export default function Page() {
|
|
|
462
465
|
))}
|
|
463
466
|
</div>
|
|
464
467
|
) : (
|
|
465
|
-
<
|
|
466
|
-
<
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
{(searchTerm || statusFilter !== 'all') && (
|
|
478
|
-
<Button
|
|
479
|
-
variant="outline"
|
|
480
|
-
onClick={() => {
|
|
481
|
-
setSearchTerm('');
|
|
482
|
-
setStatusFilter('');
|
|
483
|
-
refetchList();
|
|
484
|
-
}}
|
|
485
|
-
>
|
|
486
|
-
{t('clearFilters')}
|
|
487
|
-
</Button>
|
|
488
|
-
)}
|
|
489
|
-
</div>
|
|
490
|
-
</CardContent>
|
|
491
|
-
</Card>
|
|
468
|
+
<EmptyState
|
|
469
|
+
icon={<MessageSquare className="h-12 w-12" />}
|
|
470
|
+
title={t('noContactsFound')}
|
|
471
|
+
description={t('adjustFiltersSearch')}
|
|
472
|
+
actionLabel={t('clearFilters')}
|
|
473
|
+
onAction={() => {
|
|
474
|
+
setSearchTerm('');
|
|
475
|
+
setStatusFilter('all');
|
|
476
|
+
setPage(1);
|
|
477
|
+
refetchList();
|
|
478
|
+
}}
|
|
479
|
+
/>
|
|
492
480
|
)}
|
|
481
|
+
|
|
482
|
+
<PaginationFooter
|
|
483
|
+
currentPage={contactList.page || page}
|
|
484
|
+
pageSize={contactList.pageSize || pageSize}
|
|
485
|
+
totalItems={contactList.total}
|
|
486
|
+
onPageChange={setPage}
|
|
487
|
+
onPageSizeChange={(nextPageSize) => {
|
|
488
|
+
setPageSize(nextPageSize);
|
|
489
|
+
setPage(1);
|
|
490
|
+
}}
|
|
491
|
+
/>
|
|
493
492
|
</div>
|
|
494
493
|
|
|
495
494
|
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
|
@@ -587,17 +586,25 @@ export default function Page() {
|
|
|
587
586
|
</DialogContent>
|
|
588
587
|
</Dialog>
|
|
589
588
|
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
589
|
+
<Sheet
|
|
590
|
+
open={isReplyDialogOpen}
|
|
591
|
+
onOpenChange={(open) => {
|
|
592
|
+
setIsReplyDialogOpen(open);
|
|
593
|
+
if (!open) {
|
|
594
|
+
setReplyMessage('');
|
|
595
|
+
}
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
|
|
599
|
+
<SheetHeader className="mb-2">
|
|
600
|
+
<SheetTitle>{t('replyContact')}</SheetTitle>
|
|
601
|
+
<SheetDescription>
|
|
595
602
|
{t('sendResponseTo', { name: String(selectedContact?.name) })}
|
|
596
|
-
</
|
|
597
|
-
</
|
|
603
|
+
</SheetDescription>
|
|
604
|
+
</SheetHeader>
|
|
598
605
|
|
|
599
606
|
{selectedContact && (
|
|
600
|
-
<div className="space-y-4">
|
|
607
|
+
<div className="space-y-4 px-4">
|
|
601
608
|
<div className="rounded-md border bg-muted/50 p-3">
|
|
602
609
|
<div className="mb-2 text-sm font-medium">
|
|
603
610
|
{t('originalMessage')}
|
|
@@ -622,21 +629,11 @@ export default function Page() {
|
|
|
622
629
|
</div>
|
|
623
630
|
)}
|
|
624
631
|
|
|
625
|
-
<
|
|
626
|
-
<Button
|
|
627
|
-
variant="outline"
|
|
628
|
-
onClick={() => {
|
|
629
|
-
setIsReplyDialogOpen(false);
|
|
630
|
-
setReplyMessage('');
|
|
631
|
-
}}
|
|
632
|
-
>
|
|
633
|
-
<X className="mr-2 h-4 w-4" />
|
|
634
|
-
{t('cancel')}
|
|
635
|
-
</Button>
|
|
632
|
+
<SheetFooter>
|
|
636
633
|
<Button
|
|
637
634
|
onClick={handleSendReply}
|
|
638
635
|
disabled={!replyMessage.trim() || loadingRespond}
|
|
639
|
-
className="transition-colors hover:bg-primary/90"
|
|
636
|
+
className="w-full transition-colors hover:bg-primary/90"
|
|
640
637
|
>
|
|
641
638
|
{loadingRespond ? (
|
|
642
639
|
<>
|
|
@@ -650,9 +647,9 @@ export default function Page() {
|
|
|
650
647
|
</>
|
|
651
648
|
)}
|
|
652
649
|
</Button>
|
|
653
|
-
</
|
|
654
|
-
</
|
|
655
|
-
</
|
|
656
|
-
</
|
|
650
|
+
</SheetFooter>
|
|
651
|
+
</SheetContent>
|
|
652
|
+
</Sheet>
|
|
653
|
+
</Page>
|
|
657
654
|
);
|
|
658
655
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/contact-us",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.285",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
-
"@hed-hog/core": "0.0.278",
|
|
13
12
|
"@hed-hog/api-locale": "0.0.13",
|
|
14
13
|
"@hed-hog/api-pagination": "0.0.6",
|
|
15
|
-
"@hed-hog/
|
|
16
|
-
"@hed-hog/api-prisma": "0.0.5"
|
|
14
|
+
"@hed-hog/core": "0.0.285",
|
|
15
|
+
"@hed-hog/api-prisma": "0.0.5",
|
|
16
|
+
"@hed-hog/api": "0.0.4"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|