@hed-hog/contact 0.0.186 → 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,616 @@
1
+ 'use client';
2
+
3
+ import {
4
+ PageHeader,
5
+ PaginationFooter,
6
+ SearchBar,
7
+ } from '@/components/entity-list';
8
+ import { Button } from '@/components/ui/button';
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from '@/components/ui/dialog';
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from '@/components/ui/dropdown-menu';
24
+ import {
25
+ Form,
26
+ FormControl,
27
+ FormField,
28
+ FormItem,
29
+ FormLabel,
30
+ FormMessage,
31
+ } from '@/components/ui/form';
32
+ import { Input } from '@/components/ui/input';
33
+ import {
34
+ Select,
35
+ SelectContent,
36
+ SelectItem,
37
+ SelectTrigger,
38
+ SelectValue,
39
+ } from '@/components/ui/select';
40
+ import { Switch } from '@/components/ui/switch';
41
+ import {
42
+ Table,
43
+ TableBody,
44
+ TableCell,
45
+ TableHead,
46
+ TableHeader,
47
+ TableRow,
48
+ } from '@/components/ui/table';
49
+ import { COUNTRIES } from '@/constants/countries';
50
+ import { formatDate } from '@/lib/format-date';
51
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
52
+ import { zodResolver } from '@hookform/resolvers/zod';
53
+ import { MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';
54
+ import { useTranslations } from 'next-intl';
55
+ import { useState } from 'react';
56
+ import { useForm } from 'react-hook-form';
57
+ import { toast } from 'sonner';
58
+ import { z } from 'zod';
59
+
60
+ type PaginatedResult<T> = {
61
+ data: T[];
62
+ total: number;
63
+ page: number;
64
+ pageSize: number;
65
+ };
66
+
67
+ type DocumentType = {
68
+ document_type_id?: number;
69
+ id: number;
70
+ code: string;
71
+ name: string;
72
+ country_code: string;
73
+ is_unique: boolean;
74
+ created_at: string;
75
+ };
76
+
77
+ export default function DocumentTypePage() {
78
+ const t = useTranslations('contact.DocumentType');
79
+
80
+ const documentTypeSchema = z.object({
81
+ code: z.string().min(2, t('errorCode')),
82
+ name: z.string().min(2, t('errorName')),
83
+ country_code: z.string().min(2, t('errorCountry')),
84
+ is_unique: z.boolean(),
85
+ });
86
+ const { request, currentLocaleCode, getSettingValue } = useApp();
87
+
88
+ const [searchQuery, setSearchQuery] = useState('');
89
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
90
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
91
+ const [editingDocumentType, setEditingDocumentType] =
92
+ useState<DocumentType | null>(null);
93
+
94
+ const [page, setPage] = useState(1);
95
+ const [pageSize, setPageSize] = useState(12);
96
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
97
+ const [deletingId, setDeletingId] = useState<number | null>(null);
98
+
99
+ const {
100
+ data: paginate = { data: [], total: 0, page: 1, pageSize: 12 },
101
+ isLoading,
102
+ refetch,
103
+ } = useQuery<PaginatedResult<DocumentType>>({
104
+ queryKey: [
105
+ 'document-types',
106
+ page,
107
+ pageSize,
108
+ searchQuery,
109
+ currentLocaleCode,
110
+ ],
111
+ queryFn: async () => {
112
+ const params = new URLSearchParams();
113
+ params.set('page', String(page));
114
+ params.set('pageSize', String(pageSize));
115
+ if (searchQuery) params.set('search', searchQuery);
116
+
117
+ const response = await request<PaginatedResult<DocumentType>>({
118
+ url: `/person-document-type?${params.toString()}`,
119
+ method: 'GET',
120
+ });
121
+
122
+ return response.data;
123
+ },
124
+ });
125
+
126
+ const form = useForm<z.infer<typeof documentTypeSchema>>({
127
+ resolver: zodResolver(documentTypeSchema),
128
+ defaultValues: {
129
+ code: '',
130
+ name: '',
131
+ country_code: '',
132
+ is_unique: false,
133
+ },
134
+ });
135
+
136
+ const editForm = useForm<z.infer<typeof documentTypeSchema>>({
137
+ resolver: zodResolver(documentTypeSchema),
138
+ });
139
+
140
+ const onSubmit = async (values: z.infer<typeof documentTypeSchema>) => {
141
+ try {
142
+ const payload = {
143
+ code: values.code,
144
+ country_code: values.country_code,
145
+ is_unique: values.is_unique,
146
+ locale: {
147
+ [currentLocaleCode]: {
148
+ name: values.name,
149
+ },
150
+ },
151
+ };
152
+
153
+ await request({
154
+ url: '/person-document-type',
155
+ method: 'POST',
156
+ data: payload,
157
+ });
158
+
159
+ toast.success(t('successCreate'));
160
+ setIsDialogOpen(false);
161
+ form.reset();
162
+ refetch();
163
+ } catch (error: any) {
164
+ toast.error(error?.message || t('errorCreate'));
165
+ }
166
+ };
167
+
168
+ const onEditSubmit = async (values: z.infer<typeof documentTypeSchema>) => {
169
+ if (!editingDocumentType) return;
170
+
171
+ try {
172
+ const payload = {
173
+ code: values.code,
174
+ country_code: values.country_code,
175
+ is_unique: values.is_unique,
176
+ locale: {
177
+ [currentLocaleCode]: {
178
+ name: values.name,
179
+ },
180
+ },
181
+ };
182
+
183
+ await request({
184
+ url: `/person-document-type/${editingDocumentType.document_type_id || editingDocumentType.id}`,
185
+ method: 'PATCH',
186
+ data: payload,
187
+ });
188
+
189
+ toast.success(t('successUpdate'));
190
+ setIsEditDialogOpen(false);
191
+ setEditingDocumentType(null);
192
+ editForm.reset();
193
+ refetch();
194
+ } catch (error: any) {
195
+ toast.error(error?.message || t('errorUpdate'));
196
+ }
197
+ };
198
+
199
+ const handleDelete = async () => {
200
+ if (deletingId === null) return;
201
+ try {
202
+ await request({
203
+ url: '/person-document-type',
204
+ method: 'DELETE',
205
+ data: { ids: [deletingId] },
206
+ });
207
+ toast.success(t('successDelete'));
208
+ setDeleteDialogOpen(false);
209
+ setDeletingId(null);
210
+ refetch();
211
+ } catch (error: any) {
212
+ toast.error(error?.message || t('errorDelete'));
213
+ }
214
+ };
215
+
216
+ const handleEdit = (documentType: DocumentType) => {
217
+ (async () => {
218
+ setEditingDocumentType(documentType);
219
+ try {
220
+ const { data } = await request<any>({
221
+ url: `/person-document-type/${documentType.document_type_id || documentType.id}?locale=${currentLocaleCode}`,
222
+ method: 'GET',
223
+ });
224
+ editForm.reset({
225
+ code: data.code || documentType.code,
226
+ name: data.name || documentType.name || '',
227
+ country_code: data.country_code || documentType.country_code || '',
228
+ is_unique:
229
+ typeof data.is_unique === 'boolean'
230
+ ? data.is_unique
231
+ : (documentType.is_unique ?? false),
232
+ });
233
+ } catch {
234
+ editForm.reset({
235
+ code: documentType.code,
236
+ name: documentType.name || '',
237
+ country_code: documentType.country_code || '',
238
+ is_unique: documentType.is_unique ?? false,
239
+ });
240
+ }
241
+ setIsEditDialogOpen(true);
242
+ })();
243
+ };
244
+
245
+ return (
246
+ <div className="flex flex-col h-screen px-4">
247
+ <PageHeader
248
+ breadcrumbs={[
249
+ { label: t('breadcrumbContact'), href: '/contact' },
250
+ { label: t('breadcrumbTitle') },
251
+ ]}
252
+ title={t('pageTitle')}
253
+ description={t('pageDescription')}
254
+ actions={[
255
+ {
256
+ label: t('buttonNewType'),
257
+ onClick: () => setIsDialogOpen(true),
258
+ variant: 'default',
259
+ icon: <Plus />,
260
+ },
261
+ ]}
262
+ />
263
+
264
+ <div className="mb-4">
265
+ <SearchBar
266
+ searchQuery={searchQuery}
267
+ onSearchChange={setSearchQuery}
268
+ onSearch={() => refetch()}
269
+ placeholder={t('searchPlaceholder')}
270
+ />
271
+ </div>
272
+
273
+ <div className="rounded-md border mb-4">
274
+ <Table>
275
+ <TableHeader>
276
+ <TableRow>
277
+ <TableHead>{t('tableSlug')}</TableHead>
278
+ <TableHead>{t('tableName')}</TableHead>
279
+ <TableHead>{t('tableCreatedAt')}</TableHead>
280
+ <TableHead className="w-[70px]"></TableHead>
281
+ </TableRow>
282
+ </TableHeader>
283
+ <TableBody>
284
+ {isLoading ? (
285
+ <TableRow>
286
+ <TableCell colSpan={5} className="text-center">
287
+ {t('loading')}
288
+ </TableCell>
289
+ </TableRow>
290
+ ) : paginate.data.length === 0 ? (
291
+ <TableRow>
292
+ <TableCell colSpan={5} className="text-center">
293
+ {t('noResults')}
294
+ </TableCell>
295
+ </TableRow>
296
+ ) : (
297
+ paginate.data.map((documentType) => (
298
+ <TableRow
299
+ key={documentType.id}
300
+ onDoubleClick={() => handleEdit(documentType)}
301
+ className="cursor-pointer"
302
+ >
303
+ <TableCell className="font-medium">
304
+ {documentType.code}
305
+ </TableCell>
306
+ <TableCell>{documentType.name}</TableCell>
307
+ <TableCell>
308
+ {documentType.created_at
309
+ ? formatDate(
310
+ documentType.created_at,
311
+ getSettingValue,
312
+ currentLocaleCode
313
+ )
314
+ : null}
315
+ </TableCell>
316
+ <TableCell>
317
+ <DropdownMenu>
318
+ <DropdownMenuTrigger asChild>
319
+ <Button variant="ghost" className="size-8 p-0">
320
+ <span className="sr-only">{t('menuOpen')}</span>
321
+ <MoreHorizontal className="size-4" />
322
+ </Button>
323
+ </DropdownMenuTrigger>
324
+ <DropdownMenuContent align="end">
325
+ <DropdownMenuLabel>
326
+ {t('menuActions')}
327
+ </DropdownMenuLabel>
328
+ <DropdownMenuSeparator />
329
+ <DropdownMenuItem
330
+ onClick={() => handleEdit(documentType)}
331
+ >
332
+ <Pencil className="mr-2 size-4" />
333
+ {t('menuEdit')}
334
+ </DropdownMenuItem>
335
+ <DropdownMenuItem
336
+ onClick={() => {
337
+ setDeletingId(
338
+ documentType.document_type_id || documentType.id
339
+ );
340
+ setDeleteDialogOpen(true);
341
+ }}
342
+ className="text-red-600"
343
+ >
344
+ <Trash2 className="mr-2 size-4" />
345
+ {t('menuDelete')}
346
+ </DropdownMenuItem>
347
+ </DropdownMenuContent>
348
+ </DropdownMenu>
349
+ </TableCell>
350
+ </TableRow>
351
+ ))
352
+ )}
353
+ </TableBody>
354
+ </Table>
355
+ </div>
356
+
357
+ <PaginationFooter
358
+ currentPage={page}
359
+ pageSize={pageSize}
360
+ totalItems={paginate.total}
361
+ onPageChange={setPage}
362
+ onPageSizeChange={setPageSize}
363
+ pageSizeOptions={[10, 20, 30, 40, 50]}
364
+ />
365
+
366
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
367
+ <DialogContent className="max-w-md">
368
+ <DialogHeader>
369
+ <DialogTitle>{t('dialogNewTitle')}</DialogTitle>
370
+ <DialogDescription>{t('dialogNewDescription')}</DialogDescription>
371
+ </DialogHeader>
372
+
373
+ <Form {...form}>
374
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
375
+ <FormField
376
+ control={form.control}
377
+ name="code"
378
+ render={({ field }) => (
379
+ <FormItem>
380
+ <FormLabel>{t('formSlugLabel')}</FormLabel>
381
+ <FormControl>
382
+ <Input
383
+ placeholder={t('formSlugPlaceholder')}
384
+ {...field}
385
+ />
386
+ </FormControl>
387
+ <FormMessage />
388
+ </FormItem>
389
+ )}
390
+ />
391
+
392
+ <FormField
393
+ control={form.control}
394
+ name="name"
395
+ render={({ field }) => (
396
+ <FormItem>
397
+ <FormLabel>{t('formNameLabel')}</FormLabel>
398
+ <FormControl>
399
+ <Input
400
+ placeholder={t('formNamePlaceholder')}
401
+ {...field}
402
+ />
403
+ </FormControl>
404
+ <FormMessage />
405
+ </FormItem>
406
+ )}
407
+ />
408
+
409
+ <div className="grid grid-cols-2">
410
+ <FormField
411
+ control={form.control}
412
+ name="country_code"
413
+ render={({ field }) => (
414
+ <FormItem>
415
+ <FormLabel>{t('formCountryLabel')}</FormLabel>
416
+ <FormControl>
417
+ <Select
418
+ value={field.value}
419
+ onValueChange={field.onChange}
420
+ >
421
+ <SelectTrigger>
422
+ <SelectValue
423
+ placeholder={t('formCountryPlaceholder')}
424
+ />
425
+ </SelectTrigger>
426
+ <SelectContent>
427
+ {COUNTRIES.map((country) => (
428
+ <SelectItem
429
+ key={country.code}
430
+ value={country.code}
431
+ >
432
+ {country.name}
433
+ </SelectItem>
434
+ ))}
435
+ </SelectContent>
436
+ </Select>
437
+ </FormControl>
438
+ <FormMessage />
439
+ </FormItem>
440
+ )}
441
+ />
442
+
443
+ <FormField
444
+ control={form.control}
445
+ name="is_unique"
446
+ render={({ field }) => (
447
+ <FormItem>
448
+ <FormLabel>{t('formIsUniqueLabel')}</FormLabel>
449
+ <FormControl>
450
+ <Switch
451
+ checked={field.value}
452
+ onCheckedChange={field.onChange}
453
+ />
454
+ </FormControl>
455
+ <FormMessage />
456
+ </FormItem>
457
+ )}
458
+ />
459
+ </div>
460
+
461
+ <div className="flex justify-end gap-2">
462
+ <Button
463
+ type="button"
464
+ variant="outline"
465
+ onClick={() => setIsDialogOpen(false)}
466
+ >
467
+ {t('buttonCancel')}
468
+ </Button>
469
+ <Button type="submit">{t('buttonCreate')}</Button>
470
+ </div>
471
+ </form>
472
+ </Form>
473
+ </DialogContent>
474
+ </Dialog>
475
+
476
+ <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
477
+ <DialogContent className="max-w-md">
478
+ <DialogHeader>
479
+ <DialogTitle>{t('dialogEditTitle')}</DialogTitle>
480
+ <DialogDescription>{t('dialogEditDescription')}</DialogDescription>
481
+ </DialogHeader>
482
+
483
+ <Form {...editForm}>
484
+ <form
485
+ onSubmit={editForm.handleSubmit(onEditSubmit)}
486
+ className="space-y-4"
487
+ >
488
+ <FormField
489
+ control={editForm.control}
490
+ name="code"
491
+ render={({ field }) => (
492
+ <FormItem>
493
+ <FormLabel>{t('formSlugLabel')}</FormLabel>
494
+ <FormControl>
495
+ <Input
496
+ placeholder={t('formSlugPlaceholder')}
497
+ {...field}
498
+ />
499
+ </FormControl>
500
+ <FormMessage />
501
+ </FormItem>
502
+ )}
503
+ />
504
+
505
+ <FormField
506
+ control={editForm.control}
507
+ name="name"
508
+ render={({ field }) => (
509
+ <FormItem>
510
+ <FormLabel>{t('formNameLabel')}</FormLabel>
511
+ <FormControl>
512
+ <Input
513
+ placeholder={t('formNamePlaceholder')}
514
+ {...field}
515
+ />
516
+ </FormControl>
517
+ <FormMessage />
518
+ </FormItem>
519
+ )}
520
+ />
521
+
522
+ <div className="grid grid-cols-2">
523
+ <FormField
524
+ control={editForm.control}
525
+ name="country_code"
526
+ render={({ field }) => (
527
+ <FormItem>
528
+ <FormLabel>{t('formCountryLabel')}</FormLabel>
529
+ <FormControl>
530
+ <Select
531
+ value={field.value}
532
+ onValueChange={field.onChange}
533
+ >
534
+ <SelectTrigger>
535
+ <SelectValue
536
+ placeholder={t('formCountryPlaceholder')}
537
+ />
538
+ </SelectTrigger>
539
+ <SelectContent>
540
+ {COUNTRIES.map((country) => (
541
+ <SelectItem
542
+ key={country.code}
543
+ value={country.code}
544
+ >
545
+ {country.name}
546
+ </SelectItem>
547
+ ))}
548
+ </SelectContent>
549
+ </Select>
550
+ </FormControl>
551
+ <FormMessage />
552
+ </FormItem>
553
+ )}
554
+ />
555
+
556
+ <FormField
557
+ control={editForm.control}
558
+ name="is_unique"
559
+ render={({ field }) => (
560
+ <FormItem>
561
+ <FormLabel>{t('formIsUniqueLabel')}</FormLabel>
562
+ <FormControl>
563
+ <Switch
564
+ checked={field.value}
565
+ onCheckedChange={field.onChange}
566
+ />
567
+ </FormControl>
568
+ <FormMessage />
569
+ </FormItem>
570
+ )}
571
+ />
572
+ </div>
573
+
574
+ <div className="flex justify-end gap-2">
575
+ <Button
576
+ type="button"
577
+ variant="outline"
578
+ onClick={() => setIsEditDialogOpen(false)}
579
+ >
580
+ {t('buttonCancel')}
581
+ </Button>
582
+ <Button type="submit">{t('buttonUpdate')}</Button>
583
+ </div>
584
+ </form>
585
+ </Form>
586
+ </DialogContent>
587
+ </Dialog>
588
+
589
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
590
+ <DialogContent className="max-w-md">
591
+ <DialogHeader>
592
+ <DialogTitle>{t('dialogDeleteTitle')}</DialogTitle>
593
+ <DialogDescription>
594
+ {t('dialogDeleteDescription')}
595
+ </DialogDescription>
596
+ </DialogHeader>
597
+ <div className="flex justify-end gap-2 mt-4">
598
+ <Button
599
+ type="button"
600
+ variant="outline"
601
+ onClick={() => {
602
+ setDeleteDialogOpen(false);
603
+ setDeletingId(null);
604
+ }}
605
+ >
606
+ {t('buttonCancel')}
607
+ </Button>
608
+ <Button type="button" variant="destructive" onClick={handleDelete}>
609
+ {t('buttonDelete')}
610
+ </Button>
611
+ </div>
612
+ </DialogContent>
613
+ </Dialog>
614
+ </div>
615
+ );
616
+ }