@hed-hog/finance 0.0.232 → 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.
@@ -100,6 +100,7 @@ function NovoTituloSheet({
100
100
  const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
101
101
  const [uploadedFileName, setUploadedFileName] = useState('');
102
102
  const [isUploadingFile, setIsUploadingFile] = useState(false);
103
+ const [isExtractingFileData, setIsExtractingFileData] = useState(false);
103
104
  const [uploadProgress, setUploadProgress] = useState(0);
104
105
 
105
106
  const normalizeFilenameForDisplay = (filename: string) => {
@@ -215,6 +216,95 @@ function NovoTituloSheet({
215
216
  );
216
217
  setUploadProgress(100);
217
218
  showToastHandler?.('success', 'Arquivo relacionado com sucesso');
219
+
220
+ setIsExtractingFileData(true);
221
+ try {
222
+ const extraction = await request<{
223
+ documento?: string | null;
224
+ fornecedorId?: string;
225
+ competencia?: string;
226
+ vencimento?: string;
227
+ valor?: number | null;
228
+ categoriaId?: string;
229
+ centroCustoId?: string;
230
+ metodo?: string;
231
+ descricao?: string | null;
232
+ }>({
233
+ url: '/finance/accounts-payable/installments/extract-from-file',
234
+ method: 'POST',
235
+ data: {
236
+ file_id: data.id,
237
+ },
238
+ });
239
+
240
+ const extracted = extraction.data || {};
241
+
242
+ if (extracted.documento) {
243
+ form.setValue('documento', extracted.documento, {
244
+ shouldValidate: true,
245
+ });
246
+ }
247
+
248
+ if (extracted.fornecedorId) {
249
+ form.setValue('fornecedorId', extracted.fornecedorId, {
250
+ shouldValidate: true,
251
+ });
252
+ }
253
+
254
+ if (extracted.competencia) {
255
+ form.setValue('competencia', extracted.competencia, {
256
+ shouldValidate: true,
257
+ });
258
+ }
259
+
260
+ if (extracted.vencimento) {
261
+ form.setValue('vencimento', extracted.vencimento, {
262
+ shouldValidate: true,
263
+ });
264
+ }
265
+
266
+ if (typeof extracted.valor === 'number' && extracted.valor > 0) {
267
+ form.setValue('valor', extracted.valor, {
268
+ shouldValidate: true,
269
+ });
270
+ }
271
+
272
+ if (extracted.categoriaId) {
273
+ form.setValue('categoriaId', extracted.categoriaId, {
274
+ shouldValidate: true,
275
+ });
276
+ }
277
+
278
+ if (extracted.centroCustoId) {
279
+ form.setValue('centroCustoId', extracted.centroCustoId, {
280
+ shouldValidate: true,
281
+ });
282
+ }
283
+
284
+ if (extracted.metodo) {
285
+ form.setValue('metodo', extracted.metodo, {
286
+ shouldValidate: true,
287
+ });
288
+ }
289
+
290
+ if (extracted.descricao) {
291
+ form.setValue('descricao', extracted.descricao, {
292
+ shouldValidate: true,
293
+ });
294
+ }
295
+
296
+ showToastHandler?.(
297
+ 'success',
298
+ 'Dados da fatura extraídos e preenchidos automaticamente'
299
+ );
300
+ } catch {
301
+ showToastHandler?.(
302
+ 'error',
303
+ 'Não foi possível extrair os dados automaticamente'
304
+ );
305
+ } finally {
306
+ setIsExtractingFileData(false);
307
+ }
218
308
  } catch {
219
309
  setUploadedFileId(null);
220
310
  setUploadedFileName('');
@@ -299,6 +389,11 @@ function NovoTituloSheet({
299
389
  Arquivo relacionado: {uploadedFileName}
300
390
  </p>
301
391
  )}
392
+ {isExtractingFileData && (
393
+ <p className="text-xs text-muted-foreground">
394
+ Extraindo dados da fatura com IA...
395
+ </p>
396
+ )}
302
397
  </div>
303
398
 
304
399
  <FormField
@@ -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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/finance",
3
- "version": "0.0.232",
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",
14
+ "@hed-hog/api-prisma": "0.0.4",
15
15
  "@hed-hog/api-types": "0.0.1",
16
+ "@hed-hog/tag": "0.0.233",
16
17
  "@hed-hog/api": "0.0.3",
17
- "@hed-hog/tag": "0.0.232",
18
- "@hed-hog/contact": "0.0.232"
18
+ "@hed-hog/contact": "0.0.233",
19
+ "@hed-hog/core": "0.0.233"
19
20
  },
20
21
  "exports": {
21
22
  ".": {
@@ -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,