@hed-hog/finance 0.0.235 → 0.0.236

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.
Files changed (72) hide show
  1. package/dist/dto/create-cost-center.dto.d.ts +4 -0
  2. package/dist/dto/create-cost-center.dto.d.ts.map +1 -0
  3. package/dist/dto/create-cost-center.dto.js +24 -0
  4. package/dist/dto/create-cost-center.dto.js.map +1 -0
  5. package/dist/dto/create-finance-category.dto.d.ts +6 -0
  6. package/dist/dto/create-finance-category.dto.d.ts.map +1 -0
  7. package/dist/dto/create-finance-category.dto.js +37 -0
  8. package/dist/dto/create-finance-category.dto.js.map +1 -0
  9. package/dist/dto/create-period-close.dto.d.ts +7 -0
  10. package/dist/dto/create-period-close.dto.d.ts.map +1 -0
  11. package/dist/dto/create-period-close.dto.js +44 -0
  12. package/dist/dto/create-period-close.dto.js.map +1 -0
  13. package/dist/dto/move-finance-category.dto.d.ts +5 -0
  14. package/dist/dto/move-finance-category.dto.d.ts.map +1 -0
  15. package/dist/dto/move-finance-category.dto.js +32 -0
  16. package/dist/dto/move-finance-category.dto.js.map +1 -0
  17. package/dist/dto/update-cost-center.dto.d.ts +5 -0
  18. package/dist/dto/update-cost-center.dto.d.ts.map +1 -0
  19. package/dist/dto/update-cost-center.dto.js +32 -0
  20. package/dist/dto/update-cost-center.dto.js.map +1 -0
  21. package/dist/dto/update-finance-category.dto.d.ts +7 -0
  22. package/dist/dto/update-finance-category.dto.d.ts.map +1 -0
  23. package/dist/dto/update-finance-category.dto.js +46 -0
  24. package/dist/dto/update-finance-category.dto.js.map +1 -0
  25. package/dist/finance-audit-logs.controller.d.ts +13 -0
  26. package/dist/finance-audit-logs.controller.d.ts.map +1 -0
  27. package/dist/finance-audit-logs.controller.js +54 -0
  28. package/dist/finance-audit-logs.controller.js.map +1 -0
  29. package/dist/finance-categories.controller.d.ts +42 -0
  30. package/dist/finance-categories.controller.d.ts.map +1 -0
  31. package/dist/finance-categories.controller.js +84 -0
  32. package/dist/finance-categories.controller.js.map +1 -0
  33. package/dist/finance-cost-centers.controller.d.ts +32 -0
  34. package/dist/finance-cost-centers.controller.d.ts.map +1 -0
  35. package/dist/finance-cost-centers.controller.js +72 -0
  36. package/dist/finance-cost-centers.controller.js.map +1 -0
  37. package/dist/finance-period-close.controller.d.ts +27 -0
  38. package/dist/finance-period-close.controller.d.ts.map +1 -0
  39. package/dist/finance-period-close.controller.js +64 -0
  40. package/dist/finance-period-close.controller.js.map +1 -0
  41. package/dist/finance.module.d.ts.map +1 -1
  42. package/dist/finance.module.js +8 -0
  43. package/dist/finance.module.js.map +1 -1
  44. package/dist/finance.service.d.ts +111 -0
  45. package/dist/finance.service.d.ts.map +1 -1
  46. package/dist/finance.service.js +446 -17
  47. package/dist/finance.service.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -0
  51. package/dist/index.js.map +1 -1
  52. package/hedhog/data/route.yaml +108 -0
  53. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +309 -0
  54. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +642 -0
  55. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +371 -0
  56. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +502 -0
  57. package/hedhog/frontend/messages/en.json +225 -0
  58. package/hedhog/frontend/messages/pt.json +225 -0
  59. package/package.json +4 -4
  60. package/src/dto/create-cost-center.dto.ts +9 -0
  61. package/src/dto/create-finance-category.dto.ts +21 -0
  62. package/src/dto/create-period-close.dto.ts +34 -0
  63. package/src/dto/move-finance-category.dto.ts +18 -0
  64. package/src/dto/update-cost-center.dto.ts +17 -0
  65. package/src/dto/update-finance-category.dto.ts +30 -0
  66. package/src/finance-audit-logs.controller.ts +30 -0
  67. package/src/finance-categories.controller.ts +52 -0
  68. package/src/finance-cost-centers.controller.ts +43 -0
  69. package/src/finance-period-close.controller.ts +34 -0
  70. package/src/finance.module.ts +8 -0
  71. package/src/finance.service.ts +578 -9
  72. package/src/index.ts +4 -0
