@hed-hog/finance 0.0.365 → 0.0.370

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 (38) hide show
  1. package/dist/dto/create-financial-title.dto.d.ts +1 -0
  2. package/dist/dto/create-financial-title.dto.d.ts.map +1 -1
  3. package/dist/dto/create-financial-title.dto.js +6 -0
  4. package/dist/dto/create-financial-title.dto.js.map +1 -1
  5. package/dist/finance-data.controller.d.ts +4 -0
  6. package/dist/finance-data.controller.d.ts.map +1 -1
  7. package/dist/finance-installments.controller.d.ts +40 -0
  8. package/dist/finance-installments.controller.d.ts.map +1 -1
  9. package/dist/finance-statements.controller.d.ts +2 -0
  10. package/dist/finance-statements.controller.d.ts.map +1 -1
  11. package/dist/finance.service.d.ts +47 -0
  12. package/dist/finance.service.d.ts.map +1 -1
  13. package/dist/finance.service.js +156 -109
  14. package/dist/finance.service.js.map +1 -1
  15. package/dist/mcp-tools/finance-installments.mcp-tools.d.ts.map +1 -1
  16. package/dist/mcp-tools/finance-installments.mcp-tools.js +12 -2
  17. package/dist/mcp-tools/finance-installments.mcp-tools.js.map +1 -1
  18. package/hedhog/frontend/app/_components/bank-account-picker-field.tsx.ejs +3 -0
  19. package/hedhog/frontend/app/_components/bank-account-sheet.tsx.ejs +902 -0
  20. package/hedhog/frontend/app/_components/finance-picker.tsx.ejs +95 -0
  21. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +117 -43
  22. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +8 -2
  23. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +114 -43
  24. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +4 -1
  25. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +4 -1
  26. package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +4 -1
  27. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +4 -1
  28. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +6 -893
  29. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +4 -1
  30. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +8 -2
  31. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +4 -1
  32. package/hedhog/frontend/messages/en.json +14 -1
  33. package/hedhog/frontend/messages/pt.json +14 -1
  34. package/hedhog/table/financial_title.yaml +6 -1
  35. package/package.json +6 -6
  36. package/src/dto/create-financial-title.dto.ts +5 -0
  37. package/src/finance.service.ts +187 -134
  38. package/src/mcp-tools/finance-installments.mcp-tools.ts +12 -2
@@ -25,38 +25,8 @@ import {
25
25
  CardHeader,
26
26
  CardTitle,
27
27
  } from '@/components/ui/card';
