@hed-hog/finance 0.0.231 → 0.0.233

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 (29) 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 +11 -0
  4. package/dist/dto/create-financial-title.dto.js.map +1 -1
  5. package/dist/dto/extract-financial-title-from-file.dto.d.ts +4 -0
  6. package/dist/dto/extract-financial-title-from-file.dto.d.ts.map +1 -0
  7. package/dist/dto/extract-financial-title-from-file.dto.js +24 -0
  8. package/dist/dto/extract-financial-title-from-file.dto.js.map +1 -0
  9. package/dist/finance-installments.controller.d.ts +39 -0
  10. package/dist/finance-installments.controller.d.ts.map +1 -1
  11. package/dist/finance-installments.controller.js +26 -0
  12. package/dist/finance-installments.controller.js.map +1 -1
  13. package/dist/finance.module.d.ts.map +1 -1
  14. package/dist/finance.module.js +3 -1
  15. package/dist/finance.module.js.map +1 -1
  16. package/dist/finance.service.d.ts +33 -1
  17. package/dist/finance.service.d.ts.map +1 -1
  18. package/dist/finance.service.js +213 -2
  19. package/dist/finance.service.js.map +1 -1
  20. package/hedhog/data/route.yaml +18 -0
  21. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +234 -4
  22. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +205 -0
  23. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +78 -61
  24. package/package.json +6 -5
  25. package/src/dto/create-financial-title.dto.ts +12 -0
  26. package/src/dto/extract-financial-title-from-file.dto.ts +9 -0
  27. package/src/finance-installments.controller.ts +33 -7
  28. package/src/finance.module.ts +3 -1
  29. package/src/finance.service.ts +251 -3