@@ -0,0 +1,642 @@
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from '@/components/ui/alert-dialog';
14
+ import { Badge } from '@/components/ui/badge';
15
+ import { Button } from '@/components/ui/button';
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from '@/components/ui/card';
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormField,
27
+ FormItem,
28
+ FormLabel,
29
+ FormMessage,
30
+ } from '@/components/ui/form';
31
+ import { Input } from '@/components/ui/input';
32
+ import {
33
+ Select,
34
+ SelectContent,
35
+ SelectItem,
36
+ SelectTrigger,
37
+ SelectValue,
38
+ } from '@/components/ui/select';
39
+ import {
40
+ Sheet,
41
+ SheetContent,
42
+ SheetDescription,
43
+ SheetHeader,
44
+ SheetTitle,
45
+ } from '@/components/ui/sheet';
46
+ import {
47
+ DndContext,
48
+ DragEndEvent,
49
+ PointerSensor,
50
+ closestCenter,
51
+ useSensor,
52
+ useSensors,
53
+ } from '@dnd-kit/core';
54
+ import {
55
+ SortableContext,
56
+ arrayMove,
57
+ useSortable,
58
+ verticalListSortingStrategy,
59
+ } from '@dnd-kit/sortable';
60
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
+ import { zodResolver } from '@hookform/resolvers/zod';
62
+ import {
63
+ ChevronDown,
64
+ ChevronRight,
65
+ FolderTree,
66
+ GripVertical,
67
+ Pencil,
68
+ Plus,
69
+ Trash2,
70
+ } from 'lucide-react';
71
+ import { useTranslations } from 'next-intl';
72
+ import { useMemo, useState } from 'react';
73
+ import { useForm } from 'react-hook-form';
74
+ import { z } from 'zod';
75
+
76
+ type CategoryFormValues = {
77
+ nome: string;
78
+ natureza: string;
79
+ parentId?: string;
80
+ };
81
+
82
+ type FinanceCategory = {
83
+ id: string;
84
+ codigo: string;
85
+ nome: string;
86
+ parentId: string | null;
87
+ natureza: 'receita' | 'despesa' | 'transferencia' | 'ajuste' | 'outro';
88
+ status: 'active' | 'inactive';
89
+ ativo: boolean;
90
+ };
91
+
92
+ type CategoryNode = FinanceCategory & { children: CategoryNode[] };
93
+
94
+ function buildTree(categories: FinanceCategory[]): CategoryNode[] {
95
+ const byId = new Map<string, CategoryNode>();
96
+ const roots: CategoryNode[] = [];
97
+
98
+ categories.forEach((item) => {
99
+ byId.set(item.id, { ...item, children: [] });
100
+ });
101
+
102
+ categories.forEach((item) => {
103
+ const node = byId.get(item.id)!;
104
+
105
+ if (item.parentId && byId.has(item.parentId)) {
106
+ byId.get(item.parentId)!.children.push(node);
107
+ return;
108
+ }
109
+
110
+ roots.push(node);
111
+ });
112
+
113
+ return roots;
114
+ }
115
+
116
+ function flattenTree(
117
+ nodes: CategoryNode[],
118
+ level = 0
119
+ ): Array<CategoryNode & { level: number }> {
120
+ return nodes.flatMap((node) => [
121
+ { ...node, level },
122
+ ...flattenTree(node.children, level + 1),
123
+ ]);
124
+ }
125
+
126
+ function CategoriaSheet({
127
+ open,
128
+ onOpenChange,
129
+ onSaved,
130
+ editing,
131
+ setEditing,
132
+ categories,
133
+ t,
134
+ }: {
135
+ open: boolean;
136
+ onOpenChange: (open: boolean) => void;
137
+ onSaved: () => Promise<any> | void;
138
+ editing: FinanceCategory | null;
139
+ setEditing: (item: FinanceCategory | null) => void;
140
+ categories: FinanceCategory[];
141
+ t: ReturnType<typeof useTranslations>;
142
+ }) {
143
+ const { request, showToastHandler } = useApp();
144
+
145
+ const categorySchema = z.object({
146
+ nome: z.string().trim().min(1, t('sheet.validation.nameRequired')),
147
+ natureza: z.string().min(1, t('sheet.validation.kindRequired')),
148
+ parentId: z.string().optional(),
149
+ });
150
+
151
+ const form = useForm<CategoryFormValues>({
152
+ resolver: zodResolver(categorySchema),
153
+ defaultValues: {
154
+ nome: '',
155
+ natureza: 'despesa',
156
+ parentId: 'root',
157
+ },
158
+ });
159
+
160
+ const handleOpenChange = (nextOpen: boolean) => {
161
+ onOpenChange(nextOpen);
162
+
163
+ if (!nextOpen) {
164
+ setEditing(null);
165
+ form.reset({ nome: '', natureza: 'despesa', parentId: 'root' });
166
+ return;
167
+ }
168
+
169
+ if (editing) {
170
+ form.reset({
171
+ nome: editing.nome,
172
+ natureza: editing.natureza,
173
+ parentId: editing.parentId || 'root',
174
+ });
175
+ }
176
+ };
177
+
178
+ const onSubmit = async (values: CategoryFormValues) => {
179
+ try {
180
+ if (editing) {
181
+ await request({
182
+ url: `/finance/categories/${editing.id}`,
183
+ method: 'PATCH',
184
+ data: {
185
+ name: values.nome,
186
+ kind: values.natureza,
187
+ parent_id:
188
+ values.parentId === 'root' ? null : Number(values.parentId),
189
+ },
190
+ });
191
+ } else {
192
+ await request({
193
+ url: '/finance/categories',
194
+ method: 'POST',
195
+ data: {
196
+ name: values.nome,
197
+ kind: values.natureza,
198
+ parent_id:
199
+ values.parentId === 'root' ? null : Number(values.parentId),
200
+ },
201
+ });
202
+ }
203
+
204
+ await onSaved();
205
+ onOpenChange(false);
206
+ setEditing(null);
207
+ form.reset({ nome: '', natureza: 'despesa', parentId: 'root' });
208
+ showToastHandler?.(
209
+ 'success',
210
+ editing ? t('messages.updateSuccess') : t('messages.createSuccess')
211
+ );
212
+ } catch {
213
+ showToastHandler?.('error', t('messages.saveError'));
214
+ }
215
+ };
216
+
217
+ return (
218
+ <Sheet open={open} onOpenChange={handleOpenChange}>
219
+ <SheetContent className="w-full sm:max-w-lg">
220
+ <SheetHeader>
221
+ <SheetTitle>
222
+ {editing ? t('sheet.editTitle') : t('sheet.newTitle')}
223
+ </SheetTitle>
224
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
225
+ </SheetHeader>
226
+
227
+ <Form {...form}>
228
+ <form
229
+ className="space-y-4 p-4"
230
+ onSubmit={form.handleSubmit(onSubmit)}
231
+ >
232
+ <FormField
233
+ control={form.control}
234
+ name="nome"
235
+ render={({ field }) => (
236
+ <FormItem>
237
+ <FormLabel>{t('sheet.fields.name')}</FormLabel>
238
+ <FormControl>
239
+ <Input
240
+ placeholder={t('sheet.fields.namePlaceholder')}
241
+ {...field}
242
+ />
243
+ </FormControl>
244
+ <FormMessage />
245
+ </FormItem>
246
+ )}
247
+ />
248
+
249
+ <FormField
250
+ control={form.control}
251
+ name="natureza"
252
+ render={({ field }) => (
253
+ <FormItem>
254
+ <FormLabel>{t('sheet.fields.kind')}</FormLabel>
255
+ <Select value={field.value} onValueChange={field.onChange}>
256
+ <FormControl>
257
+ <SelectTrigger>
258
+ <SelectValue placeholder={t('common.select')} />
259
+ </SelectTrigger>
260
+ </FormControl>
261
+ <SelectContent>
262
+ <SelectItem value="receita">
263
+ {t('natureOptions.receita')}
264
+ </SelectItem>
265
+ <SelectItem value="despesa">
266
+ {t('natureOptions.despesa')}
267
+ </SelectItem>
268
+ <SelectItem value="transferencia">
269
+ {t('natureOptions.transferencia')}
270
+ </SelectItem>
271
+ <SelectItem value="ajuste">
272
+ {t('natureOptions.ajuste')}
273
+ </SelectItem>
274
+ <SelectItem value="outro">
275
+ {t('natureOptions.outro')}
276
+ </SelectItem>
277
+ </SelectContent>
278
+ </Select>
279
+ <FormMessage />
280
+ </FormItem>
281
+ )}
282
+ />
283
+
284
+ <FormField
285
+ control={form.control}
286
+ name="parentId"
287
+ render={({ field }) => (
288
+ <FormItem>
289
+ <FormLabel>{t('sheet.fields.parent')}</FormLabel>
290
+ <Select
291
+ value={field.value || 'root'}
292
+ onValueChange={field.onChange}
293
+ >
294
+ <FormControl>
295
+ <SelectTrigger>
296
+ <SelectValue placeholder={t('sheet.fields.noParent')} />
297
+ </SelectTrigger>
298
+ </FormControl>
299
+ <SelectContent>
300
+ <SelectItem value="root">
301
+ {t('sheet.fields.noneRoot')}
302
+ </SelectItem>
303
+ {categories
304
+ .filter((item) => item.id !== editing?.id)
305
+ .map((item) => (
306
+ <SelectItem key={item.id} value={item.id}>
307
+ {item.nome}
308
+ </SelectItem>
309
+ ))}
310
+ </SelectContent>
311
+ </Select>
312
+ <FormMessage />
313
+ </FormItem>
314
+ )}
315
+ />
316
+
317
+ <div className="flex justify-end gap-2">
318
+ <Button
319
+ type="button"
320
+ variant="outline"
321
+ onClick={() => handleOpenChange(false)}
322
+ >
323
+ {t('common.cancel')}
324
+ </Button>
325
+ <Button type="submit" disabled={form.formState.isSubmitting}>
326
+ {t('common.save')}
327
+ </Button>
328
+ </div>
329
+ </form>
330
+ </Form>
331
+ </SheetContent>
332
+ </Sheet>
333
+ );
334
+ }
335
+
336
+ function CategoryRow({
337
+ item,
338
+ expanded,
339
+ setExpanded,
340
+ onEdit,
341
+ onDelete,
342
+ t,
343
+ }: {
344
+ item: CategoryNode & { level: number };
345
+ expanded: Set<string>;
346
+ setExpanded: (value: Set<string>) => void;
347
+ onEdit: (item: FinanceCategory) => void;
348
+ onDelete: (id: string) => void;
349
+ t: ReturnType<typeof useTranslations>;
350
+ }) {
351
+ const sortable = useSortable({ id: item.id });
352
+ const style = {
353
+ transform: sortable.transform
354
+ ? `translate3d(${sortable.transform.x}px, ${sortable.transform.y}px, 0)`
355
+ : undefined,
356
+ transition: sortable.transition,
357
+ };
358
+
359
+ const hasChildren = item.children.length > 0;
360
+ const isExpanded = expanded.has(item.id);
361
+
362
+ return (
363
+ <div
364
+ ref={sortable.setNodeRef}
365
+ style={style}
366
+ className="rounded-md border bg-background"
367
+ >
368
+ <div
369
+ className="flex items-center justify-between gap-2 p-3"
370
+ style={{ paddingLeft: `${12 + item.level * 24}px` }}
371
+ >
372
+ <div className="flex min-w-0 items-center gap-2">
373
+ <button
374
+ type="button"
375
+ className="inline-flex h-5 w-5 items-center justify-center rounded"
376
+ onClick={() => {
377
+ if (!hasChildren) return;
378
+ const next = new Set(expanded);
379
+ if (next.has(item.id)) {
380
+ next.delete(item.id);
381
+ } else {
382
+ next.add(item.id);
383
+ }
384
+ setExpanded(next);
385
+ }}
386
+ >
387
+ {hasChildren ? (
388
+ isExpanded ? (
389
+ <ChevronDown className="h-4 w-4" />
390
+ ) : (
391
+ <ChevronRight className="h-4 w-4" />
392
+ )
393
+ ) : null}
394
+ </button>
395
+
396
+ <button
397
+ type="button"
398
+ ref={sortable.setActivatorNodeRef}
399
+ {...sortable.listeners}
400
+ {...sortable.attributes}
401
+ className="inline-flex h-7 w-7 items-center justify-center rounded border"
402
+ >
403
+ <GripVertical className="h-4 w-4" />
404
+ </button>
405
+
406
+ <div className="min-w-0">
407
+ <p className="truncate text-sm font-medium">{item.nome}</p>
408
+ <p className="text-xs text-muted-foreground">{item.codigo}</p>
409
+ </div>
410
+ </div>
411
+
412
+ <div className="flex items-center gap-2">
413
+ <Badge variant={item.ativo ? 'default' : 'secondary'}>
414
+ {item.ativo ? t('table.status.active') : t('table.status.inactive')}
415
+ </Badge>
416
+ <Badge variant="outline">{t(`natureOptions.${item.natureza}`)}</Badge>
417
+ <Button variant="outline" size="icon" onClick={() => onEdit(item)}>
418
+ <Pencil className="h-4 w-4" />
419
+ </Button>
420
+ <Button
421
+ variant="outline"
422
+ size="icon"
423
+ disabled={!item.ativo}
424
+ onClick={() => onDelete(item.id)}
425
+ >
426
+ <Trash2 className="h-4 w-4" />
427
+ </Button>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ );
432
+ }
433
+
434
+ export default function CategoriesPage() {
435
+ const t = useTranslations('finance.AdminCategoriesPage');
436
+ const { request, showToastHandler } = useApp();
437
+
438
+ const [sheetOpen, setSheetOpen] = useState(false);
439
+ const [editing, setEditing] = useState<FinanceCategory | null>(null);
440
+ const [deleteId, setDeleteId] = useState<string | null>(null);
441
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
442
+
443
+ const sensors = useSensors(
444
+ useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
445
+ );
446
+
447
+ const { data, refetch } = useQuery<FinanceCategory[]>({
448
+ queryKey: ['finance-categories'],
449
+ queryFn: async () => {
450
+ const response = await request({
451
+ url: '/finance/categories',
452
+ method: 'GET',
453
+ });
454
+ return (response.data || []) as FinanceCategory[];
455
+ },
456
+ placeholderData: [],
457
+ });
458
+
459
+ const categories = data || [];
460
+ const tree = useMemo(() => buildTree(categories), [categories]);
461
+ const flat = useMemo(() => flattenTree(tree), [tree]);
462
+
463
+ const idToParent = useMemo(() => {
464
+ const map = new Map<string, string | null>();
465
+ categories.forEach((item) => map.set(item.id, item.parentId || null));
466
+ return map;
467
+ }, [categories]);
468
+
469
+ const handleDelete = async () => {
470
+ if (!deleteId) return;
471
+
472
+ try {
473
+ await request({
474
+ url: `/finance/categories/${deleteId}`,
475
+ method: 'DELETE',
476
+ });
477
+ await refetch();
478
+ setDeleteId(null);
479
+ showToastHandler?.('success', t('messages.deleteSuccess'));
480
+ } catch {
481
+ showToastHandler?.('error', t('messages.deleteError'));
482
+ }
483
+ };
484
+
485
+ const handleDragEnd = async (event: DragEndEvent) => {
486
+ const { active, over } = event;
487
+
488
+ if (!over || active.id === over.id) {
489
+ return;
490
+ }
491
+
492
+ const activeId = String(active.id);
493
+ const overId = String(over.id);
494
+
495
+ const activeIndex = flat.findIndex((item) => item.id === activeId);
496
+ const overIndex = flat.findIndex((item) => item.id === overId);
497
+
498
+ if (activeIndex === -1 || overIndex === -1) {
499
+ return;
500
+ }
501
+
502
+ const reordered = arrayMove(flat, activeIndex, overIndex);
503
+ const movedItem = reordered[overIndex];
504
+ if (!movedItem) {
505
+ return;
506
+ }
507
+ const targetParentId = idToParent.get(overId) || null;
508
+
509
+ const siblings = reordered.filter(
510
+ (item) =>
511
+ (idToParent.get(item.id) || null) === targetParentId &&
512
+ item.id !== movedItem.id
513
+ );
514
+
515
+ const position = siblings.findIndex((item) => item.id === overId);
516
+
517
+ try {
518
+ await request({
519
+ url: `/finance/categories/${activeId}/move`,
520
+ method: 'PATCH',
521
+ data: {
522
+ parent_id: targetParentId ? Number(targetParentId) : null,
523
+ position: position < 0 ? siblings.length : position,
524
+ },
525
+ });
526
+
527
+ await refetch();
528
+ showToastHandler?.('success', t('messages.moveSuccess'));
529
+ } catch {
530
+ showToastHandler?.('error', t('messages.moveError'));
531
+ }
532
+ };
533
+
534
+ return (
535
+ <Page>
536
+ <PageHeader
537
+ title={t('header.title')}
538
+ description={t('header.description')}
539
+ breadcrumbs={[
540
+ { label: t('breadcrumbs.finance'), href: '/finance' },
541
+ {
542
+ label: t('breadcrumbs.administration'),
543
+ href: '/finance/administration',
544
+ },
545
+ { label: t('breadcrumbs.current') },
546
+ ]}
547
+ />
548
+
549
+ <Card>
550
+ <CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
551
+ <div>
552
+ <CardTitle className="flex items-center gap-2">
553
+ <FolderTree className="h-5 w-5" />
554
+ {t('card.title')}
555
+ </CardTitle>
556
+ <CardDescription>{t('card.description')}</CardDescription>
557
+ </div>
558
+ <Button
559
+ className="gap-2"
560
+ onClick={() => {
561
+ setEditing(null);
562
+ setSheetOpen(true);
563
+ }}
564
+ >
565
+ <Plus className="h-4 w-4" />
566
+ {t('actions.newCategory')}
567
+ </Button>
568
+ </CardHeader>
569
+
570
+ <CardContent>
571
+ {flat.length === 0 ? (
572
+ <div className="rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground">
573
+ {t('table.empty')}
574
+ </div>
575
+ ) : (
576
+ <DndContext
577
+ sensors={sensors}
578
+ collisionDetection={closestCenter}
579
+ onDragEnd={handleDragEnd}
580
+ >
581
+ <SortableContext
582
+ items={flat.map((item) => item.id)}
583
+ strategy={verticalListSortingStrategy}
584
+ >
585
+ <div className="space-y-2">
586
+ {flat.map((item) => (
587
+ <CategoryRow
588
+ key={item.id}
589
+ item={item}
590
+ expanded={expanded}
591
+ setExpanded={setExpanded}
592
+ onEdit={(category) => {
593
+ setEditing(category);
594
+ setSheetOpen(true);
595
+ }}
596
+ onDelete={(id) => setDeleteId(id)}
597
+ t={t}
598
+ />
599
+ ))}
600
+ </div>
601
+ </SortableContext>
602
+ </DndContext>
603
+ )}
604
+ </CardContent>
605
+ </Card>
606
+
607
+ <CategoriaSheet
608
+ open={sheetOpen}
609
+ onOpenChange={setSheetOpen}
610
+ onSaved={refetch}
611
+ editing={editing}
612
+ setEditing={setEditing}
613
+ categories={categories}
614
+ t={t}
615
+ />
616
+
617
+ <AlertDialog
618
+ open={!!deleteId}
619
+ onOpenChange={(open) => {
620
+ if (!open) {
621
+ setDeleteId(null);
622
+ }
623
+ }}
624
+ >
625
+ <AlertDialogContent>
626
+ <AlertDialogHeader>
627
+ <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
628
+ <AlertDialogDescription>
629
+ {t('deleteDialog.description')}
630
+ </AlertDialogDescription>
631
+ </AlertDialogHeader>
632
+ <AlertDialogFooter>
633
+ <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
634
+ <AlertDialogAction onClick={handleDelete}>
635
+ {t('deleteDialog.confirm')}
636
+ </AlertDialogAction>
637
+ </AlertDialogFooter>
638
+ </AlertDialogContent>
639
+ </AlertDialog>
640
+ </Page>
641
+ );
642
+ }