@hed-hog/contact 0.0.186 → 0.0.191

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,480 @@
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
+ Table,
35
+ TableBody,
36
+ TableCell,
37
+ TableHead,
38
+ TableHeader,
39
+ TableRow,
40
+ } from '@/components/ui/table';
41
+ import { formatDate } from '@/lib/format-date';
42
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
43
+ import { zodResolver } from '@hookform/resolvers/zod';
44
+ import { MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';
45
+ import { useTranslations } from 'next-intl';
46
+ import { useState } from 'react';
47
+ import { useForm } from 'react-hook-form';
48
+ import { toast } from 'sonner';
49
+ import { z } from 'zod';
50
+
51
+ type PaginatedResult<T> = {
52
+ data: T[];
53
+ total: number;
54
+ page: number;
55
+ pageSize: number;
56
+ };
57
+
58
+ type AddressType = {
59
+ address_type_id?: number;
60
+ id: number;
61
+ code: string;
62
+ name: string;
63
+ created_at: string;
64
+ };
65
+
66
+ export default function AddressTypePage() {
67
+ const t = useTranslations('contact.AddressType');
68
+
69
+ const addressTypeSchema = z.object({
70
+ code: z.string().min(2, t('errorCode')),
71
+ name: z.string().min(2, t('errorName')),
72
+ });
73
+ const { request, currentLocaleCode, getSettingValue } = useApp();
74
+
75
+ const [searchQuery, setSearchQuery] = useState('');
76
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
77
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
78
+ const [editingAddressType, setEditingAddressType] =
79
+ useState<AddressType | null>(null);
80
+
81
+ const [page, setPage] = useState(1);
82
+ const [pageSize, setPageSize] = useState(12);
83
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
84
+ const [deletingId, setDeletingId] = useState<number | null>(null);
85
+
86
+ const {
87
+ data: paginate = { data: [], total: 0, page: 1, pageSize: 12 },
88
+ isLoading,
89
+ refetch,
90
+ } = useQuery<PaginatedResult<AddressType>>({
91
+ queryKey: ['address-types', page, pageSize, searchQuery, currentLocaleCode],
92
+ queryFn: async () => {
93
+ const params = new URLSearchParams();
94
+ params.set('page', String(page));
95
+ params.set('pageSize', String(pageSize));
96
+ if (searchQuery) params.set('search', searchQuery);
97
+
98
+ const response = await request<PaginatedResult<AddressType>>({
99
+ url: `/person-address-type?${params.toString()}`,
100
+ method: 'GET',
101
+ });
102
+
103
+ return response.data;
104
+ },
105
+ });
106
+
107
+ const form = useForm<z.infer<typeof addressTypeSchema>>({
108
+ resolver: zodResolver(addressTypeSchema),
109
+ defaultValues: {
110
+ code: '',
111
+ name: '',
112
+ },
113
+ });
114
+
115
+ const editForm = useForm<z.infer<typeof addressTypeSchema>>({
116
+ resolver: zodResolver(addressTypeSchema),
117
+ });
118
+
119
+ const onSubmit = async (values: z.infer<typeof addressTypeSchema>) => {
120
+ try {
121
+ const payload = {
122
+ code: values.code,
123
+ locale: {
124
+ [currentLocaleCode]: {
125
+ name: values.name,
126
+ },
127
+ },
128
+ };
129
+
130
+ await request({
131
+ url: '/person-address-type',
132
+ method: 'POST',
133
+ data: payload,
134
+ });
135
+
136
+ toast.success(t('successCreate'));
137
+ setIsDialogOpen(false);
138
+ form.reset();
139
+ refetch();
140
+ } catch (error: any) {
141
+ toast.error(error?.message || t('errorCreate'));
142
+ }
143
+ };
144
+
145
+ const onEditSubmit = async (values: z.infer<typeof addressTypeSchema>) => {
146
+ if (!editingAddressType) return;
147
+
148
+ try {
149
+ const payload = {
150
+ code: values.code,
151
+ locale: {
152
+ [currentLocaleCode]: {
153
+ name: values.name,
154
+ },
155
+ },
156
+ };
157
+
158
+ await request({
159
+ url: `/person-address-type/${editingAddressType.address_type_id || editingAddressType.id}`,
160
+ method: 'PATCH',
161
+ data: payload,
162
+ });
163
+
164
+ toast.success(t('successUpdate'));
165
+ setIsEditDialogOpen(false);
166
+ setEditingAddressType(null);
167
+ editForm.reset();
168
+ refetch();
169
+ } catch (error: any) {
170
+ toast.error(error?.message || t('errorUpdate'));
171
+ }
172
+ };
173
+
174
+ const handleDelete = async () => {
175
+ if (deletingId === null) return;
176
+ try {
177
+ await request({
178
+ url: '/person-address-type',
179
+ method: 'DELETE',
180
+ data: { ids: [deletingId] },
181
+ });
182
+ toast.success(t('successDelete'));
183
+ setDeleteDialogOpen(false);
184
+ setDeletingId(null);
185
+ refetch();
186
+ } catch (error: any) {
187
+ toast.error(error?.message || t('errorDelete'));
188
+ }
189
+ };
190
+
191
+ const handleEdit = (addressType: AddressType) => {
192
+ (async () => {
193
+ setEditingAddressType(addressType);
194
+ try {
195
+ const { data } = await request<any>({
196
+ url: `/person-address-type/${addressType.address_type_id || addressType.id}?locale=${currentLocaleCode}`,
197
+ method: 'GET',
198
+ });
199
+ editForm.reset({
200
+ code: data.code || addressType.code,
201
+ name: data.name || addressType.name || '',
202
+ });
203
+ } catch {
204
+ editForm.reset({
205
+ code: addressType.code,
206
+ name: addressType.name || '',
207
+ });
208
+ }
209
+ setIsEditDialogOpen(true);
210
+ })();
211
+ };
212
+
213
+ return (
214
+ <div className="flex flex-col h-screen px-4">
215
+ <PageHeader
216
+ breadcrumbs={[
217
+ { label: t('breadcrumbContact'), href: '/contact' },
218
+ { label: t('breadcrumbTitle') },
219
+ ]}
220
+ title={t('pageTitle')}
221
+ description={t('pageDescription')}
222
+ actions={[
223
+ {
224
+ label: t('buttonNewType'),
225
+ onClick: () => setIsDialogOpen(true),
226
+ variant: 'default',
227
+ icon: <Plus />,
228
+ },
229
+ ]}
230
+ />
231
+
232
+ <div className="mb-4">
233
+ <SearchBar
234
+ searchQuery={searchQuery}
235
+ onSearchChange={setSearchQuery}
236
+ onSearch={() => refetch()}
237
+ placeholder={t('searchPlaceholder')}
238
+ />
239
+ </div>
240
+
241
+ <div className="rounded-md border mb-4">
242
+ <Table>
243
+ <TableHeader>
244
+ <TableRow>
245
+ <TableHead>{t('tableSlug')}</TableHead>
246
+ <TableHead>{t('tableName')}</TableHead>
247
+ <TableHead>{t('tableCreatedAt')}</TableHead>
248
+ <TableHead className="w-[70px]"></TableHead>
249
+ </TableRow>
250
+ </TableHeader>
251
+ <TableBody>
252
+ {isLoading ? (
253
+ <TableRow>
254
+ <TableCell colSpan={5} className="text-center">
255
+ {t('loading')}
256
+ </TableCell>
257
+ </TableRow>
258
+ ) : paginate.data.length === 0 ? (
259
+ <TableRow>
260
+ <TableCell colSpan={5} className="text-center">
261
+ {t('noResults')}
262
+ </TableCell>
263
+ </TableRow>
264
+ ) : (
265
+ paginate.data.map((addressType) => (
266
+ <TableRow
267
+ key={addressType.id}
268
+ onDoubleClick={() => handleEdit(addressType)}
269
+ className="cursor-pointer"
270
+ >
271
+ <TableCell className="font-medium">
272
+ {addressType.code}
273
+ </TableCell>
274
+ <TableCell>{addressType.name}</TableCell>
275
+ <TableCell>
276
+ {addressType.created_at
277
+ ? formatDate(
278
+ addressType.created_at,
279
+ getSettingValue,
280
+ currentLocaleCode
281
+ )
282
+ : null}
283
+ </TableCell>
284
+ <TableCell>
285
+ <DropdownMenu>
286
+ <DropdownMenuTrigger asChild>
287
+ <Button variant="ghost" className="size-8 p-0">
288
+ <span className="sr-only">{t('menuOpen')}</span>
289
+ <MoreHorizontal className="size-4" />
290
+ </Button>
291
+ </DropdownMenuTrigger>
292
+ <DropdownMenuContent align="end">
293
+ <DropdownMenuLabel>
294
+ {t('menuActions')}
295
+ </DropdownMenuLabel>
296
+ <DropdownMenuSeparator />
297
+ <DropdownMenuItem
298
+ onClick={() => handleEdit(addressType)}
299
+ >
300
+ <Pencil className="mr-2 size-4" />
301
+ {t('menuEdit')}
302
+ </DropdownMenuItem>
303
+ <DropdownMenuItem
304
+ onClick={() => {
305
+ setDeletingId(
306
+ addressType.address_type_id || addressType.id
307
+ );
308
+ setDeleteDialogOpen(true);
309
+ }}
310
+ className="text-red-600"
311
+ >
312
+ <Trash2 className="mr-2 size-4" />
313
+ {t('menuDelete')}
314
+ </DropdownMenuItem>
315
+ </DropdownMenuContent>
316
+ </DropdownMenu>
317
+ </TableCell>
318
+ </TableRow>
319
+ ))
320
+ )}
321
+ </TableBody>
322
+ </Table>
323
+ </div>
324
+
325
+ <PaginationFooter
326
+ currentPage={page}
327
+ pageSize={pageSize}
328
+ totalItems={paginate.total}
329
+ onPageChange={setPage}
330
+ onPageSizeChange={setPageSize}
331
+ pageSizeOptions={[10, 20, 30, 40, 50]}
332
+ />
333
+
334
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
335
+ <DialogContent className="max-w-md">
336
+ <DialogHeader>
337
+ <DialogTitle>{t('dialogNewTitle')}</DialogTitle>
338
+ <DialogDescription>{t('dialogNewDescription')}</DialogDescription>
339
+ </DialogHeader>
340
+
341
+ <Form {...form}>
342
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
343
+ <FormField
344
+ control={form.control}
345
+ name="code"
346
+ render={({ field }) => (
347
+ <FormItem>
348
+ <FormLabel>{t('formSlugLabel')}</FormLabel>
349
+ <FormControl>
350
+ <Input
351
+ placeholder={t('formSlugPlaceholder')}
352
+ {...field}
353
+ />
354
+ </FormControl>
355
+ <FormMessage />
356
+ </FormItem>
357
+ )}
358
+ />
359
+
360
+ <FormField
361
+ control={form.control}
362
+ name="name"
363
+ render={({ field }) => (
364
+ <FormItem>
365
+ <FormLabel>{t('formNameLabel')}</FormLabel>
366
+ <FormControl>
367
+ <Input
368
+ placeholder={t('formNamePlaceholderNew')}
369
+ {...field}
370
+ />
371
+ </FormControl>
372
+ <FormMessage />
373
+ </FormItem>
374
+ )}
375
+ />
376
+
377
+ <div className="flex justify-end gap-2">
378
+ <Button
379
+ type="button"
380
+ variant="outline"
381
+ onClick={() => setIsDialogOpen(false)}
382
+ >
383
+ {t('buttonCancel')}
384
+ </Button>
385
+ <Button type="submit">{t('buttonCreate')}</Button>
386
+ </div>
387
+ </form>
388
+ </Form>
389
+ </DialogContent>
390
+ </Dialog>
391
+
392
+ <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
393
+ <DialogContent className="max-w-md">
394
+ <DialogHeader>
395
+ <DialogTitle>{t('dialogEditTitle')}</DialogTitle>
396
+ <DialogDescription>{t('dialogEditDescription')}</DialogDescription>
397
+ </DialogHeader>
398
+
399
+ <Form {...editForm}>
400
+ <form
401
+ onSubmit={editForm.handleSubmit(onEditSubmit)}
402
+ className="space-y-4"
403
+ >
404
+ <FormField
405
+ control={editForm.control}
406
+ name="code"
407
+ render={({ field }) => (
408
+ <FormItem>
409
+ <FormLabel>{t('formSlugLabel')}</FormLabel>
410
+ <FormControl>
411
+ <Input
412
+ placeholder={t('formSlugPlaceholder')}
413
+ {...field}
414
+ />
415
+ </FormControl>
416
+ <FormMessage />
417
+ </FormItem>
418
+ )}
419
+ />
420
+
421
+ <FormField
422
+ control={editForm.control}
423
+ name="name"
424
+ render={({ field }) => (
425
+ <FormItem>
426
+ <FormLabel>{t('formNameLabel')}</FormLabel>
427
+ <FormControl>
428
+ <Input
429
+ placeholder={t('formNamePlaceholderEdit')}
430
+ {...field}
431
+ />
432
+ </FormControl>
433
+ <FormMessage />
434
+ </FormItem>
435
+ )}
436
+ />
437
+
438
+ <div className="flex justify-end gap-2">
439
+ <Button
440
+ type="button"
441
+ variant="outline"
442
+ onClick={() => setIsEditDialogOpen(false)}
443
+ >
444
+ {t('buttonCancel')}
445
+ </Button>
446
+ <Button type="submit">{t('buttonUpdate')}</Button>
447
+ </div>
448
+ </form>
449
+ </Form>
450
+ </DialogContent>
451
+ </Dialog>
452
+
453
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
454
+ <DialogContent className="max-w-md">
455
+ <DialogHeader>
456
+ <DialogTitle>{t('dialogDeleteTitle')}</DialogTitle>
457
+ <DialogDescription>
458
+ {t('dialogDeleteDescription')}
459
+ </DialogDescription>
460
+ </DialogHeader>
461
+ <div className="flex justify-end gap-2 mt-4">
462
+ <Button
463
+ type="button"
464
+ variant="outline"
465
+ onClick={() => {
466
+ setDeleteDialogOpen(false);
467
+ setDeletingId(null);
468
+ }}
469
+ >
470
+ {t('buttonCancel')}
471
+ </Button>
472
+ <Button type="button" variant="destructive" onClick={handleDelete}>
473
+ {t('buttonDelete')}
474
+ </Button>
475
+ </div>
476
+ </DialogContent>
477
+ </Dialog>
478
+ </div>
479
+ );
480
+ }