@hed-hog/category 0.0.185 → 0.0.187

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,827 @@
1
+ 'use client';
2
+
3
+ import {
4
+ PageHeader,
5
+ PaginationFooter,
6
+ SearchBar,
7
+ } from '@/components/entity-list';
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
17
+ AlertDialogTrigger,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import { Card, CardContent } from '@/components/ui/card';
22
+ import {
23
+ Dialog,
24
+ DialogContent,
25
+ DialogDescription,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ } from '@/components/ui/dialog';
30
+ import { Input } from '@/components/ui/input';
31
+ import { Label } from '@/components/ui/label';
32
+ import {
33
+ Select,
34
+ SelectContent,
35
+ SelectItem,
36
+ SelectTrigger,
37
+ SelectValue,
38
+ } from '@/components/ui/select';
39
+ import { useDebounce } from '@/hooks/use-debounce';
40
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
41
+ import * as TablerIcons from '@tabler/icons-react';
42
+ import {
43
+ Edit,
44
+ FolderTree,
45
+ Globe,
46
+ Layers,
47
+ Plus,
48
+ Save,
49
+ Tag,
50
+ Trash2,
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
+ type PaginationResult<T> = {
58
+ data: T[];
59
+ total: number;
60
+ page: number;
61
+ pageSize: number;
62
+ };
63
+
64
+ type CategoryLocale = {
65
+ name: string;
66
+ locale_id?: number;
67
+ };
68
+
69
+ type Category = {
70
+ id: number;
71
+ slug: string;
72
+ name: string;
73
+ category_id?: number | null;
74
+ color?: string;
75
+ icon?: string;
76
+ status: 'active' | 'inactive';
77
+ category_locale?: CategoryLocale[];
78
+ };
79
+
80
+ type Locale = {
81
+ id?: number;
82
+ code: string;
83
+ name: string;
84
+ };
85
+
86
+ export default function CategoryPage() {
87
+ const t = useTranslations('category.Category');
88
+ const [categories, setCategories] = useState<Category[]>([]);
89
+ const [selectedCategory, setSelectedCategory] = useState<any | null>(null);
90
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
91
+ const [isNewCategory, setIsNewCategory] = useState(false);
92
+ const [searchTerm, setSearchTerm] = useState('');
93
+ const debouncedSearch = useDebounce(searchTerm);
94
+ const [page, setPage] = useState(1);
95
+ const [pageSize, setPageSize] = useState(10);
96
+ const [statusFilter, setStatusFilter] = useState<string>('all');
97
+ const [parentFilter, setParentFilter] = useState<string>('all');
98
+ const [selectedLocale, setSelectedLocale] = useState<string>('');
99
+ const [fullLocales, setFullLocales] = useState<any[]>([]);
100
+ const { request, locales, currentLocaleCode } = useApp();
101
+
102
+ // Buscar locales completos com id
103
+ useEffect(() => {
104
+ const fetchLocales = async () => {
105
+ try {
106
+ const response: any = await request({
107
+ url: '/locale',
108
+ params: { pageSize: 100 },
109
+ });
110
+ if (response.data?.data) {
111
+ setFullLocales(response.data.data);
112
+ }
113
+ } catch (error) {
114
+ console.error('Erro ao buscar locales:', error);
115
+ }
116
+ };
117
+ fetchLocales();
118
+ }, []);
119
+
120
+ useEffect(() => {
121
+ if (currentLocaleCode && !selectedLocale) {
122
+ setSelectedLocale(currentLocaleCode);
123
+ }
124
+ }, [currentLocaleCode]);
125
+
126
+ const {
127
+ data: { data, total },
128
+ refetch: refetchCategories,
129
+ } = useQuery<PaginationResult<Category>>({
130
+ queryKey: [
131
+ 'categories',
132
+ debouncedSearch,
133
+ page,
134
+ pageSize,
135
+ statusFilter,
136
+ parentFilter,
137
+ ],
138
+ queryFn: async () => {
139
+ const response = await request({
140
+ url: '/category',
141
+ params: {
142
+ search: debouncedSearch,
143
+ page,
144
+ pageSize,
145
+ status: statusFilter,
146
+ parent: parentFilter,
147
+ },
148
+ });
149
+ return response.data as PaginationResult<Category>;
150
+ },
151
+ initialData: {
152
+ data: [],
153
+ total: 0,
154
+ page: 1,
155
+ pageSize: 10,
156
+ },
157
+ });
158
+
159
+ const { data: rootCategories = [] } = useQuery<Category[]>({
160
+ queryKey: ['root-categories'],
161
+ queryFn: async () => {
162
+ const response = await request({
163
+ url: '/category/root',
164
+ });
165
+ return (response.data || []) as Category[];
166
+ },
167
+ initialData: [],
168
+ enabled: true,
169
+ staleTime: 0,
170
+ refetchOnMount: true,
171
+ });
172
+
173
+ const { data: statsData, refetch: refetchStats } = useQuery<any>({
174
+ queryKey: ['category-stats'],
175
+ queryFn: async () => {
176
+ const response = await request({
177
+ url: '/category/stats',
178
+ });
179
+ return response.data;
180
+ },
181
+ });
182
+
183
+ useEffect(() => {
184
+ if (data) {
185
+ setCategories(data);
186
+ }
187
+ }, [data]);
188
+
189
+ const renderIcon = (iconName?: string) => {
190
+ const toPascalCase = (str: string) =>
191
+ str.replace(/(^\w|-\w)/g, (match) =>
192
+ match.replace('-', '').toUpperCase()
193
+ );
194
+
195
+ const pascalName = toPascalCase(String(iconName));
196
+
197
+ let IconComponent = (TablerIcons as any)[`Icon${pascalName}`];
198
+ if (!IconComponent) {
199
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Filled`];
200
+ }
201
+ if (!IconComponent) {
202
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Circle`];
203
+ }
204
+ if (IconComponent) {
205
+ return <IconComponent className="h-4 w-4" />;
206
+ }
207
+ // fallback para Briefcase se não encontrar
208
+ return <TablerIcons.IconBriefcase className="h-4 w-4" />;
209
+ };
210
+
211
+ const handleNewCategory = (): void => {
212
+ const newCategory: any = {
213
+ slug: '',
214
+ category_id: null,
215
+ color: '#000000',
216
+ icon: '',
217
+ status: 'active',
218
+ locale: {},
219
+ };
220
+
221
+ locales.forEach((locale: Locale) => {
222
+ newCategory.locale[locale.code] = {
223
+ name: '',
224
+ };
225
+ });
226
+
227
+ setSelectedCategory(newCategory);
228
+ setIsNewCategory(true);
229
+ setIsEditDialogOpen(true);
230
+ };
231
+
232
+ const handleEditCategory = async (category: Category): Promise<void> => {
233
+ try {
234
+ const response = await request({
235
+ url: `/category/${category.category_id}`,
236
+ method: 'GET',
237
+ });
238
+
239
+ const categoryData = response.data as any;
240
+ const localeData: Record<string, { name: string }> = {};
241
+
242
+ if (
243
+ categoryData.category_locale &&
244
+ Array.isArray(categoryData.category_locale)
245
+ ) {
246
+ categoryData.category_locale.forEach((cl: any) => {
247
+ const locale = locales.find((l: Locale) => {
248
+ const fullLocale = fullLocales.find(
249
+ (fl: any) => fl.id === cl.locale_id
250
+ );
251
+ return fullLocale?.code === l.code;
252
+ });
253
+
254
+ if (locale) {
255
+ localeData[locale.code] = {
256
+ name: cl.name || '',
257
+ };
258
+ }
259
+ });
260
+ }
261
+
262
+ // Garantir que todos os locales estejam presentes
263
+ locales.forEach((locale: Locale) => {
264
+ if (!localeData[locale.code]) {
265
+ localeData[locale.code] = {
266
+ name: '',
267
+ };
268
+ }
269
+ });
270
+
271
+ setSelectedCategory({
272
+ ...categoryData,
273
+ locale: localeData,
274
+ });
275
+ setIsNewCategory(false);
276
+ setIsEditDialogOpen(true);
277
+ } catch (error) {
278
+ console.error(error);
279
+ toast.error(t('errorLoading'));
280
+ }
281
+ };
282
+
283
+ const handleSaveCategory = async () => {
284
+ if (!selectedCategory) return;
285
+
286
+ const payload = {
287
+ locale: selectedCategory.locale,
288
+ slug: selectedCategory.slug,
289
+ category_id: selectedCategory.category_id || null,
290
+ color: selectedCategory.color,
291
+ icon: selectedCategory.icon,
292
+ status: selectedCategory.status,
293
+ };
294
+
295
+ try {
296
+ if (selectedCategory.id) {
297
+ await request({
298
+ url: `/category/${selectedCategory.id}`,
299
+ method: 'PATCH',
300
+ data: payload,
301
+ });
302
+ toast.success(t('successUpdate'));
303
+ } else {
304
+ await request({
305
+ url: `/category`,
306
+ method: 'POST',
307
+ data: payload,
308
+ });
309
+ toast.success(t('successCreate'));
310
+ }
311
+
312
+ setIsEditDialogOpen(false);
313
+ await refetchCategories();
314
+ await refetchStats();
315
+ } catch (error) {
316
+ console.error(error);
317
+ toast.error(t('errorSave'));
318
+ }
319
+ };
320
+
321
+ const handleDeleteCategory = async (categoryId: number): Promise<void> => {
322
+ try {
323
+ await request({
324
+ url: `/category/${categoryId}`,
325
+ method: 'DELETE',
326
+ });
327
+ toast.success(t('successDelete'));
328
+ await refetchCategories();
329
+ await refetchStats();
330
+ } catch (error) {
331
+ console.error(error);
332
+ toast.error(t('errorDelete'));
333
+ }
334
+ };
335
+
336
+ const handleSearchChange = (value: string): void => {
337
+ setSearchTerm(value);
338
+ };
339
+
340
+ useEffect(() => {
341
+ refetchCategories();
342
+ refetchStats();
343
+ }, [
344
+ isEditDialogOpen,
345
+ debouncedSearch,
346
+ page,
347
+ pageSize,
348
+ statusFilter,
349
+ parentFilter,
350
+ ]);
351
+
352
+ const getStatusBadge = (status: string) => {
353
+ return status === 'active' ? (
354
+ <Badge variant="default" className="bg-green-500">
355
+ {t('active')}
356
+ </Badge>
357
+ ) : (
358
+ <Badge variant="secondary">{t('inactive')}</Badge>
359
+ );
360
+ };
361
+
362
+ return (
363
+ <div className="flex flex-col h-screen px-4">
364
+ <PageHeader
365
+ breadcrumbs={[
366
+ { label: 'Home', href: '/' },
367
+ { label: t('description') },
368
+ ]}
369
+ actions={[
370
+ {
371
+ label: t('newCategory'),
372
+ onClick: () => handleNewCategory(),
373
+ variant: 'default',
374
+ },
375
+ ]}
376
+ title={t('title')}
377
+ description={t('description')}
378
+ />
379
+
380
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
381
+ <Card className="transition-shadow hover:shadow-md p-0">
382
+ <CardContent className="p-4">
383
+ <div className="flex items-center space-x-3">
384
+ <div className="rounded-full bg-blue-100 p-2 dark:bg-blue-900">
385
+ <Layers className="h-6 w-6 text-blue-600 dark:text-blue-400" />
386
+ </div>
387
+ <div>
388
+ <p className="text-sm font-medium text-muted-foreground">
389
+ {t('totalCategories')}
390
+ </p>
391
+ <p className="text-2xl font-bold">{statsData?.total || 0}</p>
392
+ </div>
393
+ </div>
394
+ </CardContent>
395
+ </Card>
396
+
397
+ <Card className="transition-shadow hover:shadow-md p-0">
398
+ <CardContent className="p-4">
399
+ <div className="flex items-center space-x-3">
400
+ <div className="rounded-full bg-green-100 p-2 dark:bg-green-900">
401
+ <Tag className="h-6 w-6 text-green-600 dark:text-green-400" />
402
+ </div>
403
+ <div>
404
+ <p className="text-sm font-medium text-muted-foreground">
405
+ {t('actives')}
406
+ </p>
407
+ <p className="text-2xl font-bold">
408
+ {statsData?.totalActive || 0}
409
+ </p>
410
+ </div>
411
+ </div>
412
+ </CardContent>
413
+ </Card>
414
+
415
+ <Card className="transition-shadow hover:shadow-md p-0">
416
+ <CardContent className="p-4">
417
+ <div className="flex items-center space-x-3">
418
+ <div className="rounded-full bg-orange-100 p-2 dark:bg-orange-900">
419
+ <Tag className="h-6 w-6 text-orange-600 dark:text-orange-400" />
420
+ </div>
421
+ <div>
422
+ <p className="text-sm font-medium text-muted-foreground">
423
+ {t('inactives')}
424
+ </p>
425
+ <p className="text-2xl font-bold">
426
+ {statsData?.totalInactive || 0}
427
+ </p>
428
+ </div>
429
+ </div>
430
+ </CardContent>
431
+ </Card>
432
+
433
+ <Card className="transition-shadow hover:shadow-md p-0">
434
+ <CardContent className="p-4">
435
+ <div className="flex items-center space-x-3">
436
+ <div className="rounded-full bg-purple-100 p-2 dark:bg-purple-900">
437
+ <FolderTree className="h-6 w-6 text-purple-600 dark:text-purple-400" />
438
+ </div>
439
+ <div>
440
+ <p className="text-sm font-medium text-muted-foreground">
441
+ {t('root')}
442
+ </p>
443
+ <p className="text-2xl font-bold">
444
+ {statsData?.totalRoot || 0}
445
+ </p>
446
+ </div>
447
+ </div>
448
+ </CardContent>
449
+ </Card>
450
+ </div>
451
+
452
+ <div className="flex flex-col gap-4 sm:flex-row my-4">
453
+ <SearchBar
454
+ searchQuery={searchTerm}
455
+ onSearchChange={handleSearchChange}
456
+ onSearch={() => refetchCategories()}
457
+ placeholder={t('searchPlaceholder')}
458
+ />
459
+
460
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
461
+ <SelectTrigger className="w-full sm:w-[180px]">
462
+ <SelectValue placeholder={t('status')} />
463
+ </SelectTrigger>
464
+ <SelectContent>
465
+ <SelectItem value="all">{t('all')}</SelectItem>
466
+ <SelectItem value="active">{t('actives')}</SelectItem>
467
+ <SelectItem value="inactive">{t('inactives')}</SelectItem>
468
+ </SelectContent>
469
+ </Select>
470
+ <Select value={parentFilter} onValueChange={setParentFilter}>
471
+ <SelectTrigger className="w-full sm:w-[180px]">
472
+ <SelectValue placeholder={t('hierarchy')} />
473
+ </SelectTrigger>
474
+ <SelectContent>
475
+ <SelectItem value="all">{t('allHierarchy')}</SelectItem>
476
+ <SelectItem value="root">{t('rootHierarchy')}</SelectItem>
477
+ </SelectContent>
478
+ </Select>
479
+ </div>
480
+
481
+ <div className="space-y-4">
482
+ {categories.length > 0 ? (
483
+ <div className="space-y-4">
484
+ {categories.map((category) => (
485
+ <Card
486
+ key={category.id}
487
+ onDoubleClick={() => handleEditCategory(category)}
488
+ className="cursor-pointer transition-all duration-200 hover:border-primary/20 hover:shadow-md"
489
+ >
490
+ <CardContent className="p-6">
491
+ <div className="flex items-start justify-between gap-4">
492
+ <div className="flex-1 space-y-3">
493
+ <div className="flex items-start space-x-3">
494
+ <div
495
+ className="mt-1 rounded-full p-2"
496
+ style={{
497
+ backgroundColor: category.color
498
+ ? `${category.color}20`
499
+ : '#00000020',
500
+ }}
501
+ >
502
+ <div style={{ color: category.color || '#000000' }}>
503
+ {renderIcon(category.icon)}
504
+ </div>
505
+ </div>
506
+ <div className="flex-1 space-y-2">
507
+ <div className="flex items-center gap-2 flex-wrap">
508
+ <h3 className="text-lg font-semibold leading-tight">
509
+ {category.name}
510
+ </h3>
511
+ {getStatusBadge(category.status)}
512
+ </div>
513
+ <div className="flex flex-col gap-1 text-sm text-muted-foreground">
514
+ <span>
515
+ <strong>{t('slug')}:</strong> {category.slug}
516
+ </span>
517
+ {category.color && (
518
+ <span className="flex items-center gap-2">
519
+ <strong>{t('color')}:</strong>
520
+ <span
521
+ className="inline-block h-4 w-4 rounded border"
522
+ style={{
523
+ backgroundColor: category.color,
524
+ }}
525
+ />
526
+ {category.color}
527
+ </span>
528
+ )}
529
+ {category.icon && (
530
+ <span>
531
+ <strong>{t('icon')}:</strong> {category.icon}
532
+ </span>
533
+ )}
534
+ </div>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <div className="flex flex-col gap-2">
540
+ <Button
541
+ variant="outline"
542
+ size="sm"
543
+ onClick={() => handleEditCategory(category)}
544
+ className="transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950"
545
+ >
546
+ <Edit className="mr-1 h-4 w-4" />
547
+ {t('edit')}
548
+ </Button>
549
+
550
+ <AlertDialog>
551
+ <AlertDialogTrigger asChild>
552
+ <Button
553
+ variant="outline"
554
+ size="sm"
555
+ className="bg-transparent transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950"
556
+ >
557
+ <Trash2 className="mr-1 h-4 w-4" />
558
+ {t('delete')}
559
+ </Button>
560
+ </AlertDialogTrigger>
561
+ <AlertDialogContent>
562
+ <AlertDialogHeader>
563
+ <AlertDialogTitle>
564
+ {t('confirmDelete')}
565
+ </AlertDialogTitle>
566
+ <AlertDialogDescription>
567
+ {t('deleteDescription')}
568
+ </AlertDialogDescription>
569
+ </AlertDialogHeader>
570
+ <AlertDialogFooter>
571
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
572
+ <AlertDialogAction
573
+ onClick={() =>
574
+ handleDeleteCategory(
575
+ Number(category.category_id)
576
+ )
577
+ }
578
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
579
+ >
580
+ {t('delete')}
581
+ </AlertDialogAction>
582
+ </AlertDialogFooter>
583
+ </AlertDialogContent>
584
+ </AlertDialog>
585
+ </div>
586
+ </div>
587
+ </CardContent>
588
+ </Card>
589
+ ))}
590
+ </div>
591
+ ) : (
592
+ <Card>
593
+ <CardContent className="p-12 text-center">
594
+ <div className="flex flex-col items-center space-y-4">
595
+ <Layers className="h-12 w-12 text-muted-foreground" />
596
+ <div>
597
+ <h3 className="text-lg font-semibold">
598
+ {t('noCategoriesFound')}
599
+ </h3>
600
+ <p className="text-muted-foreground">{t('adjustFilters')}</p>
601
+ </div>
602
+ <Button onClick={handleNewCategory}>
603
+ <Plus className="mr-2 h-4 w-4" />
604
+ {t('createFirstCategory')}
605
+ </Button>
606
+ </div>
607
+ </CardContent>
608
+ </Card>
609
+ )}
610
+
611
+ <PaginationFooter
612
+ currentPage={page}
613
+ pageSize={pageSize}
614
+ totalItems={total}
615
+ onPageChange={setPage}
616
+ onPageSizeChange={setPageSize}
617
+ pageSizeOptions={[10, 20, 30, 40, 50]}
618
+ />
619
+ </div>
620
+
621
+ <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
622
+ <DialogContent className="max-h-[95vh] max-w-2xl overflow-y-auto">
623
+ <DialogHeader>
624
+ <DialogTitle className="flex items-center space-x-2">
625
+ <Edit className="h-5 w-5" />
626
+ <span>
627
+ {isNewCategory ? t('newCategoryTitle') : t('editCategory')}
628
+ </span>
629
+ </DialogTitle>
630
+ <DialogDescription>
631
+ {isNewCategory ? t('createDescription') : t('editDescription')}
632
+ </DialogDescription>
633
+ </DialogHeader>
634
+
635
+ {selectedCategory && (
636
+ <div className="space-y-4">
637
+ <div className="space-y-2">
638
+ <Label
639
+ htmlFor="locale-select"
640
+ className="flex items-center gap-2"
641
+ >
642
+ <Globe className="h-4 w-4" />
643
+ {t('language')}
644
+ </Label>
645
+ <Select
646
+ value={selectedLocale}
647
+ onValueChange={setSelectedLocale}
648
+ >
649
+ <SelectTrigger id="locale-select">
650
+ <SelectValue placeholder={t('selectLanguage')} />
651
+ </SelectTrigger>
652
+ <SelectContent>
653
+ {locales.map((locale: Locale) => (
654
+ <SelectItem key={locale.code} value={locale.code}>
655
+ {locale.name}
656
+ </SelectItem>
657
+ ))}
658
+ </SelectContent>
659
+ </Select>
660
+ </div>
661
+
662
+ <div className="space-y-2">
663
+ <Label htmlFor="name">{t('nameRequired')}</Label>
664
+ <Input
665
+ id="name"
666
+ placeholder={t('namePlaceholder')}
667
+ value={selectedCategory.locale?.[selectedLocale]?.name || ''}
668
+ onChange={(e) =>
669
+ setSelectedCategory({
670
+ ...selectedCategory,
671
+ locale: {
672
+ ...selectedCategory.locale,
673
+ [selectedLocale]: {
674
+ ...selectedCategory.locale?.[selectedLocale],
675
+ name: e.target.value,
676
+ },
677
+ },
678
+ })
679
+ }
680
+ />
681
+ </div>
682
+
683
+ <div className="space-y-2">
684
+ <Label htmlFor="slug">{t('slugRequired')}</Label>
685
+ <Input
686
+ id="slug"
687
+ placeholder={t('slugPlaceholder')}
688
+ value={selectedCategory.slug || ''}
689
+ onChange={(e) =>
690
+ setSelectedCategory({
691
+ ...selectedCategory,
692
+ slug: e.target.value,
693
+ })
694
+ }
695
+ />
696
+ </div>
697
+
698
+ <div className="space-y-2">
699
+ <Label htmlFor="color">{t('color')}</Label>
700
+ <div className="flex gap-2">
701
+ <Input
702
+ id="color"
703
+ type="color"
704
+ className="h-10 w-20"
705
+ value={selectedCategory.color || '#000000'}
706
+ onChange={(e) =>
707
+ setSelectedCategory({
708
+ ...selectedCategory,
709
+ color: e.target.value,
710
+ })
711
+ }
712
+ />
713
+ <Input
714
+ placeholder={t('colorPlaceholder')}
715
+ value={selectedCategory.color || '#000000'}
716
+ onChange={(e) =>
717
+ setSelectedCategory({
718
+ ...selectedCategory,
719
+ color: e.target.value,
720
+ })
721
+ }
722
+ />
723
+ </div>
724
+ </div>
725
+
726
+ <div className="space-y-2">
727
+ <Label htmlFor="icon">{t('icon')}</Label>
728
+ <div className="flex gap-2 items-center">
729
+ <div className="shrink-0">
730
+ {renderIcon(selectedCategory.icon)}
731
+ </div>
732
+ <Input
733
+ id="icon"
734
+ placeholder={t('iconPlaceholder')}
735
+ value={selectedCategory.icon || ''}
736
+ onChange={(e) =>
737
+ setSelectedCategory({
738
+ ...selectedCategory,
739
+ icon: e.target.value,
740
+ })
741
+ }
742
+ />
743
+ </div>
744
+ </div>
745
+
746
+ <div className="space-y-2">
747
+ <Label htmlFor="parent">{t('parentCategory')}</Label>
748
+ <Select
749
+ value={selectedCategory.category_id?.toString() || 'none'}
750
+ onValueChange={(value) =>
751
+ setSelectedCategory({
752
+ ...selectedCategory,
753
+ category_id: value === 'none' ? null : Number(value),
754
+ })
755
+ }
756
+ >
757
+ <SelectTrigger className="w-full" id="parent">
758
+ <SelectValue placeholder={t('selectParent')} />
759
+ </SelectTrigger>
760
+ <SelectContent>
761
+ <SelectItem value="none">{t('noneRoot')}</SelectItem>
762
+ {Array.isArray(rootCategories) &&
763
+ rootCategories.map((cat: Category) => (
764
+ <SelectItem
765
+ key={cat.id}
766
+ value={String(cat.category_id)}
767
+ >
768
+ {cat.name}
769
+ </SelectItem>
770
+ ))}
771
+ </SelectContent>
772
+ </Select>
773
+ </div>
774
+
775
+ <div className="space-y-2">
776
+ <Label htmlFor="status">{t('status')}</Label>
777
+ <Select
778
+ value={selectedCategory.status}
779
+ onValueChange={(value) =>
780
+ setSelectedCategory({
781
+ ...selectedCategory,
782
+ status: value,
783
+ })
784
+ }
785
+ >
786
+ <SelectTrigger className="w-full" id="status">
787
+ <SelectValue placeholder={t('selectStatus')} />
788
+ </SelectTrigger>
789
+ <SelectContent>
790
+ <SelectItem value="active">{t('active')}</SelectItem>
791
+ <SelectItem value="inactive">{t('inactive')}</SelectItem>
792
+ </SelectContent>
793
+ </Select>
794
+ </div>
795
+ </div>
796
+ )}
797
+
798
+ <DialogFooter className="mt-4">
799
+ <Button
800
+ variant="outline"
801
+ onClick={() => {
802
+ setIsEditDialogOpen(false);
803
+ setSelectedCategory(null);
804
+ setIsNewCategory(false);
805
+ }}
806
+ >
807
+ <X className="mr-2 h-4 w-4" />
808
+ {t('cancel')}
809
+ </Button>
810
+ <Button
811
+ onClick={handleSaveCategory}
812
+ disabled={
813
+ !selectedCategory?.slug ||
814
+ !selectedCategory?.locale ||
815
+ !Object.values(selectedCategory.locale).some((l: any) => l.name)
816
+ }
817
+ className="transition-colors hover:bg-primary/90"
818
+ >
819
+ <Save className="mr-2 h-4 w-4" />
820
+ {isNewCategory ? t('createCategoryButton') : t('saveChanges')}
821
+ </Button>
822
+ </DialogFooter>
823
+ </DialogContent>
824
+ </Dialog>
825
+ </div>
826
+ );
827
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "Category": {
3
+ "title": "Categories",
4
+ "description": "Manage all system categories",
5
+ "newCategory": "New Category",
6
+ "totalCategories": "Total Categories",
7
+ "active": "Active",
8
+ "inactive": "Inactive",
9
+ "root": "Root",
10
+ "searchPlaceholder": "Search by name or slug...",
11
+ "status": "Status",
12
+ "all": "All",
13
+ "actives": "Active",
14
+ "inactives": "Inactive",
15
+ "hierarchy": "Hierarchy",
16
+ "allHierarchy": "All",
17
+ "rootHierarchy": "Root",
18
+ "slug": "Slug",
19
+ "color": "Color",
20
+ "icon": "Icon",
21
+ "edit": "Edit",
22
+ "delete": "Delete",
23
+ "confirmDelete": "Confirm Deletion",
24
+ "deleteDescription": "Are you sure you want to delete this category? This action cannot be undone.",
25
+ "cancel": "Cancel",
26
+ "noCategoriesFound": "No categories found",
27
+ "adjustFilters": "Try adjusting the filters or create a new category.",
28
+ "createFirstCategory": "Create First Category",
29
+ "editCategory": "Edit Category",
30
+ "newCategoryTitle": "New Category",
31
+ "editDescription": "Edit the category information",
32
+ "createDescription": "Create a new category",
33
+ "language": "Language",
34
+ "selectLanguage": "Select a language",
35
+ "name": "Name",
36
+ "nameRequired": "Name *",
37
+ "namePlaceholder": "Enter the category name",
38
+ "slugRequired": "Slug *",
39
+ "slugPlaceholder": "Enter the slug (e.g., my-category)",
40
+ "colorPlaceholder": "#000000",
41
+ "iconPlaceholder": "Enter the icon name (e.g., Home, Star, Heart)",
42
+ "parentCategory": "Parent Category",
43
+ "selectParent": "Select the parent category",
44
+ "noneRoot": "None (Root)",
45
+ "selectStatus": "Select the status",
46
+ "createCategoryButton": "Create Category",
47
+ "saveChanges": "Save Changes",
48
+ "errorLoading": "Error loading category data.",
49
+ "successUpdate": "Category updated successfully!",
50
+ "successCreate": "Category created successfully!",
51
+ "errorSave": "Error saving category.",
52
+ "successDelete": "Category deleted successfully!",
53
+ "errorDelete": "Error deleting category."
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "Category": {
3
+ "title": "Categorias",
4
+ "description": "Gerencie todas as categorias do sistema",
5
+ "newCategory": "Nova Categoria",
6
+ "totalCategories": "Total de Categorias",
7
+ "active": "Ativo",
8
+ "inactive": "Inativo",
9
+ "root": "Raiz",
10
+ "searchPlaceholder": "Buscar por nome ou slug...",
11
+ "status": "Status",
12
+ "all": "Todos",
13
+ "actives": "Ativos",
14
+ "inactives": "Inativos",
15
+ "hierarchy": "Hierarquia",
16
+ "allHierarchy": "Todas",
17
+ "rootHierarchy": "Raiz",
18
+ "slug": "Slug",
19
+ "color": "Cor",
20
+ "icon": "Ícone",
21
+ "edit": "Editar",
22
+ "delete": "Excluir",
23
+ "confirmDelete": "Confirmar Exclusão",
24
+ "deleteDescription": "Tem certeza que deseja excluir esta categoria? Esta ação não pode ser desfeita.",
25
+ "cancel": "Cancelar",
26
+ "noCategoriesFound": "Nenhuma categoria encontrada",
27
+ "adjustFilters": "Tente ajustar os filtros ou criar uma nova categoria.",
28
+ "createFirstCategory": "Criar Primeira Categoria",
29
+ "editCategory": "Editar Categoria",
30
+ "newCategoryTitle": "Nova Categoria",
31
+ "editDescription": "Edite as informações da categoria",
32
+ "createDescription": "Crie uma nova categoria",
33
+ "language": "Idioma",
34
+ "selectLanguage": "Selecione um idioma",
35
+ "name": "Nome",
36
+ "nameRequired": "Nome *",
37
+ "namePlaceholder": "Digite o nome da categoria",
38
+ "slugRequired": "Slug *",
39
+ "slugPlaceholder": "Digite o slug (ex: minha-categoria)",
40
+ "colorPlaceholder": "#000000",
41
+ "iconPlaceholder": "Digite o nome do ícone (ex: Home, Star, Heart)",
42
+ "parentCategory": "Categoria Pai",
43
+ "selectParent": "Selecione a categoria pai",
44
+ "noneRoot": "Nenhuma (Raiz)",
45
+ "selectStatus": "Selecione o status",
46
+ "createCategoryButton": "Criar Categoria",
47
+ "saveChanges": "Salvar Alterações",
48
+ "errorLoading": "Erro ao carregar os dados da categoria.",
49
+ "successUpdate": "Categoria atualizada com sucesso!",
50
+ "successCreate": "Categoria criada com sucesso!",
51
+ "errorSave": "Erro ao salvar a categoria.",
52
+ "successDelete": "Categoria excluída com sucesso!",
53
+ "errorDelete": "Erro ao excluir a categoria."
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/category",
3
- "version": "0.0.185",
3
+ "version": "0.0.187",
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-pagination": "0.0.5",
13
+ "@hed-hog/api-locale": "0.0.11",
14
14
  "@hed-hog/api-prisma": "0.0.4",
15
- "@hed-hog/core": "0.0.185",
16
- "@hed-hog/api": "0.0.3"
15
+ "@hed-hog/api": "0.0.3",
16
+ "@hed-hog/core": "0.0.186"
17
17
  },
18
18
  "exports": {
19
19
  ".": {