@@ -22,6 +22,7 @@ import {
22
22
  import { Input } from '@/components/ui/input';
23
23
  import { InputMoney } from '@/components/ui/input-money';
24
24
  import { Money } from '@/components/ui/money';
25
+ import { Progress } from '@/components/ui/progress';
25
26
  import {
26
27
  Select,
27
28
  SelectContent,
@@ -95,6 +96,29 @@ function NovoTituloSheet({
95
96
  }) {
96
97
  const { request, showToastHandler } = useApp();
97
98
  const [open, setOpen] = useState(false);
99
+ const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
100
+ const [uploadedFileName, setUploadedFileName] = useState('');
101
+ const [isUploadingFile, setIsUploadingFile] = useState(false);
102
+ const [isExtractingFileData, setIsExtractingFileData] = useState(false);
103
+ const [uploadProgress, setUploadProgress] = useState(0);
104
+
105
+ const normalizeFilenameForDisplay = (filename: string) => {
106
+ if (!filename) {
107
+ return filename;
108
+ }
109
+
110
+ if (!/Ã.|Â.|â[\u0080-\u00BF]/.test(filename)) {
111
+ return filename;
112
+ }
113
+
114
+ try {
115
+ const bytes = Uint8Array.from(filename, (char) => char.charCodeAt(0));
116
+ const decoded = new TextDecoder('utf-8').decode(bytes);
117
+ return /Ã.|Â.|â[\u0080-\u00BF]/.test(decoded) ? filename : decoded;
118
+ } catch {
119
+ return filename;
120
+ }
121
+ };
98
122
 
99
123
  const form = useForm<NewTitleFormValues>({
100
124
  resolver: zodResolver(newTitleFormSchema),
@@ -132,11 +156,14 @@ function NovoTituloSheet({
132
156
  : undefined,
133
157
  payment_channel: values.canal || undefined,
134
158
  description: values.descricao?.trim() || undefined,
159
+ attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
135
160
  },
136
161
  });
137
162
 
138
163
  await onCreated();
139
164
  form.reset();
165
+ setUploadedFileId(null);
166
+ setUploadedFileName('');
140
167
  setOpen(false);
141
168
  showToastHandler?.('success', 'Título criado com sucesso');
142
169
  } catch {
@@ -146,9 +173,147 @@ function NovoTituloSheet({
146
173
 
147
174
  const handleCancel = () => {
148
175
  form.reset();
176
+ setUploadedFileId(null);
177
+ setUploadedFileName('');
178
+ setUploadProgress(0);
149
179
  setOpen(false);
150
180
  };
151
181
 
182
+ const uploadRelatedFile = async (file: File) => {
183
+ setIsUploadingFile(true);
184
+ setUploadProgress(0);
185
+
186
+ try {
187
+ const formData = new FormData();
188
+ formData.append('file', file);
189
+ formData.append('destination', 'finance/titles');
190
+
191
+ const { data } = await request<{ id: number; filename: string }>({
192
+ url: '/file',
193
+ method: 'POST',
194
+ data: formData,
195
+ headers: {
196
+ 'Content-Type': 'multipart/form-data',
197
+ },
198
+ onUploadProgress: (event) => {
199
+ if (!event.total) {
200
+ return;
201
+ }
202
+
203
+ const progress = Math.round((event.loaded * 100) / event.total);
204
+ setUploadProgress(progress);
205
+ },
206
+ });
207
+
208
+ if (!data?.id) {
209
+ throw new Error('Arquivo inválido');
210
+ }
211
+
212
+ setUploadedFileId(data.id);
213
+ setUploadedFileName(
214
+ normalizeFilenameForDisplay(data.filename || file.name)
215
+ );
216
+ setUploadProgress(100);
217
+ showToastHandler?.('success', 'Arquivo relacionado com sucesso');
218
+
219
+ setIsExtractingFileData(true);
220
+ try {
221
+ const extraction = await request<{
222
+ documento?: string | null;
223
+ clienteId?: string;
224
+ competencia?: string;
225
+ vencimento?: string;
226
+ valor?: number | null;
227
+ categoriaId?: string;
228
+ centroCustoId?: string;
229
+ canal?: string;
230
+ descricao?: string | null;
231
+ }>({
232
+ url: '/finance/accounts-receivable/installments/extract-from-file',
233
+ method: 'POST',
234
+ data: {
235
+ file_id: data.id,
236
+ },
237
+ });
238
+
239
+ const extracted = extraction.data || {};
240
+
241
+ if (extracted.documento) {
242
+ form.setValue('documento', extracted.documento, {
243
+ shouldValidate: true,
244
+ });
245
+ }
246
+
247
+ if (extracted.clienteId) {
248
+ form.setValue('clienteId', extracted.clienteId, {
249
+ shouldValidate: true,
250
+ });
251
+ }
252
+
253
+ if (extracted.competencia) {
254
+ form.setValue('competencia', extracted.competencia, {
255
+ shouldValidate: true,
256
+ });
257
+ }
258
+
259
+ if (extracted.vencimento) {
260
+ form.setValue('vencimento', extracted.vencimento, {
261
+ shouldValidate: true,
262
+ });
263
+ }
264
+
265
+ if (typeof extracted.valor === 'number' && extracted.valor > 0) {
266
+ form.setValue('valor', extracted.valor, {
267
+ shouldValidate: true,
268
+ });
269
+ }
270
+
271
+ if (extracted.categoriaId) {
272
+ form.setValue('categoriaId', extracted.categoriaId, {
273
+ shouldValidate: true,
274
+ });
275
+ }
276
+
277
+ if (extracted.centroCustoId) {
278
+ form.setValue('centroCustoId', extracted.centroCustoId, {
279
+ shouldValidate: true,
280
+ });
281
+ }
282
+
283
+ if (extracted.canal) {
284
+ form.setValue('canal', extracted.canal, {
285
+ shouldValidate: true,
286
+ });
287
+ }
288
+
289
+ if (extracted.descricao) {
290
+ form.setValue('descricao', extracted.descricao, {
291
+ shouldValidate: true,
292
+ });
293
+ }
294
+
295
+ showToastHandler?.(
296
+ 'success',
297
+ 'Dados da fatura extraídos e preenchidos automaticamente'
298
+ );
299
+ } catch {
300
+ showToastHandler?.(
301
+ 'error',
302
+ 'Não foi possível extrair os dados automaticamente'
303
+ );
304
+ } finally {
305
+ setIsExtractingFileData(false);
306
+ }
307
+ } catch {
308
+ setUploadedFileId(null);
309
+ setUploadedFileName('');
310
+ setUploadProgress(0);
311
+ showToastHandler?.('error', 'Não foi possível enviar o arquivo');
312
+ } finally {
313
+ setIsUploadingFile(false);
314
+ }
315
+ };
316
+
152
317
  return (
153
318
  <Sheet open={open} onOpenChange={setOpen}>
154
319
  <SheetTrigger asChild>
@@ -165,6 +330,46 @@ function NovoTituloSheet({
165
330
  <Form {...form}>
166
331
  <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
167
332
  <div className="grid gap-4">
333
+ <div className="grid gap-2">
334
+ <FormLabel>Arquivo da fatura (opcional)</FormLabel>
335
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
336
+ <Input
337
+ type="file"
338
+ accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
339
+ onChange={(event) => {
340
+ const file = event.target.files?.[0];
341
+ if (!file) {
342
+ return;
343
+ }
344
+
345
+ setUploadedFileId(null);
346
+ setUploadedFileName('');
347
+ setUploadProgress(0);
348
+ void uploadRelatedFile(file);
349
+ }}
350
+ disabled={isUploadingFile || form.formState.isSubmitting}
351
+ />
352
+ </div>
353
+ {isUploadingFile && (
354
+ <div className="space-y-1">
355
+ <Progress value={uploadProgress} className="h-2" />
356
+ <p className="text-xs text-muted-foreground">
357
+ Upload em andamento: {uploadProgress}%
358
+ </p>
359
+ </div>
360
+ )}
361
+ {uploadedFileId && (
362
+ <p className="text-xs text-muted-foreground">
363
+ Arquivo relacionado: {uploadedFileName}
364
+ </p>
365
+ )}
366
+ {isExtractingFileData && (
367
+ <p className="text-xs text-muted-foreground">
368
+ Extraindo dados da fatura com IA...
369
+ </p>
370
+ )}
371
+ </div>
372
+
168
373
  <FormField
169
374
  control={form.control}
170
375
  name="documento"
@@ -285,37 +285,64 @@ function NovaContaSheet({
285
285
  />
286
286
  </div>
287
287
 
288
- <FormField
289
- control={form.control}
290
- name="tipo"
291
- render={({ field }) => (
292
- <FormItem>
293
- <FormLabel>{t('fields.type')}</FormLabel>
294
- <Select value={field.value} onValueChange={field.onChange}>
288
+ <div className="grid grid-cols-2 gap-4">
289
+ <FormField
290
+ control={form.control}
291
+ name="tipo"
292
+ render={({ field }) => (
293
+ <FormItem>
294
+ <FormLabel>{t('fields.type')}</FormLabel>
295
+ <Select
296
+ value={field.value}
297
+ onValueChange={field.onChange}
298
+ >
299
+ <FormControl>
300
+ <SelectTrigger className="w-full">
301
+ <SelectValue placeholder={t('common.select')} />
302
+ </SelectTrigger>
303
+ </FormControl>
304
+ <SelectContent>
305
+ <SelectItem value="corrente">
306
+ {t('types.corrente')}
307
+ </SelectItem>
308
+ <SelectItem value="poupanca">
309
+ {t('types.poupanca')}
310
+ </SelectItem>
311
+ <SelectItem value="investimento">
312
+ {t('types.investimento')}
313
+ </SelectItem>
314
+ <SelectItem value="caixa">
315
+ {t('types.caixa')}
316
+ </SelectItem>
317
+ </SelectContent>
318
+ </Select>
319
+ <FormMessage />
320
+ </FormItem>
321
+ )}
322
+ />
323
+
324
+ <FormField
325
+ control={form.control}
326
+ name="saldoInicial"
327
+ render={({ field }) => (
328
+ <FormItem>
329
+ <FormLabel>{t('fields.initialBalance')}</FormLabel>
295
330
  <FormControl>
296
- <SelectTrigger>
297
- <SelectValue placeholder={t('common.select')} />
298
- </SelectTrigger>
331
+ <InputMoney
332
+ ref={field.ref}
333
+ name={field.name}
334
+ value={field.value}
335
+ onBlur={field.onBlur}
336
+ onValueChange={(value) => field.onChange(value ?? 0)}
337
+ placeholder="0,00"
338
+ disabled={!!editingAccount}
339
+ />
299
340
  </FormControl>
300
- <SelectContent>
301
- <SelectItem value="corrente">
302
- {t('types.corrente')}
303
- </SelectItem>
304
- <SelectItem value="poupanca">
305
- {t('types.poupanca')}
306
- </SelectItem>
307
- <SelectItem value="investimento">
308
- {t('types.investimento')}
309
- </SelectItem>
310
- <SelectItem value="caixa">
311
- {t('types.caixa')}
312
- </SelectItem>
313
- </SelectContent>
314
- </Select>
315
- <FormMessage />
316
- </FormItem>
317
- )}
318
- />
341
+ <FormMessage />
342
+ </FormItem>
343
+ )}
344
+ />
345
+ </div>
319
346
 
320
347
  <FormField
321
348
  control={form.control}
@@ -334,28 +361,6 @@ function NovaContaSheet({
334
361
  </FormItem>
335
362
  )}
336
363
  />
337
-
338
- <FormField
339
- control={form.control}
340
- name="saldoInicial"
341
- render={({ field }) => (
342
- <FormItem>
343
- <FormLabel>{t('fields.initialBalance')}</FormLabel>
344
- <FormControl>
345
- <InputMoney
346
- ref={field.ref}
347
- name={field.name}
348
- value={field.value}
349
- onBlur={field.onBlur}
350
- onValueChange={(value) => field.onChange(value ?? 0)}
351
- placeholder="0,00"
352
- disabled={!!editingAccount}
353
- />
354
- </FormControl>
355
- <FormMessage />
356
- </FormItem>
357
- )}
358
- />
359
364
  </div>
360
365
 
361
366
  <div className="flex justify-end gap-2 pt-4">
@@ -412,6 +417,7 @@ export default function ContasBancariasPage() {
412
417
  },
413
418
  placeholderData: [],
414
419
  });
420
+ const accounts = contasBancarias ?? [];
415
421
 
416
422
  const tipoConfig = {
417
423
  corrente: { label: t('types.corrente'), icon: Building2 },
@@ -420,11 +426,11 @@ export default function ContasBancariasPage() {
420
426
  caixa: { label: t('types.caixa'), icon: Wallet },
421
427
  };
422
428
 
423
- const saldoTotal = contasBancarias
429
+ const saldoTotal = accounts
424
430
  .filter((c) => c.ativo)
425
431
  .reduce((acc, c) => acc + c.saldoAtual, 0);
426
432
 
427
- const saldoConciliadoTotal = contasBancarias
433
+ const saldoConciliadoTotal = accounts
428
434
  .filter((c) => c.ativo)
429
435
  .reduce((acc, c) => acc + c.saldoConciliado, 0);
430
436
 
@@ -522,7 +528,7 @@ export default function ContasBancariasPage() {
522
528
  </div>
523
529
  <p className="text-xs text-muted-foreground">
524
530
  {t('cards.activeAccounts', {
525
- count: contasBancarias.filter((c) => c.ativo).length,
531
+ count: accounts.filter((c) => c.ativo).length,
526
532
  })}
527
533
  </p>
528
534
  </CardContent>
@@ -547,7 +553,7 @@ export default function ContasBancariasPage() {
547
553
  </div>
548
554
 
549
555
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
550
- {contasBancarias.map((conta) => {
556
+ {accounts.map((conta) => {
551
557
  const tipo =
552
558
  tipoConfig[conta.tipo as keyof typeof tipoConfig] ||
553
559
  tipoConfig.corrente;
@@ -563,14 +569,25 @@ export default function ContasBancariasPage() {
563
569
  <TipoIcon className="h-5 w-5" />
564
570
  </div>
565
571
  <div>
566
- <CardTitle className="text-base">{conta.banco}</CardTitle>
567
- <CardDescription>
568
- {conta.agencia !== '-'
569
- ? t('accountCard.bankAccount', {
572
+ <div className="flex gap-4 items-center">
573
+ <CardTitle className="text-base">
574
+ {conta.banco}
575
+ </CardTitle>
576
+ {conta.descricao && (
577
+ <span className="block text-muted-foreground text-xs">
578
+ {conta.descricao}
579
+ </span>
580
+ )}
581
+ </div>
582
+ <CardDescription className="space-y-0.5">
583
+ {conta.agencia !== '-' && (
584
+ <span className="block">
585
+ {t('accountCard.bankAccount', {
570
586
  agency: conta.agencia,
571
587
  account: conta.conta,
572
- })
573
- : conta.descricao}
588
+ })}
589
+ </span>
590
+ )}
574
591
  </CardDescription>
575
592
  </div>
576
593
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/finance",
3
- "version": "0.0.231",
3
+ "version": "0.0.233",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -10,12 +10,13 @@
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
12
  "@hed-hog/api-locale": "0.0.11",
13
- "@hed-hog/api-prisma": "0.0.4",
14
13
  "@hed-hog/api-pagination": "0.0.5",
15
- "@hed-hog/tag": "0.0.223",
16
- "@hed-hog/api": "0.0.3",
14
+ "@hed-hog/api-prisma": "0.0.4",
17
15
  "@hed-hog/api-types": "0.0.1",
18
- "@hed-hog/contact": "0.0.223"
16
+ "@hed-hog/tag": "0.0.233",
17
+ "@hed-hog/api": "0.0.3",
18
+ "@hed-hog/contact": "0.0.233",
19
+ "@hed-hog/core": "0.0.233"
19
20
  },
20
21
  "exports": {
21
22
  ".": {
@@ -139,4 +139,16 @@ export class CreateFinancialTitleDto {
139
139
  @ValidateNested({ each: true })
140
140
  @Type(() => CreateFinancialInstallmentDto)
141
141
  installments?: CreateFinancialInstallmentDto[];
142
+
143
+ @IsOptional()
144
+ @IsArray({
145
+ message: (args) =>
146
+ getLocaleText('validation.attachmentFileIdsMustBeArray', args.value),
147
+ })
148
+ @IsInt({
149
+ each: true,
150
+ message: (args) =>
151
+ getLocaleText('validation.attachmentFileIdMustBeNumber', args.value),
152
+ })
153
+ attachment_file_ids?: number[];
142
154
  }
@@ -0,0 +1,9 @@
1
+ import { Type } from 'class-transformer';
2
+ import { IsInt, IsOptional } from 'class-validator';
3
+
4
+ export class ExtractFinancialTitleFromFileDto {
5
+ @IsOptional()
6
+ @Type(() => Number)
7
+ @IsInt()
8
+ file_id?: number;
9
+ }
@@ -2,15 +2,19 @@ import { Role, User } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
3
  import { Pagination } from '@hed-hog/api-pagination';
4
4
  import {
5
- Body,
6
- Controller,
7
- Get,
8
- Param,
9
- ParseIntPipe,
10
- Post,
11
- Query,
5
+ Body,
6
+ Controller,
7
+ Get,
8
+ Param,
9
+ ParseIntPipe,
10
+ Post,
11
+ Query,
12
+ UploadedFile,
13
+ UseInterceptors,
12
14
  } from '@nestjs/common';
15
+ import { FileInterceptor } from '@nestjs/platform-express';
13
16
  import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
17
+ import { ExtractFinancialTitleFromFileDto } from './dto/extract-financial-title-from-file.dto';
14
18
  import { FinanceService } from './finance.service';
15
19
 
16
20
  @Role()
@@ -50,6 +54,15 @@ export class FinanceInstallmentsController {
50
54
  );
51
55
  }
52
56
 
57
+ @Post('accounts-payable/installments/extract-from-file')
58
+ @UseInterceptors(FileInterceptor('file'))
59
+ async extractAccountsPayableInfoFromFile(
60
+ @UploadedFile() file: MulterFile,
61
+ @Body() data: ExtractFinancialTitleFromFileDto,
62
+ ) {
63
+ return this.financeService.getAgentExtractInfoFromFile(file, data.file_id);
64
+ }
65
+
53
66
  @Get('accounts-receivable/installments')
54
67
  async listAccountsReceivableInstallments(
55
68
  @Pagination() paginationParams,
@@ -81,4 +94,17 @@ export class FinanceInstallmentsController {
81
94
  user?.id,
82
95
  );
83
96
  }
97
+
98
+ @Post('accounts-receivable/installments/extract-from-file')
99
+ @UseInterceptors(FileInterceptor('file'))
100
+ async extractAccountsReceivableInfoFromFile(
101
+ @UploadedFile() file: MulterFile,
102
+ @Body() data: ExtractFinancialTitleFromFileDto,
103
+ ) {
104
+ return this.financeService.getAgentExtractInfoFromFile(
105
+ file,
106
+ data.file_id,
107
+ 'receivable',
108
+ );
109
+ }
84
110
  }
@@ -1,6 +1,7 @@
1
1
  import { LocaleModule } from '@hed-hog/api-locale';
2
2
  import { PaginationModule } from '@hed-hog/api-pagination';
3
3
  import { PrismaModule } from '@hed-hog/api-prisma';
4
+ import { AiModule } from '@hed-hog/core';
4
5
  import { forwardRef, Module } from '@nestjs/common';
5
6
  import { ConfigModule } from '@nestjs/config';
6
7
  import { FinanceBankAccountsController } from './finance-bank-accounts.controller';
@@ -14,7 +15,8 @@ import { FinanceService } from './finance.service';
14
15
  ConfigModule.forRoot(),
15
16
  forwardRef(() => PaginationModule),
16
17
  forwardRef(() => PrismaModule),
17
- forwardRef(() => LocaleModule)
18
+ forwardRef(() => LocaleModule),
19
+ forwardRef(() => AiModule),
18
20
  ],
19
21
  controllers: [
20
22
  FinanceDataController,