@hed-hog/contact-us 0.0.185 → 0.0.190
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.
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PageHeader, SearchBar } from '@/components/entity-list';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from '@/components/ui/alert-dialog';
|
|
15
|
+
import { Badge } from '@/components/ui/badge';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
18
|
+
import {
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogDescription,
|
|
22
|
+
DialogFooter,
|
|
23
|
+
DialogHeader,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
} from '@/components/ui/dialog';
|
|
26
|
+
import { Label } from '@/components/ui/label';
|
|
27
|
+
import {
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectItem,
|
|
31
|
+
SelectTrigger,
|
|
32
|
+
SelectValue,
|
|
33
|
+
} from '@/components/ui/select';
|
|
34
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
35
|
+
import { useDebounce } from '@/hooks/use-debounce';
|
|
36
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
37
|
+
import { cn } from '@/lib/utils';
|
|
38
|
+
import { User } from '@hed-hog/api-types';
|
|
39
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
40
|
+
import {
|
|
41
|
+
Calendar,
|
|
42
|
+
Clock,
|
|
43
|
+
Eye,
|
|
44
|
+
Loader2,
|
|
45
|
+
Mail,
|
|
46
|
+
MessageSquare,
|
|
47
|
+
Reply,
|
|
48
|
+
Send,
|
|
49
|
+
Trash2,
|
|
50
|
+
UserIcon,
|
|
51
|
+
X,
|
|
52
|
+
} from 'lucide-react';
|
|
53
|
+
import { useTranslations } from 'next-intl';
|
|
54
|
+
import { useEffect, useState } from 'react';
|
|
55
|
+
import { toast } from 'sonner';
|
|
56
|
+
|
|
57
|
+
interface Stats {
|
|
58
|
+
total: number;
|
|
59
|
+
totalNew: number;
|
|
60
|
+
totalResponded: number;
|
|
61
|
+
totalProgress: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
enum ContactUsStatusEnum {
|
|
65
|
+
new = 'new',
|
|
66
|
+
progress = 'progress',
|
|
67
|
+
answered = 'answered',
|
|
68
|
+
archived = 'archived',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type PaginationResult<T> = {
|
|
72
|
+
total: number;
|
|
73
|
+
lastPage: number;
|
|
74
|
+
page: number;
|
|
75
|
+
pageSize: number;
|
|
76
|
+
prev: number | null;
|
|
77
|
+
next: number | null;
|
|
78
|
+
data: T[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
interface ContactUs {
|
|
82
|
+
id?: number;
|
|
83
|
+
name: string;
|
|
84
|
+
email: string;
|
|
85
|
+
message: string;
|
|
86
|
+
created_at?: string;
|
|
87
|
+
updated_at?: string;
|
|
88
|
+
status?: ContactUsStatusEnum;
|
|
89
|
+
response?: string;
|
|
90
|
+
response_at?: string;
|
|
91
|
+
response_id?: number;
|
|
92
|
+
user_id?: number;
|
|
93
|
+
user?: User;
|
|
94
|
+
user_contact_us_user_idTouser?: User;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function Page() {
|
|
98
|
+
const t = useTranslations('contact-us.ContactUs');
|
|
99
|
+
|
|
100
|
+
const statusOptions = [
|
|
101
|
+
{ value: 'new', label: t('statusNew'), color: 'bg-blue-500' },
|
|
102
|
+
{ value: 'progress', label: t('statusProgress'), color: 'bg-yellow-500' },
|
|
103
|
+
{ value: 'answered', label: t('statusAnswered'), color: 'bg-green-500' },
|
|
104
|
+
{ value: 'archived', label: t('statusArchived'), color: 'bg-gray-500' },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const [selectedContact, setSelectedContact] = useState<ContactUs | null>(
|
|
108
|
+
null
|
|
109
|
+
);
|
|
110
|
+
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
|
|
111
|
+
const [isReplyDialogOpen, setIsReplyDialogOpen] = useState(false);
|
|
112
|
+
const [replyMessage, setReplyMessage] = useState('');
|
|
113
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
114
|
+
const debouncedSearch = useDebounce(searchTerm);
|
|
115
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
116
|
+
const [loadingRespond, setLoadingRespond] = useState(false);
|
|
117
|
+
|
|
118
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
119
|
+
|
|
120
|
+
const { data: stats, refetch: refetchStats } = useQuery<Stats>({
|
|
121
|
+
queryKey: ['contact-us-stats'],
|
|
122
|
+
queryFn: async () => {
|
|
123
|
+
const response = await request({
|
|
124
|
+
url: '/contact-us/stats',
|
|
125
|
+
});
|
|
126
|
+
return response.data as Stats;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const { data: contacts, refetch: refetchList } = useQuery<
|
|
131
|
+
PaginationResult<ContactUs>
|
|
132
|
+
>({
|
|
133
|
+
queryKey: ['contact-us'],
|
|
134
|
+
queryFn: async () => {
|
|
135
|
+
const response = await request({
|
|
136
|
+
url: '/contact-us',
|
|
137
|
+
params: {
|
|
138
|
+
search: debouncedSearch,
|
|
139
|
+
status: statusFilter,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return response.data as PaginationResult<ContactUs>;
|
|
143
|
+
},
|
|
144
|
+
initialData: {
|
|
145
|
+
data: [],
|
|
146
|
+
total: 0,
|
|
147
|
+
page: 1,
|
|
148
|
+
pageSize: 10,
|
|
149
|
+
lastPage: 1,
|
|
150
|
+
prev: null,
|
|
151
|
+
next: null,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const getStatusBadge = (status: ContactUsStatusEnum) => {
|
|
156
|
+
const statusOption = statusOptions.find((s) => s.value === status);
|
|
157
|
+
return statusOption || statusOptions[0];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleViewContact = (contact: ContactUs): void => {
|
|
161
|
+
setSelectedContact(contact);
|
|
162
|
+
setIsViewDialogOpen(true);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleReplyContact = (contact: ContactUs): void => {
|
|
166
|
+
setSelectedContact(contact);
|
|
167
|
+
setIsReplyDialogOpen(true);
|
|
168
|
+
setReplyMessage('');
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleSendReply = (): void => {
|
|
172
|
+
if (selectedContact && replyMessage.trim()) {
|
|
173
|
+
setLoadingRespond(true);
|
|
174
|
+
request({
|
|
175
|
+
url: `/contact-us/response/${selectedContact.id}`,
|
|
176
|
+
method: 'POST',
|
|
177
|
+
data: {
|
|
178
|
+
response: replyMessage,
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
.then(() => {
|
|
182
|
+
toast.success(t('successResponse'));
|
|
183
|
+
setReplyMessage('');
|
|
184
|
+
setIsReplyDialogOpen(false);
|
|
185
|
+
setSelectedContact(null);
|
|
186
|
+
refetchList();
|
|
187
|
+
refetchStats();
|
|
188
|
+
})
|
|
189
|
+
.finally(() => {
|
|
190
|
+
setLoadingRespond(false);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleDeleteContact = (contactId: number): void => {
|
|
196
|
+
request({
|
|
197
|
+
url: `/contact-us/${contactId}`,
|
|
198
|
+
method: 'DELETE',
|
|
199
|
+
}).then(() => {
|
|
200
|
+
toast.success(t('successDelete'));
|
|
201
|
+
refetchList();
|
|
202
|
+
refetchStats();
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleStatusChange = (contactId: number, newStatus: string): void => {
|
|
207
|
+
request({
|
|
208
|
+
url: `/contact-us/status/${contactId}`,
|
|
209
|
+
method: 'PATCH',
|
|
210
|
+
data: {
|
|
211
|
+
status: newStatus,
|
|
212
|
+
},
|
|
213
|
+
}).then(() => {
|
|
214
|
+
toast.success(t('successStatusUpdate'));
|
|
215
|
+
refetchList();
|
|
216
|
+
refetchStats();
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
refetchList();
|
|
222
|
+
}, [debouncedSearch, statusFilter]);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className="flex flex-col h-screen px-4">
|
|
226
|
+
<PageHeader
|
|
227
|
+
breadcrumbs={[
|
|
228
|
+
{ label: 'Home', href: '/' },
|
|
229
|
+
{ label: t('description') },
|
|
230
|
+
]}
|
|
231
|
+
title={t('title')}
|
|
232
|
+
description={t('description')}
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
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>
|
|
277
|
+
|
|
278
|
+
<Card className="transition-shadow hover:shadow-md p-0">
|
|
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">
|
|
294
|
+
<SearchBar
|
|
295
|
+
searchQuery={searchTerm}
|
|
296
|
+
onSearchChange={setSearchTerm}
|
|
297
|
+
onSearch={() => refetchList()}
|
|
298
|
+
placeholder={t('searchPlaceholder')}
|
|
299
|
+
/>
|
|
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
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="space-y-4">
|
|
318
|
+
{contacts.data.length > 0 ? (
|
|
319
|
+
<div className="grid gap-4">
|
|
320
|
+
{contacts.data.map((contact) => (
|
|
321
|
+
<Card
|
|
322
|
+
key={contact.id}
|
|
323
|
+
className="overflow-hidden transition-all duration-200 hover:border-primary/20 hover:shadow-md"
|
|
324
|
+
>
|
|
325
|
+
<CardContent className="p-6">
|
|
326
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
327
|
+
<div className="flex-1 space-y-3">
|
|
328
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
329
|
+
<div className="flex items-center space-x-3">
|
|
330
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
|
|
331
|
+
<UserIcon className="h-5 w-5 text-primary" />
|
|
332
|
+
</div>
|
|
333
|
+
<div>
|
|
334
|
+
<h3 className="font-semibold">{contact.name}</h3>
|
|
335
|
+
<div className="flex items-center space-x-1 text-sm text-muted-foreground">
|
|
336
|
+
<Mail className="h-3 w-3" />
|
|
337
|
+
<span>{contact.email}</span>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="space-y-2">
|
|
344
|
+
<p className="line-clamp-2 text-sm text-muted-foreground">
|
|
345
|
+
{contact.message}
|
|
346
|
+
</p>
|
|
347
|
+
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
|
348
|
+
{contact.status && (
|
|
349
|
+
<Badge
|
|
350
|
+
className={cn(
|
|
351
|
+
'text-white',
|
|
352
|
+
getStatusBadge(contact.status)?.color
|
|
353
|
+
)}
|
|
354
|
+
>
|
|
355
|
+
{getStatusBadge(contact.status)?.label}
|
|
356
|
+
</Badge>
|
|
357
|
+
)}
|
|
358
|
+
{contact.created_at && (
|
|
359
|
+
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
|
360
|
+
<Calendar className="h-3 w-3" />
|
|
361
|
+
<span>
|
|
362
|
+
{formatDateTime(
|
|
363
|
+
contact.created_at,
|
|
364
|
+
getSettingValue,
|
|
365
|
+
currentLocaleCode
|
|
366
|
+
)}
|
|
367
|
+
</span>
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div className="flex flex-wrap gap-2 lg:flex-col lg:items-end">
|
|
375
|
+
<Button
|
|
376
|
+
variant="outline"
|
|
377
|
+
size="sm"
|
|
378
|
+
onClick={() => handleViewContact(contact)}
|
|
379
|
+
className="transition-colors hover:border-primary/20 hover:bg-primary/10 hover:text-primary "
|
|
380
|
+
>
|
|
381
|
+
<Eye className="mr-1 h-4 w-4" />
|
|
382
|
+
{t('view')}
|
|
383
|
+
</Button>
|
|
384
|
+
|
|
385
|
+
{contact.status !== 'answered' && (
|
|
386
|
+
<Button
|
|
387
|
+
variant="outline"
|
|
388
|
+
size="sm"
|
|
389
|
+
onClick={() => handleReplyContact(contact)}
|
|
390
|
+
className="transition-colors hover:border-green-200 hover:bg-green-50 hover:text-green-600 dark:hover:bg-green-950"
|
|
391
|
+
>
|
|
392
|
+
<Reply className="mr-1 h-4 w-4" />
|
|
393
|
+
{t('reply')}
|
|
394
|
+
</Button>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{contact.id && (
|
|
398
|
+
<Select
|
|
399
|
+
value={contact.status}
|
|
400
|
+
onValueChange={(value) => {
|
|
401
|
+
if (contact.id)
|
|
402
|
+
handleStatusChange(contact.id, value);
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<SelectTrigger className="h-8 w-32 text-xs">
|
|
406
|
+
<SelectValue />
|
|
407
|
+
</SelectTrigger>
|
|
408
|
+
<SelectContent>
|
|
409
|
+
{statusOptions.map((status) => (
|
|
410
|
+
<SelectItem
|
|
411
|
+
key={status.value}
|
|
412
|
+
value={status.value}
|
|
413
|
+
>
|
|
414
|
+
{status.label}
|
|
415
|
+
</SelectItem>
|
|
416
|
+
))}
|
|
417
|
+
</SelectContent>
|
|
418
|
+
</Select>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
<AlertDialog>
|
|
422
|
+
<AlertDialogTrigger asChild>
|
|
423
|
+
<Button
|
|
424
|
+
variant="outline"
|
|
425
|
+
size="sm"
|
|
426
|
+
className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
|
|
427
|
+
>
|
|
428
|
+
<Trash2 className="mr-1 h-4 w-4" />
|
|
429
|
+
{t('delete')}
|
|
430
|
+
</Button>
|
|
431
|
+
</AlertDialogTrigger>
|
|
432
|
+
<AlertDialogContent>
|
|
433
|
+
<AlertDialogHeader>
|
|
434
|
+
<AlertDialogTitle>
|
|
435
|
+
{t('confirmDelete')}
|
|
436
|
+
</AlertDialogTitle>
|
|
437
|
+
<AlertDialogDescription>
|
|
438
|
+
{t('deleteDescription', {
|
|
439
|
+
name: contact.name,
|
|
440
|
+
})}
|
|
441
|
+
</AlertDialogDescription>
|
|
442
|
+
</AlertDialogHeader>
|
|
443
|
+
<AlertDialogFooter>
|
|
444
|
+
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
|
445
|
+
<AlertDialogAction
|
|
446
|
+
onClick={() => {
|
|
447
|
+
if (contact.id) {
|
|
448
|
+
handleDeleteContact(contact.id);
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
452
|
+
>
|
|
453
|
+
{t('delete')}
|
|
454
|
+
</AlertDialogAction>
|
|
455
|
+
</AlertDialogFooter>
|
|
456
|
+
</AlertDialogContent>
|
|
457
|
+
</AlertDialog>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</CardContent>
|
|
461
|
+
</Card>
|
|
462
|
+
))}
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
<Card>
|
|
466
|
+
<CardContent className="p-12 text-center">
|
|
467
|
+
<div className="flex flex-col items-center space-y-4">
|
|
468
|
+
<MessageSquare className="h-12 w-12 text-muted-foreground" />
|
|
469
|
+
<div>
|
|
470
|
+
<h3 className="text-lg font-semibold">
|
|
471
|
+
{t('noContactsFound')}
|
|
472
|
+
</h3>
|
|
473
|
+
<p className="text-muted-foreground">
|
|
474
|
+
{t('adjustFiltersSearch')}
|
|
475
|
+
</p>
|
|
476
|
+
</div>
|
|
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>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
|
496
|
+
<DialogContent className="max-w-2xl">
|
|
497
|
+
<DialogHeader className="mb-2">
|
|
498
|
+
<DialogTitle>{t('contactDetails')}</DialogTitle>
|
|
499
|
+
<DialogDescription>{t('viewAllInfo')}</DialogDescription>
|
|
500
|
+
</DialogHeader>
|
|
501
|
+
|
|
502
|
+
{selectedContact && (
|
|
503
|
+
<div className="space-y-4">
|
|
504
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
505
|
+
<div>
|
|
506
|
+
<Label className="text-sm font-medium">{t('name')}</Label>
|
|
507
|
+
<p className="text-sm text-muted-foreground">
|
|
508
|
+
{selectedContact.name}
|
|
509
|
+
</p>
|
|
510
|
+
</div>
|
|
511
|
+
<div>
|
|
512
|
+
<Label className="text-sm font-medium">{t('email')}</Label>
|
|
513
|
+
<p className="text-sm text-muted-foreground">
|
|
514
|
+
{selectedContact.email}
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
{selectedContact.status && (
|
|
518
|
+
<div>
|
|
519
|
+
<Label className="text-sm font-medium">{t('status')}</Label>
|
|
520
|
+
<div>
|
|
521
|
+
<Badge
|
|
522
|
+
className={cn(
|
|
523
|
+
'text-white',
|
|
524
|
+
getStatusBadge(selectedContact.status)?.color
|
|
525
|
+
)}
|
|
526
|
+
>
|
|
527
|
+
{getStatusBadge(selectedContact.status)?.label}
|
|
528
|
+
</Badge>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
{selectedContact.created_at && (
|
|
533
|
+
<div>
|
|
534
|
+
<Label className="text-sm font-medium">
|
|
535
|
+
{t('dateTime')}
|
|
536
|
+
</Label>
|
|
537
|
+
<p className="text-sm text-muted-foreground">
|
|
538
|
+
{formatDateTime(
|
|
539
|
+
selectedContact.created_at,
|
|
540
|
+
getSettingValue,
|
|
541
|
+
currentLocaleCode
|
|
542
|
+
)}
|
|
543
|
+
</p>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div>
|
|
549
|
+
<Label className="text-sm font-medium">{t('message')}</Label>
|
|
550
|
+
<div className="mt-2 rounded-md border p-3 font-mono text-sm">
|
|
551
|
+
{selectedContact.message}
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
{selectedContact.response && (
|
|
556
|
+
<div>
|
|
557
|
+
<Label className="text-sm font-medium">{t('response')}</Label>
|
|
558
|
+
<div className="mt-2 rounded-md border p-3 font-mono text-sm">
|
|
559
|
+
{selectedContact.response}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
)}
|
|
565
|
+
|
|
566
|
+
<DialogFooter className="mt-2">
|
|
567
|
+
<Button
|
|
568
|
+
variant="outline"
|
|
569
|
+
onClick={() => setIsViewDialogOpen(false)}
|
|
570
|
+
>
|
|
571
|
+
<X className="mr-2 h-4 w-4" />
|
|
572
|
+
{t('close')}
|
|
573
|
+
</Button>
|
|
574
|
+
{selectedContact && selectedContact?.status !== 'answered' && (
|
|
575
|
+
<Button
|
|
576
|
+
onClick={() => {
|
|
577
|
+
setIsViewDialogOpen(false);
|
|
578
|
+
handleReplyContact(selectedContact);
|
|
579
|
+
}}
|
|
580
|
+
className="transition-colors hover:bg-primary/90"
|
|
581
|
+
>
|
|
582
|
+
<Reply className="mr-2 h-4 w-4" />
|
|
583
|
+
{t('reply')}
|
|
584
|
+
</Button>
|
|
585
|
+
)}
|
|
586
|
+
</DialogFooter>
|
|
587
|
+
</DialogContent>
|
|
588
|
+
</Dialog>
|
|
589
|
+
|
|
590
|
+
<Dialog open={isReplyDialogOpen} onOpenChange={setIsReplyDialogOpen}>
|
|
591
|
+
<DialogContent className="max-w-2xl">
|
|
592
|
+
<DialogHeader className="mb-2">
|
|
593
|
+
<DialogTitle>{t('replyContact')}</DialogTitle>
|
|
594
|
+
<DialogDescription>
|
|
595
|
+
{t('sendResponseTo', { name: String(selectedContact?.name) })}
|
|
596
|
+
</DialogDescription>
|
|
597
|
+
</DialogHeader>
|
|
598
|
+
|
|
599
|
+
{selectedContact && (
|
|
600
|
+
<div className="space-y-4">
|
|
601
|
+
<div className="rounded-md border bg-muted/50 p-3">
|
|
602
|
+
<div className="mb-2 text-sm font-medium">
|
|
603
|
+
{t('originalMessage')}
|
|
604
|
+
</div>
|
|
605
|
+
<div className="font-mono text-sm text-muted-foreground">
|
|
606
|
+
{selectedContact.message}
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
<div className="space-y-2">
|
|
611
|
+
<Label htmlFor="reply">{t('yourResponse')}</Label>
|
|
612
|
+
<Textarea
|
|
613
|
+
id="reply"
|
|
614
|
+
placeholder={t('responsePlaceholder')}
|
|
615
|
+
value={replyMessage}
|
|
616
|
+
onChange={(e) => setReplyMessage(e.target.value)}
|
|
617
|
+
rows={6}
|
|
618
|
+
className="resize-none"
|
|
619
|
+
autoFocus
|
|
620
|
+
/>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
<DialogFooter className="mt-2">
|
|
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>
|
|
636
|
+
<Button
|
|
637
|
+
onClick={handleSendReply}
|
|
638
|
+
disabled={!replyMessage.trim() || loadingRespond}
|
|
639
|
+
className="transition-colors hover:bg-primary/90"
|
|
640
|
+
>
|
|
641
|
+
{loadingRespond ? (
|
|
642
|
+
<>
|
|
643
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />{' '}
|
|
644
|
+
{t('sending')}
|
|
645
|
+
</>
|
|
646
|
+
) : (
|
|
647
|
+
<>
|
|
648
|
+
<Send className="mr-2 h-4 w-4" />
|
|
649
|
+
{t('sendResponse')}
|
|
650
|
+
</>
|
|
651
|
+
)}
|
|
652
|
+
</Button>
|
|
653
|
+
</DialogFooter>
|
|
654
|
+
</DialogContent>
|
|
655
|
+
</Dialog>
|
|
656
|
+
</div>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ContactUs": {
|
|
3
|
+
"title": "Contact Us",
|
|
4
|
+
"description": "Manage contact messages sent from the website",
|
|
5
|
+
"totalContacts": "Total Contacts",
|
|
6
|
+
"new": "New",
|
|
7
|
+
"responded": "Responded",
|
|
8
|
+
"inProgress": "In Progress",
|
|
9
|
+
"searchPlaceholder": "Search by name, email or message...",
|
|
10
|
+
"status": "Status",
|
|
11
|
+
"allStatus": "All Status",
|
|
12
|
+
"statusNew": "New",
|
|
13
|
+
"statusProgress": "In Progress",
|
|
14
|
+
"statusAnswered": "Answered",
|
|
15
|
+
"statusArchived": "Archived",
|
|
16
|
+
"view": "View",
|
|
17
|
+
"reply": "Reply",
|
|
18
|
+
"delete": "Delete",
|
|
19
|
+
"confirmDelete": "Confirm Deletion",
|
|
20
|
+
"deleteDescription": "Are you sure you want to delete the contact from {name}? This action cannot be undone.",
|
|
21
|
+
"cancel": "Cancel",
|
|
22
|
+
"noContactsFound": "No contacts found",
|
|
23
|
+
"adjustFiltersSearch": "Try adjusting the filters or search terms.",
|
|
24
|
+
"clearFilters": "Clear filters",
|
|
25
|
+
"contactDetails": "Contact Details",
|
|
26
|
+
"viewAllInfo": "View all contact information sent",
|
|
27
|
+
"name": "Name",
|
|
28
|
+
"email": "Email",
|
|
29
|
+
"dateTime": "Date/Time",
|
|
30
|
+
"message": "Message",
|
|
31
|
+
"response": "Response",
|
|
32
|
+
"close": "Close",
|
|
33
|
+
"replyContact": "Reply Contact",
|
|
34
|
+
"sendResponseTo": "Send a response to {name}",
|
|
35
|
+
"originalMessage": "Original message:",
|
|
36
|
+
"yourResponse": "Your response",
|
|
37
|
+
"responsePlaceholder": "Type your response here...",
|
|
38
|
+
"sendResponse": "Send Response",
|
|
39
|
+
"sending": "Sending...",
|
|
40
|
+
"at": "at",
|
|
41
|
+
"successResponse": "Response sent successfully",
|
|
42
|
+
"successDelete": "Contact deleted successfully",
|
|
43
|
+
"successStatusUpdate": "Status updated successfully"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ContactUs": {
|
|
3
|
+
"title": "Fale Conosco",
|
|
4
|
+
"description": "Gerencie as mensagens de contato enviadas pelo site",
|
|
5
|
+
"totalContacts": "Total de Contatos",
|
|
6
|
+
"new": "Novos",
|
|
7
|
+
"responded": "Respondidos",
|
|
8
|
+
"inProgress": "Em Andamento",
|
|
9
|
+
"searchPlaceholder": "Buscar por nome, email ou mensagem...",
|
|
10
|
+
"status": "Status",
|
|
11
|
+
"allStatus": "Todos os Status",
|
|
12
|
+
"statusNew": "Novo",
|
|
13
|
+
"statusProgress": "Em Andamento",
|
|
14
|
+
"statusAnswered": "Respondido",
|
|
15
|
+
"statusArchived": "Arquivado",
|
|
16
|
+
"view": "Ver",
|
|
17
|
+
"reply": "Responder",
|
|
18
|
+
"delete": "Excluir",
|
|
19
|
+
"confirmDelete": "Confirmar Exclusão",
|
|
20
|
+
"deleteDescription": "Tem certeza que deseja excluir o contato de {name}? Esta ação não pode ser desfeita.",
|
|
21
|
+
"cancel": "Cancelar",
|
|
22
|
+
"noContactsFound": "Nenhum contato encontrado",
|
|
23
|
+
"adjustFiltersSearch": "Tente ajustar os filtros ou termos de busca.",
|
|
24
|
+
"clearFilters": "Limpar filtros",
|
|
25
|
+
"contactDetails": "Detalhes do Contato",
|
|
26
|
+
"viewAllInfo": "Visualize todas as informações do contato enviado",
|
|
27
|
+
"name": "Nome",
|
|
28
|
+
"email": "Email",
|
|
29
|
+
"dateTime": "Data/Hora",
|
|
30
|
+
"message": "Mensagem",
|
|
31
|
+
"response": "Resposta",
|
|
32
|
+
"close": "Fechar",
|
|
33
|
+
"replyContact": "Responder Contato",
|
|
34
|
+
"sendResponseTo": "Envie uma resposta para {name}",
|
|
35
|
+
"originalMessage": "Mensagem original:",
|
|
36
|
+
"yourResponse": "Sua resposta",
|
|
37
|
+
"responsePlaceholder": "Digite sua resposta aqui...",
|
|
38
|
+
"sendResponse": "Enviar Resposta",
|
|
39
|
+
"sending": "Enviando...",
|
|
40
|
+
"at": "às",
|
|
41
|
+
"successResponse": "Resposta enviada com sucesso",
|
|
42
|
+
"successDelete": "Contato excluído com sucesso",
|
|
43
|
+
"successStatusUpdate": "Status atualizado com sucesso"
|
|
44
|
+
}
|
|
45
|
+
}
|
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.190",
|
|
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/api-locale": "0.0.11",
|
|
13
12
|
"@hed-hog/api-prisma": "0.0.4",
|
|
14
|
-
"@hed-hog/
|
|
15
|
-
"@hed-hog/
|
|
16
|
-
"@hed-hog/api-
|
|
13
|
+
"@hed-hog/api-pagination": "0.0.5",
|
|
14
|
+
"@hed-hog/core": "0.0.190",
|
|
15
|
+
"@hed-hog/api-locale": "0.0.11",
|
|
16
|
+
"@hed-hog/api": "0.0.3"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|