@hed-hog/finance 0.0.318 → 0.0.321

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 (92) hide show
  1. package/dist/dto/create-bank-account.dto.d.ts +1 -0
  2. package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
  3. package/dist/dto/create-bank-account.dto.js +7 -0
  4. package/dist/dto/create-bank-account.dto.js.map +1 -1
  5. package/dist/dto/create-bank-statement-entry.dto.d.ts +8 -0
  6. package/dist/dto/create-bank-statement-entry.dto.d.ts.map +1 -0
  7. package/dist/dto/create-bank-statement-entry.dto.js +54 -0
  8. package/dist/dto/create-bank-statement-entry.dto.js.map +1 -0
  9. package/dist/dto/create-currency.dto.d.ts +6 -0
  10. package/dist/dto/create-currency.dto.d.ts.map +1 -0
  11. package/dist/dto/create-currency.dto.js +37 -0
  12. package/dist/dto/create-currency.dto.js.map +1 -0
  13. package/dist/dto/update-bank-account.dto.d.ts +1 -0
  14. package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
  15. package/dist/dto/update-bank-account.dto.js +7 -0
  16. package/dist/dto/update-bank-account.dto.js.map +1 -1
  17. package/dist/dto/update-bank-statement-entry.dto.d.ts +6 -0
  18. package/dist/dto/update-bank-statement-entry.dto.d.ts.map +1 -0
  19. package/dist/dto/update-bank-statement-entry.dto.js +42 -0
  20. package/dist/dto/update-bank-statement-entry.dto.js.map +1 -0
  21. package/dist/dto/update-currency.dto.d.ts +7 -0
  22. package/dist/dto/update-currency.dto.d.ts.map +1 -0
  23. package/dist/dto/update-currency.dto.js +47 -0
  24. package/dist/dto/update-currency.dto.js.map +1 -0
  25. package/dist/finance-bank-accounts.controller.d.ts +25 -13
  26. package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
  27. package/dist/finance-bank-accounts.controller.js +5 -3
  28. package/dist/finance-bank-accounts.controller.js.map +1 -1
  29. package/dist/finance-currencies.controller.d.ts +36 -0
  30. package/dist/finance-currencies.controller.d.ts.map +1 -0
  31. package/dist/finance-currencies.controller.js +74 -0
  32. package/dist/finance-currencies.controller.js.map +1 -0
  33. package/dist/finance-data.controller.d.ts +4 -0
  34. package/dist/finance-data.controller.d.ts.map +1 -1
  35. package/dist/finance-installments.controller.d.ts +3 -2
  36. package/dist/finance-installments.controller.d.ts.map +1 -1
  37. package/dist/finance-installments.controller.js +10 -6
  38. package/dist/finance-installments.controller.js.map +1 -1
  39. package/dist/finance-statements.controller.d.ts +61 -12
  40. package/dist/finance-statements.controller.d.ts.map +1 -1
  41. package/dist/finance-statements.controller.js +50 -8
  42. package/dist/finance-statements.controller.js.map +1 -1
  43. package/dist/finance-transfers.controller.d.ts +13 -8
  44. package/dist/finance-transfers.controller.d.ts.map +1 -1
  45. package/dist/finance-transfers.controller.js +11 -5
  46. package/dist/finance-transfers.controller.js.map +1 -1
  47. package/dist/finance.module.d.ts.map +1 -1
  48. package/dist/finance.module.js +2 -0
  49. package/dist/finance.module.js.map +1 -1
  50. package/dist/finance.service.d.ts +153 -35
  51. package/dist/finance.service.d.ts.map +1 -1
  52. package/dist/finance.service.js +468 -55
  53. package/dist/finance.service.js.map +1 -1
  54. package/hedhog/data/currency.yaml +14 -0
  55. package/hedhog/data/menu.yaml +16 -0
  56. package/hedhog/data/role.yaml +9 -1
  57. package/hedhog/data/route.yaml +78 -0
  58. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +87 -72
  59. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +53 -25
  60. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +8 -0
  61. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +60 -24
  62. package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +490 -0
  63. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +243 -65
  64. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +25 -3
  65. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +732 -61
  66. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +101 -15
  67. package/hedhog/frontend/messages/en.json +58 -0
  68. package/hedhog/frontend/messages/pt.json +58 -0
  69. package/hedhog/table/bank_account.yaml +8 -0
  70. package/hedhog/table/bank_statement_line.yaml +1 -1
  71. package/hedhog/table/cashflow_projection.yaml +1 -1
  72. package/hedhog/table/currency.yaml +21 -0
  73. package/hedhog/table/financial_installment.yaml +2 -2
  74. package/hedhog/table/financial_title.yaml +1 -1
  75. package/hedhog/table/installment_allocation.yaml +1 -1
  76. package/hedhog/table/receivable_schedule.yaml +1 -1
  77. package/hedhog/table/settlement.yaml +1 -1
  78. package/hedhog/table/settlement_allocation.yaml +5 -5
  79. package/package.json +6 -6
  80. package/src/dto/create-bank-account.dto.ts +18 -1
  81. package/src/dto/create-bank-statement-entry.dto.ts +50 -0
  82. package/src/dto/create-currency.dto.ts +21 -0
  83. package/src/dto/update-bank-account.dto.ts +11 -1
  84. package/src/dto/update-bank-statement-entry.dto.ts +31 -0
  85. package/src/dto/update-currency.dto.ts +31 -0
  86. package/src/finance-bank-accounts.controller.ts +3 -2
  87. package/src/finance-currencies.controller.ts +44 -0
  88. package/src/finance-installments.controller.ts +9 -3
  89. package/src/finance-statements.controller.ts +40 -0
  90. package/src/finance-transfers.controller.ts +7 -1
  91. package/src/finance.module.ts +2 -0
  92. package/src/finance.service.ts +633 -55