28
- import { EntityPicker } from '@/components/ui/entity-picker';
29
- import {
30
- Form,
31
- FormControl,
32
- FormField,
33
- FormItem,
34
- FormLabel,
35
- FormMessage,
36
- } from '@/components/ui/form';
37
- import { Input } from '@/components/ui/input';
38
- import { InputMoney } from '@/components/ui/input-money';
39
28
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
40
- import {
41
- Select,
42
- SelectContent,
43
- SelectItem,
44
- SelectTrigger,
45
- SelectValue,
46
- } from '@/components/ui/select';
47
- import {
48
- Sheet,
49
- SheetDescription,
50
- SheetHeader,
51
- SheetTitle,
52
- } from '@/components/ui/sheet';
53
- import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
54
- import { useFormDraft } from '@/hooks/use-form-draft';
55
- import { formatDateTime } from '@/lib/format-date';
56
29
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
57
- import { zodResolver } from '@hookform/resolvers/zod';
58
- import { formatDistanceToNow } from 'date-fns';
59
- import { enUS, ptBR } from 'date-fns/locale';
60
30
  import {
61
31
  Building2,
62
32
  Eye,
@@ -67,71 +37,19 @@ import {
67
37
  RefreshCw,
68
38
  Trash2,
69
39
  TrendingUp,
70
- Upload,
71
40
  Wallet,
72
41
  type LucideIcon,
73
42
  } from 'lucide-react';
74
43
  import { useTranslations } from 'next-intl';
75
44
  import Link from 'next/link';
76
- import { useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
77
- import { useForm, useWatch } from 'react-hook-form';
78
- import { z } from 'zod';
45
+ import { useState } from 'react';
79
46
  import {
80
- FinancePageSection,
81
- FinanceSheetBody,
82
- FinanceSheetSection,
83
- } from '../../_components/finance-layout';
47
+ BankAccountSheet,
48
+ type BankAccount,
49
+ } from '../../_components/bank-account-sheet';
50
+ import { FinancePageSection } from '../../_components/finance-layout';
84
51
  import { useFinanceRealtimeRefresh } from '../../_lib/use-finance-realtime-refresh';
85
52
 
86
- const createBankAccountFormSchema = (t: ReturnType<typeof useTranslations>) =>
87
- z.object({
88
- banco: z.string().trim().min(1, t('validation.bankRequired')),
89
- agencia: z.string().optional(),
90
- conta: z.string().optional(),
91
- tipo: z.string().min(1, t('validation.typeRequired')),
92
- descricao: z.string().optional(),
93
- logoFileId: z.number().int().nullable().optional(),
94
- currencyId: z.number().int().nullable().optional(),
95
- dataInicial: z.string().optional(),
96
- saldoInicial: z.number().min(0, t('validation.initialBalanceInvalid')),
97
- });
98
-
99
- type BankAccountFormValues = z.infer<
100
- ReturnType<typeof createBankAccountFormSchema>
101
- >;
102
-
103
- type Currency = {
104
- id: string;
105
- code: string;
106
- name: string;
107
- symbol: string;
108
- ativo: boolean;
109
- };
110
-
111
- type BankAccountCurrency = {
112
- id: string;
113
- code: string;
114
- symbol: string;
115
- name: string;
116
- };
117
-
118
- type BankAccount = {
119
- id: string;
120
- codigo: string;
121
- descricao: string;
122
- banco: string;
123
- agencia: string;
124
- conta: string;
125
- tipo: 'corrente' | 'poupanca' | 'investimento' | 'caixa';
126
- logoFileId: number | null;
127
- currencyId: number | null;
128
- currency: BankAccountCurrency | null;
129
- saldoAtual: number;
130
- saldoConciliado: number;
131
- ativo: boolean;
132
- dataInicial: string | null;
133
- };
134
-
135
53
  type PaginatedBankAccountsResponse = {
136
54
  data: BankAccount[];
137
55
  total: number;
@@ -142,21 +60,6 @@ type PaginatedBankAccountsResponse = {
142
60
  lastPage: number;
143
61
  };
144
62
 
145
- type UploadedFilePayload = {
146
- id?: number | string | null;
147
- };
148
-
149
- type BankAccountDraftPayload = {
150
- mode: 'create' | 'edit';
151
- accountId: string | null;
152
- values: BankAccountFormValues;
153
- logoFileId: number | null;
154
- logoPreviewUrl: string | null;
155
- currencyId: number | null;
156
- };
157
-
158
- const BANK_ACCOUNT_FORM_DRAFT_STORAGE_KEY = 'finance-bank-account-form-draft';
159
-
160
63
  function formatCurrency(value: number, currencyCode: string, locale: string) {
161
64
  const localeTag = locale.startsWith('pt') ? 'pt-BR' : 'en-US';
162
65
  return new Intl.NumberFormat(localeTag, {
@@ -197,796 +100,6 @@ function BankAccountLogo({
197
100
  );
198
101
  }
199
102
 
200
- function NovaContaSheet({
201
- t,
202
- onCreated,
203
- open,
204
- onOpenChange,
205
- editingAccount,
206
- onEditingAccountChange,
207
- }: {
208
- t: ReturnType<typeof useTranslations>;
209
- onCreated: () => Promise<unknown> | void;
210
- open: boolean;
211
- onOpenChange: (open: boolean) => void;
212
- editingAccount: BankAccount | null;
213
- onEditingAccountChange: (account: BankAccount | null) => void;
214
- }) {
215
- const { request, showToastHandler, currentLocaleCode, getSettingValue } =
216
- useApp();
217
-
218
- const { data: currenciesData, refetch: refetchCurrencies } = useQuery<
219
- Currency[]
220
- >({
221
- queryKey: ['finance-currencies-for-select'],
222
- queryFn: async () => {
223
- const response = await request({
224
- url: '/finance/currencies',
225
- method: 'GET',
226
- });
227
- return (response.data || []) as Currency[];
228
- },
229
- placeholderData: [],
230
- });
231
- const currencies = currenciesData || [];
232
-
233
- const createSuccessMessage = t('messages.createSuccess');
234
- const createErrorMessage = t('messages.createError');
235
- const updateSuccessMessage = t('messages.updateSuccess');
236
- const updateErrorMessage = t('messages.updateError');
237
- const logoUploadSuccessMessage = t('messages.logoUploadSuccess');
238
- const logoUploadErrorMessage = t('messages.logoUploadError');
239
- const logoRemoveSuccessMessage = t('messages.logoRemoveSuccess');
240
- const logoInvalidTypeMessage = t('messages.logoInvalidType');
241
- const logoTooLargeMessage = t('messages.logoTooLarge');
242
-
243
- const form = useForm<BankAccountFormValues>({
244
- resolver: zodResolver(createBankAccountFormSchema(t)),
245
- defaultValues: {
246
- banco: '',
247
- agencia: '',
248
- conta: '',
249
- tipo: '',
250
- descricao: '',
251
- logoFileId: null,
252
- currencyId: null,
253
- dataInicial: new Date().toISOString().slice(0, 10),
254
- saldoInicial: 0,
255
- },
256
- });
257
-
258
- const [logoFileId, setLogoFileId] = useState<number | null>(null);
259
- const [persistedLogoFileId, setPersistedLogoFileId] = useState<number | null>(
260
- null
261
- );
262
- const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
263
- const [isUploadingLogo, setIsUploadingLogo] = useState(false);
264
- const [logoUploadProgress, setLogoUploadProgress] = useState(0);
265
- const logoInputRef = useRef<HTMLInputElement | null>(null);
266
- const watchedFormValues = useWatch({
267
- control: form.control,
268
- });
269
-
270
- const hasDraftContent = useMemo(
271
- () =>
272
- Boolean(
273
- (watchedFormValues.banco ?? '').trim() ||
274
- (watchedFormValues.agencia ?? '').trim() ||
275
- (watchedFormValues.conta ?? '').trim() ||
276
- (watchedFormValues.tipo ?? '').trim() ||
277
- (watchedFormValues.descricao ?? '').trim() ||
278
- (watchedFormValues.dataInicial ?? '').trim() !==
279
- new Date().toISOString().slice(0, 10) ||
280
- (watchedFormValues.saldoInicial ?? 0) > 0 ||
281
- logoFileId != null
282
- ),
283
- [watchedFormValues, logoFileId]
284
- );
285
-
286
- const draftValue = useMemo<BankAccountDraftPayload>(
287
- () => ({
288
- mode: editingAccount ? 'edit' : 'create',
289
- accountId: editingAccount?.id ?? null,
290
- values: {
291
- banco: watchedFormValues.banco ?? '',
292
- agencia: watchedFormValues.agencia ?? '',
293
- conta: watchedFormValues.conta ?? '',
294
- tipo: watchedFormValues.tipo ?? '',
295
- descricao: watchedFormValues.descricao ?? '',
296
- logoFileId: logoFileId ?? null,
297
- dataInicial:
298
- watchedFormValues.dataInicial ||
299
- new Date().toISOString().slice(0, 10),
300
- saldoInicial: watchedFormValues.saldoInicial ?? 0,
301
- currencyId: watchedFormValues.currencyId ?? null,
302
- },
303
- logoFileId: logoFileId ?? null,
304
- logoPreviewUrl,
305
- currencyId: watchedFormValues.currencyId ?? null,
306
- }),
307
- [editingAccount, logoFileId, logoPreviewUrl, watchedFormValues]
308
- );
309
-
310
- const {
311
- clearDraft,
312
- loadDraft,
313
- hasDraft,
314
- savedAt: draftSavedAt,
315
- } = useFormDraft<BankAccountDraftPayload>({
316
- storageKey: BANK_ACCOUNT_FORM_DRAFT_STORAGE_KEY,
317
- value: draftValue,
318
- hasData: hasDraftContent,
319
- enabled: open,
320
- });
321
-
322
- const draftStatusContent = useMemo(() => {
323
- if (!hasDraft || !draftSavedAt) {
324
- return null;
325
- }
326
-
327
- const savedDate = new Date(draftSavedAt);
328
- if (Number.isNaN(savedDate.getTime())) {
329
- return null;
330
- }
331
-
332
- const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
333
- const relativeLabel = formatDistanceToNow(savedDate, {
334
- addSuffix: true,
335
- locale,
336
- });
337
- const absoluteLabel = formatDateTime(
338
- savedDate,
339
- getSettingValue,
340
- currentLocaleCode
341
- );
342
-
343
- return t('draftStatus', {
344
- relativeLabel,
345
- absoluteLabel,
346
- });
347
- }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft, t]);
348
-
349
- const getLogoUrl = (fileId?: number | null) => {
350
- if (typeof fileId !== 'number' || fileId <= 0) {
351
- return null;
352
- }
353
-
354
- return `${String(process.env.NEXT_PUBLIC_API_BASE_URL || '')}/file/open/${fileId}`;
355
- };
356
-
357
- const deleteFileById = async (fileId?: number | null) => {
358
- if (!fileId || fileId <= 0) {
359
- return;
360
- }
361
-
362
- try {
363
- await request({
364
- url: '/file',
365
- method: 'DELETE',
366
- data: { ids: [fileId] },
367
- });
368
- } catch {
369
- // Ignore cleanup failures to keep the form stable.
370
- }
371
- };
372
-
373
- const cleanupUnsavedLogo = async () => {
374
- if (logoFileId && logoFileId !== persistedLogoFileId) {
375
- await deleteFileById(logoFileId);
376
- }
377
- };
378
-
379
- const handleSelectLogo = () => {
380
- if (!logoInputRef.current || isUploadingLogo) {
381
- return;
382
- }
383
-
384
- logoInputRef.current.value = '';
385
- logoInputRef.current.click();
386
- };
387
-
388
- const handleLogoUpload = async (event: ChangeEvent<HTMLInputElement>) => {
389
- const file = event.target.files?.[0];
390
-
391
- if (!file) {
392
- return;
393
- }
394
-
395
- if (!file.type.startsWith('image/')) {
396
- showToastHandler?.('error', logoInvalidTypeMessage);
397
- event.target.value = '';
398
- return;
399
- }
400
-
401
- if (file.size > 5 * 1024 * 1024) {
402
- showToastHandler?.('error', logoTooLargeMessage);
403
- event.target.value = '';
404
- return;
405
- }
406
-
407
- setIsUploadingLogo(true);
408
- setLogoUploadProgress(0);
409
-
410
- try {
411
- const previousUploadedLogoId = logoFileId;
412
- const formData = new FormData();
413
- formData.append('file', file);
414
- formData.append('destination', 'finance/bank-account/logo');
415
-
416
- const { data } = await request<UploadedFilePayload>({
417
- url: '/file',
418
- method: 'POST',
419
- data: formData,
420
- headers: {
421
- 'Content-Type': 'multipart/form-data',
422
- },
423
- onUploadProgress: (progressEvent) => {
424
- if (!progressEvent.total) {
425
- return;
426
- }
427
-
428
- const progress = Math.round(
429
- (progressEvent.loaded * 100) / progressEvent.total
430
- );
431
- setLogoUploadProgress(progress);
432
- },
433
- });
434
-
435
- const nextLogoFileId = Number(data?.id);
436
-
437
- if (!nextLogoFileId) {
438
- throw new Error('Logo upload failed');
439
- }
440
-
441
- if (
442
- previousUploadedLogoId &&
443
- previousUploadedLogoId !== persistedLogoFileId
444
- ) {
445
- await deleteFileById(previousUploadedLogoId);
446
- }
447
-
448
- setLogoFileId(nextLogoFileId);
449
- setLogoPreviewUrl(`${getLogoUrl(nextLogoFileId)}?ts=${Date.now()}`);
450
- setLogoUploadProgress(100);
451
- form.setValue('logoFileId', nextLogoFileId, {
452
- shouldDirty: true,
453
- shouldValidate: true,
454
- });
455
- showToastHandler?.('success', logoUploadSuccessMessage);
456
- } catch {
457
- showToastHandler?.('error', logoUploadErrorMessage);
458
- setLogoUploadProgress(0);
459
- } finally {
460
- setIsUploadingLogo(false);
461
- event.target.value = '';
462
- }
463
- };
464
-
465
- const handleRemoveLogo = async () => {
466
- if (isUploadingLogo) {
467
- return;
468
- }
469
-
470
- if (logoFileId && logoFileId !== persistedLogoFileId) {
471
- await deleteFileById(logoFileId);
472
- }
473
-
474
- setLogoFileId(null);
475
- setLogoPreviewUrl(null);
476
- setLogoUploadProgress(0);
477
- form.setValue('logoFileId', null, {
478
- shouldDirty: true,
479
- shouldValidate: true,
480
- });
481
- showToastHandler?.('success', logoRemoveSuccessMessage);
482
- };
483
-
484
- useEffect(() => {
485
- if (!open || !editingAccount) {
486
- const brlCurrency = currencies.find((c) => c.code === 'BRL');
487
- if (brlCurrency && open) {
488
- form.setValue('currencyId', Number(brlCurrency.id));
489
- }
490
- }
491
- }, [currencies, open, editingAccount, form]);
492
-
493
- useEffect(() => {
494
- if (!open) {
495
- return;
496
- }
497
-
498
- const storedDraft = loadDraft();
499
-
500
- if (editingAccount) {
501
- const shouldRestoreDraft =
502
- storedDraft?.payload.mode === 'edit' &&
503
- storedDraft.payload.accountId === editingAccount.id;
504
- const currentLogoFileId = shouldRestoreDraft
505
- ? storedDraft.payload.logoFileId
506
- : (editingAccount.logoFileId ?? null);
507
-
508
- setLogoFileId(currentLogoFileId);
509
- setPersistedLogoFileId(editingAccount.logoFileId ?? null);
510
- setLogoPreviewUrl(
511
- shouldRestoreDraft
512
- ? storedDraft.payload.logoPreviewUrl
513
- : currentLogoFileId
514
- ? `${getLogoUrl(currentLogoFileId)}?ts=${Date.now()}`
515
- : null
516
- );
517
- setLogoUploadProgress(0);
518
-
519
- form.reset(
520
- shouldRestoreDraft
521
- ? storedDraft.payload.values
522
- : {
523
- banco: editingAccount.banco,
524
- agencia:
525
- editingAccount.agencia === '-' ? '' : editingAccount.agencia,
526
- conta: editingAccount.conta === '-' ? '' : editingAccount.conta,
527
- tipo: editingAccount.tipo,
528
- descricao: editingAccount.descricao,
529
- logoFileId: currentLogoFileId,
530
- currencyId: editingAccount.currencyId ?? null,
531
- dataInicial:
532
- editingAccount.dataInicial ??
533
- new Date().toISOString().slice(0, 10),
534
- saldoInicial: editingAccount.saldoAtual,
535
- }
536
- );
537
- return;
538
- }
539
-
540
- const shouldRestoreDraft = storedDraft?.payload.mode === 'create';
541
-
542
- setLogoFileId(shouldRestoreDraft ? storedDraft.payload.logoFileId : null);
543
- setPersistedLogoFileId(null);
544
- setLogoPreviewUrl(
545
- shouldRestoreDraft ? storedDraft.payload.logoPreviewUrl : null
546
- );
547
- setLogoUploadProgress(0);
548
-
549
- form.reset(
550
- shouldRestoreDraft
551
- ? storedDraft.payload.values
552
- : {
553
- banco: '',
554
- agencia: '',
555
- conta: '',
556
- tipo: '',
557
- descricao: '',
558
- logoFileId: null,
559
- currencyId: null,
560
- dataInicial: new Date().toISOString().slice(0, 10),
561
- saldoInicial: 0,
562
- }
563
- );
564
- }, [editingAccount, form, loadDraft, open]);
565
-
566
- const handleSubmit = async (values: BankAccountFormValues) => {
567
- const nextLogoFileId = values.logoFileId ?? null;
568
- const previousPersistedLogoId = persistedLogoFileId;
569
-
570
- try {
571
- if (editingAccount) {
572
- await request({
573
- url: `/finance/bank-accounts/${editingAccount.id}`,
574
- method: 'PATCH',
575
- data: {
576
- bank: values.banco,
577
- branch: values.agencia || undefined,
578
- account: values.conta || undefined,
579
- type: values.tipo,
580
- description: values.descricao?.trim() || undefined,
581
- logo_file_id: nextLogoFileId,
582
- currency_id: values.currencyId ?? null,
583
- start_date: values.dataInicial || undefined,
584
- },
585
- });
586
- } else {
587
- await request({
588
- url: '/finance/bank-accounts',
589
- method: 'POST',
590
- data: {
591
- bank: values.banco,
592
- branch: values.agencia || undefined,
593
- account: values.conta || undefined,
594
- type: values.tipo,
595
- description: values.descricao?.trim() || undefined,
596
- logo_file_id: nextLogoFileId,
597
- currency_id: values.currencyId ?? null,
598
- start_date: values.dataInicial || undefined,
599
- initial_balance: values.saldoInicial,
600
- },
601
- });
602
- }
603
-
604
- await onCreated();
605
-
606
- if (
607
- previousPersistedLogoId &&
608
- previousPersistedLogoId !== nextLogoFileId
609
- ) {
610
- await deleteFileById(previousPersistedLogoId);
611
- }
612
-
613
- clearDraft();
614
- setPersistedLogoFileId(nextLogoFileId);
615
- setLogoFileId(nextLogoFileId);
616
- setLogoPreviewUrl(
617
- nextLogoFileId ? `${getLogoUrl(nextLogoFileId)}?ts=${Date.now()}` : null
618
- );
619
- setLogoUploadProgress(0);
620
- form.reset({
621
- banco: '',
622
- agencia: '',
623
- conta: '',
624
- tipo: '',
625
- descricao: '',
626
- logoFileId: null,
627
- currencyId: null,
628
- dataInicial: new Date().toISOString().slice(0, 10),
629
- saldoInicial: 0,
630
- });
631
- onOpenChange(false);
632
- onEditingAccountChange(null);
633
- showToastHandler?.(
634
- 'success',
635
- editingAccount ? updateSuccessMessage : createSuccessMessage
636
- );
637
- } catch {
638
- showToastHandler?.(
639
- 'error',
640
- editingAccount ? updateErrorMessage : createErrorMessage
641
- );
642
- }
643
- };
644
-
645
- const handleCancel = () => {
646
- if (!hasDraftContent) {
647
- void cleanupUnsavedLogo();
648
- }
649
- form.reset();
650
- onEditingAccountChange(null);
651
- onOpenChange(false);
652
- };
653
-
654
- return (
655
- <Sheet
656
- open={open}
657
- onOpenChange={(nextOpen) => {
658
- if (!nextOpen && !hasDraftContent) {
659
- void cleanupUnsavedLogo();
660
- }
661
-
662
- onOpenChange(nextOpen);
663
- if (!nextOpen) {
664
- onEditingAccountChange(null);
665
- }
666
- }}
667
- >
668
- <ResizableSheetContent sheetId="finance-bank-account-form" defaultWidth={672} className="flex h-full w-full flex-col">
669
- <SheetHeader>
670
- <SheetTitle>
671
- {editingAccount ? t('common.edit') : t('newAccount.title')}
672
- </SheetTitle>
673
- <SheetDescription>{t('newAccount.description')}</SheetDescription>
674
- </SheetHeader>
675
- <Form {...form}>
676
- <form
677
- className="flex h-full flex-col"
678
- onSubmit={form.handleSubmit(handleSubmit)}
679
- >
680
- <FinanceSheetBody>
681
- <FinanceSheetSection
682
- title={t('sections.accountData.title')}
683
- description={t('sections.accountData.description')}
684
- >
685
- <FormField
686
- control={form.control}
687
- name="banco"
688
- render={({ field }) => (
689
- <FormItem>
690
- <FormLabel>{t('fields.bank')}</FormLabel>
691
- <FormControl>
692
- <Input
693
- placeholder={t('fields.bankPlaceholder')}
694
- {...field}
695
- />
696
- </FormControl>
697
- <FormMessage />
698
- </FormItem>
699
- )}
700
- />
701
-
702
- <div className="grid gap-4 md:grid-cols-2">
703
- <FormField
704
- control={form.control}
705
- name="agencia"
706
- render={({ field }) => (
707
- <FormItem>
708
- <FormLabel>{t('fields.branch')}</FormLabel>
709
- <FormControl>
710
- <Input
711
- placeholder="0000"
712
- {...field}
713
- value={field.value || ''}
714
- />
715
- </FormControl>
716
- <FormMessage />
717
- </FormItem>
718
- )}
719
- />
720
-
721
- <FormField
722
- control={form.control}
723
- name="conta"
724
- render={({ field }) => (
725
- <FormItem>
726
- <FormLabel>{t('fields.account')}</FormLabel>
727
- <FormControl>
728
- <Input
729
- placeholder="00000-0"
730
- {...field}
731
- value={field.value || ''}
732
- />
733
- </FormControl>
734
- <FormMessage />
735
- </FormItem>
736
- )}
737
- />
738
- </div>
739
-
740
- <FormField
741
- control={form.control}
742
- name="tipo"
743
- render={({ field }) => (
744
- <FormItem>
745
- <FormLabel>{t('fields.type')}</FormLabel>
746
- <Select
747
- value={field.value}
748
- onValueChange={field.onChange}
749
- >
750
- <FormControl>
751
- <SelectTrigger className="w-full">
752
- <SelectValue placeholder={t('common.select')} />
753
- </SelectTrigger>
754
- </FormControl>
755
- <SelectContent>
756
- <SelectItem value="corrente">
757
- {t('types.corrente')}
758
- </SelectItem>
759
- <SelectItem value="poupanca">
760
- {t('types.poupanca')}
761
- </SelectItem>
762
- <SelectItem value="investimento">
763
- {t('types.investimento')}
764
- </SelectItem>
765
- <SelectItem value="caixa">
766
- {t('types.caixa')}
767
- </SelectItem>
768
- </SelectContent>
769
- </Select>
770
- <FormMessage />
771
- </FormItem>
772
- )}
773
- />
774
-
775
- <div className="grid gap-4 md:grid-cols-2">
776
- <FormField
777
- control={form.control}
778
- name="saldoInicial"
779
- render={({ field }) => (
780
- <FormItem>
781
- <FormLabel>{t('fields.initialBalance')}</FormLabel>
782
- <FormControl>
783
- <InputMoney
784
- ref={field.ref}
785
- name={field.name}
786
- value={field.value}
787
- onBlur={field.onBlur}
788
- onValueChange={(value) =>
789
- field.onChange(value ?? 0)
790
- }
791
- placeholder="0,00"
792
- disabled={!!editingAccount}
793
- />
794
- </FormControl>
795
- <FormMessage />
796
- </FormItem>
797
- )}
798
- />
799
-
800
- <FormField
801
- control={form.control}
802
- name="dataInicial"
803
- render={({ field }) => (
804
- <FormItem>
805
- <FormLabel>Data inicial</FormLabel>
806
- <FormControl>
807
- <Input
808
- type="date"
809
- {...field}
810
- onChange={(e) =>
811
- field.onChange(
812
- e.target.value ||
813
- new Date().toISOString().slice(0, 10)
814
- )
815
- }
816
- disabled={!!editingAccount}
817
- />
818
- </FormControl>
819
- <FormMessage />
820
- </FormItem>
821
- )}
822
- />
823
- </div>
824
-
825
- <div className="grid gap-4 md:grid-cols-2">
826
- <EntityPicker<Currency>
827
- form={form as never}
828
- name="currencyId"
829
- label={t('fields.currency')}
830
- placeholder={t('fields.currencyPlaceholder')}
831
- searchPlaceholder={t('fields.currencySearchPlaceholder')}
832
- entityLabel={t('fields.currencyEntityLabel')}
833
- valueType="number"
834
- clearable
835
- options={currencies}
836
- getOptionValue={(c) => c.id}
837
- getOptionLabel={(c) => `${c.symbol} ${c.code} — ${c.name}`}
838
- createTitle={t('fields.currencyCreateTitle')}
839
- createDescription={t('fields.currencyCreateDescription')}
840
- mapSearchToCreateValues={(s) => ({ code: s.toUpperCase() })}
841
- createFields={[
842
- {
843
- name: 'code',
844
- label: t('fields.currencyCode'),
845
- placeholder: t('fields.currencyCodePlaceholder'),
846
- required: true,
847
- },
848
- {
849
- name: 'name',
850
- label: t('fields.currencyName'),
851
- placeholder: t('fields.currencyNamePlaceholder'),
852
- required: true,
853
- },
854
- {
855
- name: 'symbol',
856
- label: t('fields.currencySymbol'),
857
- placeholder: t('fields.currencySymbolPlaceholder'),
858
- required: true,
859
- },
860
- ]}
861
- onCreate={async (values) => {
862
- const response = await request({
863
- url: '/finance/currencies',
864
- method: 'POST',
865
- data: {
866
- code: (values.code ?? '').toUpperCase(),
867
- name: values.name,
868
- symbol: values.symbol,
869
- },
870
- });
871
- void refetchCurrencies();
872
- return response.data as Currency;
873
- }}
874
- />
875
-
876
- <FormField
877
- control={form.control}
878
- name="descricao"
879
- render={({ field }) => (
880
- <FormItem>
881
- <FormLabel>{t('fields.description')}</FormLabel>
882
- <FormControl>
883
- <Input
884
- placeholder={t('fields.descriptionPlaceholder')}
885
- {...field}
886
- value={field.value || ''}
887
- />
888
- </FormControl>
889
- <FormMessage />
890
- </FormItem>
891
- )}
892
- />
893
- </div>
894
- </FinanceSheetSection>
895
-
896
- <FinanceSheetSection
897
- title={t('sections.logo.title')}
898
- description={t('sections.logo.description')}
899
- >
900
- <FormItem>
901
- <FormLabel>{t('fields.logo')}</FormLabel>
902
- <div className="flex flex-col gap-4 rounded-xl border border-dashed border-border/70 bg-muted/20 p-4 sm:flex-row sm:items-start">
903
- <div className="flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
904
- {logoPreviewUrl ? (
905
- <img
906
- src={logoPreviewUrl}
907
- alt={t('fields.logo')}
908
- className="h-full w-full object-cover"
909
- />
910
- ) : (
911
- <Landmark className="h-8 w-8 text-muted-foreground" />
912
- )}
913
- </div>
914
-
915
- <div className="flex-1 space-y-2">
916
- <input
917
- ref={logoInputRef}
918
- type="file"
919
- accept="image/*"
920
- className="hidden"
921
- onChange={handleLogoUpload}
922
- />
923
-
924
- <div className="flex flex-wrap gap-2">
925
- <Button
926
- type="button"
927
- variant="outline"
928
- onClick={handleSelectLogo}
929
- disabled={isUploadingLogo}
930
- >
931
- <Upload className="mr-2 h-4 w-4" />
932
- {t('fields.logoAction')}
933
- </Button>
934
-
935
- {logoFileId ? (
936
- <Button
937
- type="button"
938
- variant="ghost"
939
- onClick={() => void handleRemoveLogo()}
940
- disabled={isUploadingLogo}
941
- >
942
- <Trash2 className="mr-2 h-4 w-4" />
943
- {t('fields.logoRemove')}
944
- </Button>
945
- ) : null}
946
- </div>
947
-
948
- <p className="text-xs text-muted-foreground">
949
- {t('fields.logoHint')}
950
- </p>
951
-
952
- {isUploadingLogo ? (
953
- <p className="text-xs text-muted-foreground">
954
- {t('fields.logoUploading', {
955
- progress: logoUploadProgress,
956
- })}
957
- </p>
958
- ) : null}
959
- </div>
960
- </div>
961
- </FormItem>
962
- </FinanceSheetSection>
963
- </FinanceSheetBody>
964
-
965
- <div className="border-t px-4 py-4 sm:px-6">
966
- {draftStatusContent ? (
967
- <p className="mb-3 text-xs text-muted-foreground">
968
- {draftStatusContent}
969
- </p>
970
- ) : null}
971
- <div className="flex justify-end gap-2">
972
- <Button type="button" variant="outline" onClick={handleCancel}>
973
- {t('common.cancel')}
974
- </Button>
975
- <Button
976
- type="submit"
977
- disabled={form.formState.isSubmitting || isUploadingLogo}
978
- >
979
- {t('common.save')}
980
- </Button>
981
- </div>
982
- </div>
983
- </form>
984
- </Form>
985
- </ResizableSheetContent>
986
- </Sheet>
987
- );
988
- }
989
-
990
103
  export default function ContasBancariasPage() {
991
104
  const t = useTranslations('finance.BankAccountsPage');
992
105
  const { request, showToastHandler, currentLocaleCode } = useApp();
@@ -1148,7 +261,7 @@ export default function ContasBancariasPage() {
1148
261
  }
1149
262
  />
1150
263
 
1151
- <NovaContaSheet
264
+ <BankAccountSheet
1152
265
  t={t}
1153
266
  onCreated={refetch}
1154
267
  open={sheetOpen}