@@ -20,6 +20,7 @@ import {
20
20
  import { Badge } from '@/components/ui/badge';
21
21
  import { Button } from '@/components/ui/button';
22
22
  import { Checkbox } from '@/components/ui/checkbox';
23
+ import { DateRangePicker } from '@/components/ui/date-range-picker';
23
24
  import {
24
25
  DropdownMenu,
25
26
  DropdownMenuContent,
@@ -1238,6 +1239,12 @@ function NovoTituloSheet({
1238
1239
  <SelectItem value="transferencia">
1239
1240
  {t('channels.transfer')}
1240
1241
  </SelectItem>
1242
+ <SelectItem value="debito_automatico">
1243
+ D&#233;bito autom&#225;tico
1244
+ </SelectItem>
1245
+ <SelectItem value="debito_em_conta">
1246
+ D&#233;bito em conta
1247
+ </SelectItem>
1241
1248
  </SelectContent>
1242
1249
  </Select>
1243
1250
  <FormMessage />
@@ -2340,6 +2347,12 @@ function EditarTituloSheet({
2340
2347
  <SelectItem value="transferencia">
2341
2348
  {t('channels.transfer')}
2342
2349
  </SelectItem>
2350
+ <SelectItem value="debito_automatico">
2351
+ D&#233;bito autom&#225;tico
2352
+ </SelectItem>
2353
+ <SelectItem value="debito_em_conta">
2354
+ D&#233;bito em conta
2355
+ </SelectItem>
2343
2356
  </SelectContent>
2344
2357
  </Select>
2345
2358
  <FormMessage />
@@ -2427,6 +2440,8 @@ export default function TitulosReceberPage() {
2427
2440
 
2428
2441
  const [search, setSearch] = useState('');
2429
2442
  const [statusFilter, setStatusFilter] = useState<string>('all');
2443
+ const [fromDate, setFromDate] = useState('');
2444
+ const [toDate, setToDate] = useState('');
2430
2445
  const [page, setPage] = useState(1);
2431
2446
  const pageSize = 10;
2432
2447
 
@@ -2446,6 +2461,8 @@ export default function TitulosReceberPage() {
2446
2461
  'finance-receivable-installments-list',
2447
2462
  search,
2448
2463
  normalizedStatusFilter,
2464
+ fromDate,
2465
+ toDate,
2449
2466
  page,
2450
2467
  pageSize,
2451
2468
  ],
@@ -2458,6 +2475,8 @@ export default function TitulosReceberPage() {
2458
2475
  pageSize,
2459
2476
  search: search.trim() || undefined,
2460
2477
  status: normalizedStatusFilter,
2478
+ from: fromDate || undefined,
2479
+ to: toDate || undefined,
2461
2480
  },
2462
2481
  });
2463
2482
 
@@ -2595,7 +2614,7 @@ export default function TitulosReceberPage() {
2595
2614
 
2596
2615
  useEffect(() => {
2597
2616
  setPage(1);
2598
- }, [search, normalizedStatusFilter]);
2617
+ }, [search, normalizedStatusFilter, fromDate, toDate]);
2599
2618
 
2600
2619
  const canalBadge = {
2601
2620
  boleto: {
@@ -2611,6 +2630,14 @@ export default function TitulosReceberPage() {
2611
2630
  label: t('channels.transfer'),
2612
2631
  className: 'bg-orange-100 text-orange-700',
2613
2632
  },
2633
+ debito_automatico: {
2634
+ label: 'D\u00e9bito autom\u00e1tico',
2635
+ className: 'bg-emerald-100 text-emerald-700',
2636
+ },
2637
+ debito_em_conta: {
2638
+ label: 'D\u00e9bito em conta',
2639
+ className: 'bg-cyan-100 text-cyan-700',
2640
+ },
2614
2641
  };
2615
2642
 
2616
2643
  const handleOpenAttachment = async (fileId?: string) => {
@@ -2675,29 +2702,38 @@ export default function TitulosReceberPage() {
2675
2702
 
2676
2703
  <KpiCardsGrid items={summaryCards} columns={3} />
2677
2704
 
2678
- <div className="min-w-0">
2679
- <SearchBar
2680
- searchQuery={search}
2681
- onSearchChange={setSearch}
2682
- onSearch={() => undefined}
2683
- placeholder={t('filters.searchPlaceholder')}
2684
- controls={[
2685
- {
2686
- id: 'status',
2687
- type: 'select',
2688
- value: statusFilter,
2689
- onChange: setStatusFilter,
2690
- placeholder: t('filters.status'),
2691
- options: [
2692
- { value: 'all', label: t('statuses.all') },
2693
- { value: 'aberto', label: t('statuses.aberto') },
2694
- { value: 'parcial', label: t('statuses.parcial') },
2695
- { value: 'liquidado', label: t('statuses.liquidado') },
2696
- { value: 'vencido', label: t('statuses.vencido') },
2697
- { value: 'cancelado', label: t('statuses.cancelado') },
2698
- ],
2699
- },
2700
- ]}
2705
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
2706
+ <div className="min-w-0 flex-1">
2707
+ <SearchBar
2708
+ searchQuery={search}
2709
+ onSearchChange={setSearch}
2710
+ onSearch={() => undefined}
2711
+ placeholder={t('filters.searchPlaceholder')}
2712
+ controls={[
2713
+ {
2714
+ id: 'status',
2715
+ type: 'select',
2716
+ value: statusFilter,
2717
+ onChange: setStatusFilter,
2718
+ placeholder: t('filters.status'),
2719
+ options: [
2720
+ { value: 'all', label: t('statuses.all') },
2721
+ { value: 'aberto', label: t('statuses.aberto') },
2722
+ { value: 'parcial', label: t('statuses.parcial') },
2723
+ { value: 'liquidado', label: t('statuses.liquidado') },
2724
+ { value: 'vencido', label: t('statuses.vencido') },
2725
+ { value: 'cancelado', label: t('statuses.cancelado') },
2726
+ ],
2727
+ },
2728
+ ]}
2729
+ />
2730
+ </div>
2731
+ <DateRangePicker
2732
+ fromDate={fromDate}
2733
+ toDate={toDate}
2734
+ onFromDateChange={setFromDate}
2735
+ onToDateChange={setToDate}
2736
+ defaultPreset="thisMonth"
2701
2737
  />
2702
2738
  </div>
2703
2739
 
@@ -0,0 +1,490 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, 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
+ Form,
18
+ FormControl,
19
+ FormField,
20
+ FormItem,
21
+ FormLabel,
22
+ FormMessage,
23
+ } from '@/components/ui/form';
24
+ import { Input } from '@/components/ui/input';
25
+ import {
26
+ Sheet,
27
+ SheetContent,
28
+ SheetDescription,
29
+ SheetHeader,
30
+ SheetTitle,
31
+ } from '@/components/ui/sheet';
32
+ import { useFormDraft } from '@/hooks/use-form-draft';
33
+ import { formatDateTime } from '@/lib/format-date';
34
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
35
+ import { zodResolver } from '@hookform/resolvers/zod';
36
+ import { formatDistanceToNow } from 'date-fns';
37
+ import { enUS, ptBR } from 'date-fns/locale';
38
+ import { Coins, Pencil, Plus, Trash2 } from 'lucide-react';
39
+ import { useTranslations } from 'next-intl';
40
+ import { useEffect, useMemo, useState } from 'react';
41
+ import { useForm, useWatch } from 'react-hook-form';
42
+ import { z } from 'zod';
43
+
44
+ type CurrencyFormValues = {
45
+ code: string;
46
+ name: string;
47
+ symbol: string;
48
+ };
49
+
50
+ type Currency = {
51
+ id: string;
52
+ code: string;
53
+ name: string;
54
+ symbol: string;
55
+ status: 'active' | 'inactive';
56
+ ativo: boolean;
57
+ };
58
+
59
+ type CurrencyDraftPayload = {
60
+ mode: 'create' | 'edit';
61
+ currencyId: string | null;
62
+ values: CurrencyFormValues;
63
+ };
64
+
65
+ const CURRENCY_FORM_DRAFT_STORAGE_KEY = 'finance-currencies-form-draft';
66
+
67
+ function CurrencySheet({
68
+ open,
69
+ onOpenChange,
70
+ onSaved,
71
+ editingCurrency,
72
+ onEditingChange,
73
+ t,
74
+ }: {
75
+ open: boolean;
76
+ onOpenChange: (open: boolean) => void;
77
+ onSaved: () => Promise<unknown> | void;
78
+ editingCurrency: Currency | null;
79
+ onEditingChange: (currency: Currency | null) => void;
80
+ t: ReturnType<typeof useTranslations>;
81
+ }) {
82
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
83
+ useApp();
84
+
85
+ const currencyFormSchema = z.object({
86
+ code: z
87
+ .string()
88
+ .trim()
89
+ .min(1, t('sheet.validation.codeRequired'))
90
+ .max(3, t('sheet.validation.codeMaxLength')),
91
+ name: z.string().trim().min(1, t('sheet.validation.nameRequired')),
92
+ symbol: z.string().trim().min(1, t('sheet.validation.symbolRequired')),
93
+ });
94
+
95
+ const form = useForm<CurrencyFormValues>({
96
+ resolver: zodResolver(currencyFormSchema),
97
+ defaultValues: {
98
+ code: '',
99
+ name: '',
100
+ symbol: '',
101
+ },
102
+ });
103
+
104
+ const watchedValues = useWatch({ control: form.control });
105
+
106
+ const {
107
+ clearDraft,
108
+ loadDraft,
109
+ hasDraft,
110
+ savedAt: draftSavedAt,
111
+ } = useFormDraft<CurrencyDraftPayload>({
112
+ storageKey: CURRENCY_FORM_DRAFT_STORAGE_KEY,
113
+ value: {
114
+ mode: editingCurrency ? 'edit' : 'create',
115
+ currencyId: editingCurrency?.id ?? null,
116
+ values: {
117
+ code: watchedValues.code ?? '',
118
+ name: watchedValues.name ?? '',
119
+ symbol: watchedValues.symbol ?? '',
120
+ },
121
+ },
122
+ hasData: Boolean(
123
+ (watchedValues.code ?? '').trim() ||
124
+ (watchedValues.name ?? '').trim() ||
125
+ (watchedValues.symbol ?? '').trim()
126
+ ),
127
+ enabled: open,
128
+ });
129
+
130
+ const draftStatusContent = useMemo(() => {
131
+ if (!hasDraft || !draftSavedAt) return null;
132
+ const savedDate = new Date(draftSavedAt);
133
+ if (Number.isNaN(savedDate.getTime())) return null;
134
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
135
+ const relativeLabel = formatDistanceToNow(savedDate, {
136
+ addSuffix: true,
137
+ locale,
138
+ });
139
+ const absoluteLabel = formatDateTime(
140
+ savedDate,
141
+ getSettingValue,
142
+ currentLocaleCode
143
+ );
144
+ return currentLocaleCode.startsWith('pt')
145
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
146
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
147
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
148
+
149
+ useEffect(() => {
150
+ if (!open) return;
151
+
152
+ const storedDraft = loadDraft();
153
+ const shouldRestoreEditDraft =
154
+ Boolean(editingCurrency) &&
155
+ storedDraft?.payload.mode === 'edit' &&
156
+ storedDraft.payload.currencyId === editingCurrency?.id;
157
+
158
+ if (editingCurrency) {
159
+ form.reset(
160
+ shouldRestoreEditDraft
161
+ ? storedDraft.payload.values
162
+ : {
163
+ code: editingCurrency.code,
164
+ name: editingCurrency.name,
165
+ symbol: editingCurrency.symbol,
166
+ }
167
+ );
168
+ return;
169
+ }
170
+
171
+ form.reset(
172
+ storedDraft?.payload.mode === 'create'
173
+ ? storedDraft.payload.values
174
+ : { code: '', name: '', symbol: '' }
175
+ );
176
+ }, [open, editingCurrency, form, loadDraft]);
177
+
178
+ const handleSubmit = async (values: CurrencyFormValues) => {
179
+ try {
180
+ if (editingCurrency) {
181
+ await request({
182
+ url: `/finance/currencies/${editingCurrency.id}`,
183
+ method: 'PATCH',
184
+ data: {
185
+ code: values.code,
186
+ name: values.name,
187
+ symbol: values.symbol,
188
+ },
189
+ });
190
+ } else {
191
+ await request({
192
+ url: '/finance/currencies',
193
+ method: 'POST',
194
+ data: {
195
+ code: values.code,
196
+ name: values.name,
197
+ symbol: values.symbol,
198
+ },
199
+ });
200
+ }
201
+
202
+ clearDraft();
203
+ await onSaved();
204
+ form.reset({ code: '', name: '', symbol: '' });
205
+ onEditingChange(null);
206
+ onOpenChange(false);
207
+ showToastHandler?.(
208
+ 'success',
209
+ editingCurrency
210
+ ? t('messages.updateSuccess')
211
+ : t('messages.createSuccess')
212
+ );
213
+ } catch {
214
+ showToastHandler?.(
215
+ 'error',
216
+ editingCurrency ? t('messages.updateError') : t('messages.createError')
217
+ );
218
+ }
219
+ };
220
+
221
+ const handleOpenChange = (nextOpen: boolean) => {
222
+ onOpenChange(nextOpen);
223
+ if (!nextOpen) onEditingChange(null);
224
+ };
225
+
226
+ return (
227
+ <Sheet open={open} onOpenChange={handleOpenChange}>
228
+ <SheetContent className="w-full sm:max-w-lg">
229
+ <SheetHeader>
230
+ <SheetTitle>
231
+ {editingCurrency ? t('sheet.editTitle') : t('sheet.newTitle')}
232
+ </SheetTitle>
233
+ <SheetDescription>
234
+ {editingCurrency
235
+ ? t('sheet.editDescription')
236
+ : t('sheet.newDescription')}
237
+ </SheetDescription>
238
+ </SheetHeader>
239
+
240
+ <Form {...form}>
241
+ <form
242
+ className="space-y-4 p-4"
243
+ onSubmit={form.handleSubmit(handleSubmit)}
244
+ >
245
+ <FormField
246
+ control={form.control}
247
+ name="code"
248
+ render={({ field }) => (
249
+ <FormItem>
250
+ <FormLabel>{t('sheet.fields.code')}</FormLabel>
251
+ <FormControl>
252
+ <Input
253
+ placeholder={t('sheet.fields.codePlaceholder')}
254
+ maxLength={3}
255
+ {...field}
256
+ onChange={(e) =>
257
+ field.onChange(e.target.value.toUpperCase())
258
+ }
259
+ />
260
+ </FormControl>
261
+ <FormMessage />
262
+ </FormItem>
263
+ )}
264
+ />
265
+
266
+ <FormField
267
+ control={form.control}
268
+ name="name"
269
+ render={({ field }) => (
270
+ <FormItem>
271
+ <FormLabel>{t('sheet.fields.name')}</FormLabel>
272
+ <FormControl>
273
+ <Input
274
+ placeholder={t('sheet.fields.namePlaceholder')}
275
+ {...field}
276
+ />
277
+ </FormControl>
278
+ <FormMessage />
279
+ </FormItem>
280
+ )}
281
+ />
282
+
283
+ <FormField
284
+ control={form.control}
285
+ name="symbol"
286
+ render={({ field }) => (
287
+ <FormItem>
288
+ <FormLabel>{t('sheet.fields.symbol')}</FormLabel>
289
+ <FormControl>
290
+ <Input
291
+ placeholder={t('sheet.fields.symbolPlaceholder')}
292
+ {...field}
293
+ />
294
+ </FormControl>
295
+ <FormMessage />
296
+ </FormItem>
297
+ )}
298
+ />
299
+
300
+ {draftStatusContent ? (
301
+ <p className="text-xs text-muted-foreground">
302
+ {draftStatusContent}
303
+ </p>
304
+ ) : null}
305
+
306
+ <div className="flex justify-end gap-2">
307
+ <Button
308
+ type="button"
309
+ variant="outline"
310
+ onClick={() => handleOpenChange(false)}
311
+ >
312
+ {t('common.cancel')}
313
+ </Button>
314
+ <Button type="submit" disabled={form.formState.isSubmitting}>
315
+ {t('common.save')}
316
+ </Button>
317
+ </div>
318
+ </form>
319
+ </Form>
320
+ </SheetContent>
321
+ </Sheet>
322
+ );
323
+ }
324
+
325
+ export default function CurrenciesPage() {
326
+ const t = useTranslations('finance.AdminCurrenciesPage');
327
+ const { request, showToastHandler } = useApp();
328
+
329
+ const [sheetOpen, setSheetOpen] = useState(false);
330
+ const [editingCurrency, setEditingCurrency] = useState<Currency | null>(null);
331
+ const [currencyIdToDelete, setCurrencyIdToDelete] = useState<string | null>(
332
+ null
333
+ );
334
+
335
+ const { data: currencies, refetch } = useQuery<Currency[]>({
336
+ queryKey: ['finance-currencies'],
337
+ queryFn: async () => {
338
+ const response = await request({
339
+ url: '/finance/currencies',
340
+ method: 'GET',
341
+ });
342
+ return (response.data || []) as Currency[];
343
+ },
344
+ placeholderData: [],
345
+ });
346
+
347
+ const handleDelete = async () => {
348
+ if (!currencyIdToDelete) return;
349
+
350
+ try {
351
+ await request({
352
+ url: `/finance/currencies/${currencyIdToDelete}`,
353
+ method: 'DELETE',
354
+ });
355
+
356
+ await refetch();
357
+ setCurrencyIdToDelete(null);
358
+ showToastHandler?.('success', t('messages.deleteSuccess'));
359
+ } catch {
360
+ showToastHandler?.('error', t('messages.deleteError'));
361
+ }
362
+ };
363
+
364
+ const items = currencies || [];
365
+
366
+ return (
367
+ <Page>
368
+ <PageHeader
369
+ title={t('header.title')}
370
+ description={t('header.description')}
371
+ breadcrumbs={[
372
+ { label: t('breadcrumbs.finance'), href: '/finance' },
373
+ {
374
+ label: t('breadcrumbs.administration'),
375
+ href: '/finance/administration',
376
+ },
377
+ { label: t('breadcrumbs.current') },
378
+ ]}
379
+ actions={
380
+ <Button
381
+ onClick={() => {
382
+ setEditingCurrency(null);
383
+ setSheetOpen(true);
384
+ }}
385
+ className="gap-2"
386
+ >
387
+ <Plus className="h-4 w-4" />
388
+ {t('actions.newCurrency')}
389
+ </Button>
390
+ }
391
+ />
392
+
393
+ <div className="space-y-4">
394
+ {items.length === 0 ? (
395
+ <EmptyState
396
+ icon={<Coins className="h-12 w-12" />}
397
+ title={t('table.empty')}
398
+ description={t('header.description')}
399
+ actionLabel={t('actions.newCurrency')}
400
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
401
+ onAction={() => {
402
+ setEditingCurrency(null);
403
+ setSheetOpen(true);
404
+ }}
405
+ />
406
+ ) : (
407
+ <div className="grid gap-3">
408
+ {items.map((currency) => (
409
+ <div
410
+ key={currency.id}
411
+ className="flex flex-col gap-3 rounded-md border p-4 sm:flex-row sm:items-center sm:justify-between"
412
+ >
413
+ <div className="flex items-start gap-3">
414
+ <div className="rounded-md bg-muted p-2">
415
+ <Coins className="h-4 w-4" />
416
+ </div>
417
+ <div>
418
+ <p className="text-sm font-medium">
419
+ {currency.symbol}{' '}
420
+ <span className="font-bold">{currency.code}</span>
421
+ {' — '}
422
+ {currency.name}
423
+ </p>
424
+ </div>
425
+ </div>
426
+
427
+ <div className="flex items-center gap-2">
428
+ <Badge variant={currency.ativo ? 'default' : 'secondary'}>
429
+ {currency.ativo
430
+ ? t('table.status.active')
431
+ : t('table.status.inactive')}
432
+ </Badge>
433
+ <Button
434
+ variant="outline"
435
+ size="icon"
436
+ onClick={() => {
437
+ setEditingCurrency(currency);
438
+ setSheetOpen(true);
439
+ }}
440
+ >
441
+ <Pencil className="h-4 w-4" />
442
+ </Button>
443
+ <Button
444
+ variant="outline"
445
+ size="icon"
446
+ onClick={() => setCurrencyIdToDelete(currency.id)}
447
+ disabled={!currency.ativo}
448
+ >
449
+ <Trash2 className="h-4 w-4" />
450
+ </Button>
451
+ </div>
452
+ </div>
453
+ ))}
454
+ </div>
455
+ )}
456
+ </div>
457
+
458
+ <CurrencySheet
459
+ open={sheetOpen}
460
+ onOpenChange={setSheetOpen}
461
+ onSaved={refetch}
462
+ editingCurrency={editingCurrency}
463
+ onEditingChange={setEditingCurrency}
464
+ t={t}
465
+ />
466
+
467
+ <AlertDialog
468
+ open={!!currencyIdToDelete}
469
+ onOpenChange={(open) => {
470
+ if (!open) setCurrencyIdToDelete(null);
471
+ }}
472
+ >
473
+ <AlertDialogContent>
474
+ <AlertDialogHeader>
475
+ <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
476
+ <AlertDialogDescription>
477
+ {t('deleteDialog.description')}
478
+ </AlertDialogDescription>
479
+ </AlertDialogHeader>
480
+ <AlertDialogFooter>
481
+ <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
482
+ <AlertDialogAction onClick={handleDelete}>
483
+ {t('deleteDialog.confirm')}
484
+ </AlertDialogAction>
485
+ </AlertDialogFooter>
486
+ </AlertDialogContent>
487
+ </AlertDialog>
488
+ </Page>
489
+ );
490
+ }