@hed-hog/finance 0.0.237 → 0.0.239

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 (47) hide show
  1. package/dist/dto/create-finance-tag.dto.d.ts +5 -0
  2. package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
  3. package/dist/dto/create-finance-tag.dto.js +29 -0
  4. package/dist/dto/create-finance-tag.dto.js.map +1 -0
  5. package/dist/dto/reject-title.dto.d.ts +4 -0
  6. package/dist/dto/reject-title.dto.d.ts.map +1 -0
  7. package/dist/dto/reject-title.dto.js +22 -0
  8. package/dist/dto/reject-title.dto.js.map +1 -0
  9. package/dist/dto/reverse-settlement.dto.d.ts +4 -0
  10. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
  11. package/dist/dto/reverse-settlement.dto.js +22 -0
  12. package/dist/dto/reverse-settlement.dto.js.map +1 -0
  13. package/dist/dto/settle-installment.dto.d.ts +12 -0
  14. package/dist/dto/settle-installment.dto.d.ts.map +1 -0
  15. package/dist/dto/settle-installment.dto.js +71 -0
  16. package/dist/dto/settle-installment.dto.js.map +1 -0
  17. package/dist/dto/update-installment-tags.dto.d.ts +4 -0
  18. package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
  19. package/dist/dto/update-installment-tags.dto.js +27 -0
  20. package/dist/dto/update-installment-tags.dto.js.map +1 -0
  21. package/dist/finance-data.controller.d.ts +17 -5
  22. package/dist/finance-data.controller.d.ts.map +1 -1
  23. package/dist/finance-installments.controller.d.ts +325 -8
  24. package/dist/finance-installments.controller.d.ts.map +1 -1
  25. package/dist/finance-installments.controller.js +128 -0
  26. package/dist/finance-installments.controller.js.map +1 -1
  27. package/dist/finance.service.d.ts +357 -13
  28. package/dist/finance.service.d.ts.map +1 -1
  29. package/dist/finance.service.js +835 -64
  30. package/dist/finance.service.js.map +1 -1
  31. package/hedhog/data/route.yaml +90 -0
  32. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
  33. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
  34. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +601 -79
  35. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +481 -19
  36. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +598 -69
  37. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +472 -15
  38. package/hedhog/frontend/messages/en.json +38 -0
  39. package/hedhog/frontend/messages/pt.json +38 -0
  40. package/package.json +5 -5
  41. package/src/dto/create-finance-tag.dto.ts +15 -0
  42. package/src/dto/reject-title.dto.ts +7 -0
  43. package/src/dto/reverse-settlement.dto.ts +7 -0
  44. package/src/dto/settle-installment.dto.ts +55 -0
  45. package/src/dto/update-installment-tags.dto.ts +12 -0
  46. package/src/finance-installments.controller.ts +145 -9
  47. package/src/finance.service.ts +1333 -165
@@ -4,6 +4,7 @@ import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/finance/_componen
4
4
  import { Page, PageHeader } from '@/components/entity-list';
5
5
  import { Badge } from '@/components/ui/badge';
6
6
  import { Button } from '@/components/ui/button';
7
+ import { Checkbox } from '@/components/ui/checkbox';
7
8
  import {
8
9
  DropdownMenu,
9
10
  DropdownMenuContent,
@@ -22,6 +23,7 @@ import {
22
23
  } from '@/components/ui/form';
23
24
  import { Input } from '@/components/ui/input';
24
25
  import { InputMoney } from '@/components/ui/input-money';
26
+ import { Label } from '@/components/ui/label';
25
27
  import { Money } from '@/components/ui/money';
26
28
  import { Progress } from '@/components/ui/progress';
27
29
  import {
@@ -63,23 +65,159 @@ import {
63
65
  } from 'lucide-react';
64
66
  import { useTranslations } from 'next-intl';
65
67
  import Link from 'next/link';
66
- import { useState } from 'react';
67
- import { useForm } from 'react-hook-form';
68
+ import { useEffect, useRef, useState } from 'react';
69
+ import { useFieldArray, useForm } from 'react-hook-form';
68
70
  import { z } from 'zod';
69
71
  import { formatarData } from '../../_lib/formatters';
70
72
  import { useFinanceData } from '../../_lib/use-finance-data';
71
73
 
72
- const newTitleFormSchema = z.object({
73
- documento: z.string().trim().min(1, 'Documento é obrigatório'),
74
- clienteId: z.string().min(1, 'Cliente é obrigatório'),
75
- competencia: z.string().optional(),
76
- vencimento: z.string().min(1, 'Vencimento é obrigatório'),
77
- valor: z.number().min(0.01, 'Valor deve ser maior que zero'),
78
- categoriaId: z.string().optional(),
79
- centroCustoId: z.string().optional(),
80
- canal: z.string().optional(),
81
- descricao: z.string().optional(),
82
- });
74
+ const INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS = 300;
75
+
76
+ const addMonthsToDate = (date: string, monthsToAdd: number) => {
77
+ if (!date) {
78
+ return '';
79
+ }
80
+
81
+ const baseDate = new Date(`${date}T00:00:00`);
82
+ if (Number.isNaN(baseDate.getTime())) {
83
+ return date;
84
+ }
85
+
86
+ const nextDate = new Date(baseDate);
87
+ nextDate.setMonth(nextDate.getMonth() + monthsToAdd);
88
+ return nextDate.toISOString().slice(0, 10);
89
+ };
90
+
91
+ const buildEqualInstallments = (
92
+ installmentsCount: number,
93
+ totalAmount: number,
94
+ firstDueDate: string
95
+ ) => {
96
+ const safeInstallmentsCount = Math.max(1, Math.floor(installmentsCount || 1));
97
+ const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
98
+ const baseCents = Math.floor(totalCents / safeInstallmentsCount);
99
+ const remainder = totalCents % safeInstallmentsCount;
100
+
101
+ return Array.from({ length: safeInstallmentsCount }, (_, index) => ({
102
+ dueDate: addMonthsToDate(firstDueDate, index),
103
+ amount: (baseCents + (index < remainder ? 1 : 0)) / 100,
104
+ }));
105
+ };
106
+
107
+ const redistributeRemainingInstallments = (
108
+ installments: Array<{ dueDate: string; amount: number }>,
109
+ editedIndex: number,
110
+ editedAmount: number,
111
+ totalAmount: number
112
+ ) => {
113
+ if (installments.length === 0) {
114
+ return installments;
115
+ }
116
+
117
+ const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
118
+
119
+ if (installments.length === 1) {
120
+ return installments.map((installment, index) =>
121
+ index === editedIndex
122
+ ? {
123
+ ...installment,
124
+ amount: totalCents / 100,
125
+ }
126
+ : installment
127
+ );
128
+ }
129
+
130
+ const minimumPerInstallmentCents = totalCents >= installments.length ? 1 : 0;
131
+ const remainingInstallmentsCount = installments.length - 1;
132
+
133
+ let editedCents = Math.round((editedAmount || 0) * 100);
134
+ const minEditedCents = minimumPerInstallmentCents;
135
+ const maxEditedCents =
136
+ totalCents - remainingInstallmentsCount * minimumPerInstallmentCents;
137
+
138
+ if (maxEditedCents < minEditedCents) {
139
+ editedCents = Math.min(Math.max(editedCents, 0), totalCents);
140
+ } else {
141
+ editedCents = Math.min(
142
+ Math.max(editedCents, minEditedCents),
143
+ maxEditedCents
144
+ );
145
+ }
146
+
147
+ const remainingCents = totalCents - editedCents;
148
+ const baseCents = Math.floor(remainingCents / remainingInstallmentsCount);
149
+ let remainderCents = remainingCents % remainingInstallmentsCount;
150
+ let processedInstallments = 0;
151
+
152
+ return installments.map((installment, index) => {
153
+ if (index === editedIndex) {
154
+ return {
155
+ ...installment,
156
+ amount: editedCents / 100,
157
+ };
158
+ }
159
+
160
+ const amountCents =
161
+ baseCents + (processedInstallments < remainderCents ? 1 : 0);
162
+ processedInstallments += 1;
163
+
164
+ return {
165
+ ...installment,
166
+ amount: amountCents / 100,
167
+ };
168
+ });
169
+ };
170
+
171
+ const newTitleFormSchema = z
172
+ .object({
173
+ documento: z.string().trim().min(1, 'Documento é obrigatório'),
174
+ clienteId: z.string().min(1, 'Cliente é obrigatório'),
175
+ competencia: z.string().optional(),
176
+ vencimento: z.string().min(1, 'Vencimento é obrigatório'),
177
+ valor: z.number().min(0.01, 'Valor deve ser maior que zero'),
178
+ installmentsCount: z.coerce
179
+ .number({ invalid_type_error: 'Quantidade de parcelas inválida' })
180
+ .int('Quantidade de parcelas inválida')
181
+ .min(1, 'Mínimo de 1 parcela')
182
+ .max(120, 'Máximo de 120 parcelas'),
183
+ installments: z
184
+ .array(
185
+ z.object({
186
+ dueDate: z.string().min(1, 'Vencimento da parcela é obrigatório'),
187
+ amount: z
188
+ .number()
189
+ .min(0.01, 'Valor da parcela deve ser maior que zero'),
190
+ })
191
+ )
192
+ .min(1, 'Informe ao menos uma parcela'),
193
+ categoriaId: z.string().optional(),
194
+ centroCustoId: z.string().optional(),
195
+ canal: z.string().optional(),
196
+ descricao: z.string().optional(),
197
+ })
198
+ .superRefine((values, ctx) => {
199
+ if (values.installments.length !== values.installmentsCount) {
200
+ ctx.addIssue({
201
+ code: z.ZodIssueCode.custom,
202
+ path: ['installments'],
203
+ message: 'Quantidade de parcelas não confere com o detalhamento',
204
+ });
205
+ }
206
+
207
+ const installmentsTotalCents = values.installments.reduce(
208
+ (acc, installment) => acc + Math.round((installment.amount || 0) * 100),
209
+ 0
210
+ );
211
+ const totalCents = Math.round((values.valor || 0) * 100);
212
+
213
+ if (installmentsTotalCents !== totalCents) {
214
+ ctx.addIssue({
215
+ code: z.ZodIssueCode.custom,
216
+ path: ['installments'],
217
+ message: 'A soma das parcelas deve ser igual ao valor total',
218
+ });
219
+ }
220
+ });
83
221
 
84
222
  type NewTitleFormValues = z.infer<typeof newTitleFormSchema>;
85
223
 
@@ -100,6 +238,12 @@ function NovoTituloSheet({
100
238
  const [uploadedFileName, setUploadedFileName] = useState('');
101
239
  const [isUploadingFile, setIsUploadingFile] = useState(false);
102
240
  const [isExtractingFileData, setIsExtractingFileData] = useState(false);
241
+ const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
242
+ const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
243
+ useState(true);
244
+ const redistributionTimeoutRef = useRef<
245
+ Record<number, ReturnType<typeof setTimeout>>
246
+ >({});
103
247
  const [extractionConfidence, setExtractionConfidence] = useState<
104
248
  number | null
105
249
  >(null);
@@ -132,6 +276,8 @@ function NovoTituloSheet({
132
276
  competencia: '',
133
277
  vencimento: '',
134
278
  valor: 0,
279
+ installmentsCount: 1,
280
+ installments: [{ dueDate: '', amount: 0 }],
135
281
  categoriaId: '',
136
282
  centroCustoId: '',
137
283
  canal: '',
@@ -139,6 +285,101 @@ function NovoTituloSheet({
139
285
  },
140
286
  });
141
287
 
288
+ const { fields: installmentFields, replace: replaceInstallments } =
289
+ useFieldArray({
290
+ control: form.control,
291
+ name: 'installments',
292
+ });
293
+
294
+ const watchedInstallmentsCount = form.watch('installmentsCount');
295
+ const watchedTotalValue = form.watch('valor');
296
+ const watchedDueDate = form.watch('vencimento');
297
+ const watchedInstallments = form.watch('installments');
298
+
299
+ useEffect(() => {
300
+ if (isInstallmentsEdited) {
301
+ return;
302
+ }
303
+
304
+ replaceInstallments(
305
+ buildEqualInstallments(
306
+ watchedInstallmentsCount,
307
+ watchedTotalValue,
308
+ watchedDueDate
309
+ )
310
+ );
311
+ }, [
312
+ isInstallmentsEdited,
313
+ replaceInstallments,
314
+ watchedDueDate,
315
+ watchedInstallmentsCount,
316
+ watchedTotalValue,
317
+ ]);
318
+
319
+ const installmentsTotal = (watchedInstallments || []).reduce(
320
+ (acc, installment) => acc + Number(installment?.amount || 0),
321
+ 0
322
+ );
323
+ const installmentsDiffCents = Math.abs(
324
+ Math.round(installmentsTotal * 100) -
325
+ Math.round((watchedTotalValue || 0) * 100)
326
+ );
327
+
328
+ const clearScheduledRedistribution = (index: number) => {
329
+ const timeout = redistributionTimeoutRef.current[index];
330
+ if (!timeout) {
331
+ return;
332
+ }
333
+
334
+ clearTimeout(timeout);
335
+ delete redistributionTimeoutRef.current[index];
336
+ };
337
+
338
+ const runInstallmentRedistribution = (index: number) => {
339
+ if (!autoRedistributeInstallments) {
340
+ return;
341
+ }
342
+
343
+ const currentInstallments = form.getValues('installments');
344
+ const editedInstallmentAmount = Number(
345
+ currentInstallments[index]?.amount || 0
346
+ );
347
+
348
+ const redistributedInstallments = redistributeRemainingInstallments(
349
+ currentInstallments,
350
+ index,
351
+ editedInstallmentAmount,
352
+ form.getValues('valor')
353
+ );
354
+
355
+ replaceInstallments(redistributedInstallments);
356
+ };
357
+
358
+ const scheduleInstallmentRedistribution = (index: number) => {
359
+ clearScheduledRedistribution(index);
360
+
361
+ redistributionTimeoutRef.current[index] = setTimeout(() => {
362
+ runInstallmentRedistribution(index);
363
+ delete redistributionTimeoutRef.current[index];
364
+ }, INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS);
365
+ };
366
+
367
+ useEffect(() => {
368
+ if (autoRedistributeInstallments) {
369
+ return;
370
+ }
371
+
372
+ Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
373
+ redistributionTimeoutRef.current = {};
374
+ }, [autoRedistributeInstallments]);
375
+
376
+ useEffect(() => {
377
+ return () => {
378
+ Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
379
+ redistributionTimeoutRef.current = {};
380
+ };
381
+ }, []);
382
+
142
383
  const handleSubmit = async (values: NewTitleFormValues) => {
143
384
  try {
144
385
  await request({
@@ -160,6 +401,11 @@ function NovoTituloSheet({
160
401
  : undefined,
161
402
  payment_channel: values.canal || undefined,
162
403
  description: values.descricao?.trim() || undefined,
404
+ installments: values.installments.map((installment, index) => ({
405
+ installment_number: index + 1,
406
+ due_date: installment.dueDate || values.vencimento,
407
+ amount: installment.amount,
408
+ })),
163
409
  attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
164
410
  },
165
411
  });
@@ -170,6 +416,8 @@ function NovoTituloSheet({
170
416
  setUploadedFileName('');
171
417
  setExtractionConfidence(null);
172
418
  setExtractionWarnings([]);
419
+ setIsInstallmentsEdited(false);
420
+ setAutoRedistributeInstallments(true);
173
421
  setOpen(false);
174
422
  showToastHandler?.('success', 'Título criado com sucesso');
175
423
  } catch {
@@ -184,6 +432,8 @@ function NovoTituloSheet({
184
432
  setExtractionConfidence(null);
185
433
  setExtractionWarnings([]);
186
434
  setUploadProgress(0);
435
+ setIsInstallmentsEdited(false);
436
+ setAutoRedistributeInstallments(true);
187
437
  setOpen(false);
188
438
  };
189
439
 
@@ -506,6 +756,168 @@ function NovoTituloSheet({
506
756
  )}
507
757
  />
508
758
 
759
+ <FormField
760
+ control={form.control}
761
+ name="installmentsCount"
762
+ render={({ field }) => (
763
+ <FormItem>
764
+ <FormLabel>Quantidade de Parcelas</FormLabel>
765
+ <FormControl>
766
+ <Input
767
+ type="number"
768
+ min={1}
769
+ max={120}
770
+ value={field.value}
771
+ onChange={(event) => {
772
+ const nextValue = Number(event.target.value || 1);
773
+ field.onChange(
774
+ Number.isNaN(nextValue) ? 1 : nextValue
775
+ );
776
+ }}
777
+ />
778
+ </FormControl>
779
+ <FormMessage />
780
+ </FormItem>
781
+ )}
782
+ />
783
+
784
+ <div className="space-y-3 rounded-md border p-3">
785
+ <div className="flex items-center justify-between gap-2">
786
+ <p className="text-sm font-medium">Parcelas</p>
787
+ <Button
788
+ type="button"
789
+ variant="outline"
790
+ size="sm"
791
+ onClick={() => {
792
+ setIsInstallmentsEdited(false);
793
+ replaceInstallments(
794
+ buildEqualInstallments(
795
+ form.getValues('installmentsCount'),
796
+ form.getValues('valor'),
797
+ form.getValues('vencimento')
798
+ )
799
+ );
800
+ }}
801
+ >
802
+ Recalcular automaticamente
803
+ </Button>
804
+ </div>
805
+
806
+ <div className="flex items-center gap-2">
807
+ <Checkbox
808
+ id="auto-redistribute-installments-receivable"
809
+ checked={autoRedistributeInstallments}
810
+ onCheckedChange={(checked) =>
811
+ setAutoRedistributeInstallments(checked === true)
812
+ }
813
+ />
814
+ <Label
815
+ htmlFor="auto-redistribute-installments-receivable"
816
+ className="text-xs text-muted-foreground"
817
+ >
818
+ Redistribuir automaticamente o restante ao editar parcela
819
+ </Label>
820
+ </div>
821
+ {autoRedistributeInstallments && (
822
+ <p className="text-xs text-muted-foreground">
823
+ A redistribuição ocorre ao parar de digitar e ao sair do
824
+ campo.
825
+ </p>
826
+ )}
827
+
828
+ <div className="space-y-2">
829
+ {installmentFields.map((installment, index) => (
830
+ <div
831
+ key={installment.id}
832
+ className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
833
+ >
834
+ <div className="flex items-center text-sm text-muted-foreground">
835
+ #{index + 1}
836
+ </div>
837
+
838
+ <FormField
839
+ control={form.control}
840
+ name={`installments.${index}.dueDate` as const}
841
+ render={({ field }) => (
842
+ <FormItem>
843
+ <FormLabel className="text-xs">
844
+ Vencimento
845
+ </FormLabel>
846
+ <FormControl>
847
+ <Input
848
+ type="date"
849
+ {...field}
850
+ value={field.value || ''}
851
+ onChange={(event) => {
852
+ setIsInstallmentsEdited(true);
853
+ field.onChange(event);
854
+ }}
855
+ />
856
+ </FormControl>
857
+ <FormMessage />
858
+ </FormItem>
859
+ )}
860
+ />
861
+
862
+ <FormField
863
+ control={form.control}
864
+ name={`installments.${index}.amount` as const}
865
+ render={({ field }) => (
866
+ <FormItem>
867
+ <FormLabel className="text-xs">Valor</FormLabel>
868
+ <FormControl>
869
+ <InputMoney
870
+ ref={field.ref}
871
+ name={field.name}
872
+ value={field.value}
873
+ onBlur={() => {
874
+ field.onBlur();
875
+
876
+ if (!autoRedistributeInstallments) {
877
+ return;
878
+ }
879
+
880
+ clearScheduledRedistribution(index);
881
+ runInstallmentRedistribution(index);
882
+ }}
883
+ onValueChange={(value) => {
884
+ setIsInstallmentsEdited(true);
885
+ field.onChange(value ?? 0);
886
+
887
+ if (!autoRedistributeInstallments) {
888
+ return;
889
+ }
890
+
891
+ scheduleInstallmentRedistribution(index);
892
+ }}
893
+ placeholder="0,00"
894
+ />
895
+ </FormControl>
896
+ <FormMessage />
897
+ </FormItem>
898
+ )}
899
+ />
900
+ </div>
901
+ ))}
902
+ </div>
903
+
904
+ <p
905
+ className={`text-xs ${
906
+ installmentsDiffCents === 0
907
+ ? 'text-muted-foreground'
908
+ : 'text-destructive'
909
+ }`}
910
+ >
911
+ Soma das parcelas: {installmentsTotal.toFixed(2)}
912
+ {installmentsDiffCents > 0 && ' (ajuste necessário)'}
913
+ </p>
914
+ {form.formState.errors.installments?.message && (
915
+ <p className="text-xs text-destructive">
916
+ {form.formState.errors.installments.message}
917
+ </p>
918
+ )}
919
+ </div>
920
+
509
921
  <FormField
510
922
  control={form.control}
511
923
  name="categoriaId"
@@ -638,6 +1050,7 @@ function NovoTituloSheet({
638
1050
 
639
1051
  export default function TitulosReceberPage() {
640
1052
  const t = useTranslations('finance.ReceivableInstallmentsPage');
1053
+ const { request, showToastHandler } = useApp();
641
1054
  const { data, refetch } = useFinanceData();
642
1055
  const { titulosReceber, pessoas, categorias, centrosCusto } = data;
643
1056
 
@@ -674,6 +1087,29 @@ export default function TitulosReceberPage() {
674
1087
  return matchesSearch && matchesStatus;
675
1088
  });
676
1089
 
1090
+ const handleOpenAttachment = async (fileId?: string) => {
1091
+ if (!fileId) {
1092
+ return;
1093
+ }
1094
+
1095
+ try {
1096
+ const response = await request<{ url?: string }>({
1097
+ url: `/file/open/${fileId}`,
1098
+ method: 'PUT',
1099
+ });
1100
+
1101
+ const url = response?.data?.url;
1102
+ if (!url) {
1103
+ showToastHandler?.('error', 'Não foi possível abrir o anexo');
1104
+ return;
1105
+ }
1106
+
1107
+ window.open(url, '_blank', 'noopener,noreferrer');
1108
+ } catch {
1109
+ showToastHandler?.('error', 'Não foi possível abrir o anexo');
1110
+ }
1111
+ };
1112
+
677
1113
  return (
678
1114
  <Page>
679
1115
  <PageHeader
@@ -754,7 +1190,22 @@ export default function TitulosReceberPage() {
754
1190
  {titulo.documento}
755
1191
  </Link>
756
1192
  {titulo.anexos.length > 0 && (
757
- <Paperclip className="ml-1 inline h-3 w-3 text-muted-foreground" />
1193
+ <Button
1194
+ type="button"
1195
+ variant="ghost"
1196
+ size="icon"
1197
+ className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
1198
+ onClick={(event) => {
1199
+ event.preventDefault();
1200
+ event.stopPropagation();
1201
+ const firstAttachmentId =
1202
+ titulo.anexosDetalhes?.[0]?.id;
1203
+ void handleOpenAttachment(firstAttachmentId);
1204
+ }}
1205
+ aria-label="Abrir anexo"
1206
+ >
1207
+ <Paperclip className="h-3 w-3" />
1208
+ </Button>
758
1209
  )}
759
1210
  </TableCell>
760
1211
  <TableCell>{cliente?.nome}</TableCell>
@@ -799,7 +1250,13 @@ export default function TitulosReceberPage() {
799
1250
  {t('table.actions.edit')}
800
1251
  </DropdownMenuItem>
801
1252
  <DropdownMenuSeparator />
802
- <DropdownMenuItem>
1253
+ <DropdownMenuItem
1254
+ disabled={
1255
+ !['aprovado', 'aberto', 'parcial'].includes(
1256
+ titulo.status
1257
+ )
1258
+ }
1259
+ >
803
1260
  <Download className="mr-2 h-4 w-4" />
804
1261
  {t('table.actions.registerReceipt')}
805
1262
  </DropdownMenuItem>
@@ -209,6 +209,25 @@
209
209
  "createdAt": "Created At",
210
210
  "tags": "Tags"
211
211
  },
212
+ "tagSelector": {
213
+ "addTag": "Add tag",
214
+ "sheetTitle": "Manage tags",
215
+ "sheetDescription": "Select existing tags or create a new one.",
216
+ "createLabel": "New tag",
217
+ "createPlaceholder": "Type the tag name",
218
+ "createAction": "Create tag",
219
+ "popularTitle": "Most used tags",
220
+ "selectedTitle": "Selected tags",
221
+ "noTags": "No tags",
222
+ "cancel": "Cancel",
223
+ "apply": "Apply",
224
+ "removeTagAria": "Remove tag {tag}",
225
+ "messages": {
226
+ "createSuccess": "Tag created successfully",
227
+ "createError": "Could not create the tag",
228
+ "updateError": "Could not update tags"
229
+ }
230
+ },
212
231
  "attachments": {
213
232
  "title": "Attachments",
214
233
  "description": "Related documents",
@@ -415,6 +434,25 @@
415
434
  "channel": "Channel",
416
435
  "tags": "Tags"
417
436
  },
437
+ "tagSelector": {
438
+ "addTag": "Add tag",
439
+ "sheetTitle": "Manage tags",
440
+ "sheetDescription": "Select existing tags or create a new one.",
441
+ "createLabel": "New tag",
442
+ "createPlaceholder": "Type the tag name",
443
+ "createAction": "Create tag",
444
+ "popularTitle": "Most used tags",
445
+ "selectedTitle": "Selected tags",
446
+ "noTags": "No tags",
447
+ "cancel": "Cancel",
448
+ "apply": "Apply",
449
+ "removeTagAria": "Remove tag {tag}",
450
+ "messages": {
451
+ "createSuccess": "Tag created successfully",
452
+ "createError": "Could not create the tag",
453
+ "updateError": "Could not update tags"
454
+ }
455
+ },
418
456
  "attachments": {
419
457
  "title": "Attachments",
420
458
  "description": "Related documents",
@@ -209,6 +209,25 @@
209
209
  "createdAt": "Data de Criação",
210
210
  "tags": "Tags"
211
211
  },
212
+ "tagSelector": {
213
+ "addTag": "Adicionar tag",
214
+ "sheetTitle": "Gerenciar tags",
215
+ "sheetDescription": "Selecione tags existentes ou crie uma nova.",
216
+ "createLabel": "Nova tag",
217
+ "createPlaceholder": "Digite o nome da tag",
218
+ "createAction": "Criar tag",
219
+ "popularTitle": "Tags mais usadas",
220
+ "selectedTitle": "Tags selecionadas",
221
+ "noTags": "Sem tags",
222
+ "cancel": "Cancelar",
223
+ "apply": "Aplicar",
224
+ "removeTagAria": "Remover tag {tag}",
225
+ "messages": {
226
+ "createSuccess": "Tag criada com sucesso",
227
+ "createError": "Não foi possível criar a tag",
228
+ "updateError": "Não foi possível atualizar as tags"
229
+ }
230
+ },
212
231
  "attachments": {
213
232
  "title": "Anexos",
214
233
  "description": "Documentos relacionados",
@@ -415,6 +434,25 @@
415
434
  "channel": "Canal",
416
435
  "tags": "Tags"
417
436
  },
437
+ "tagSelector": {
438
+ "addTag": "Adicionar tag",
439
+ "sheetTitle": "Gerenciar tags",
440
+ "sheetDescription": "Selecione tags existentes ou crie uma nova.",
441
+ "createLabel": "Nova tag",
442
+ "createPlaceholder": "Digite o nome da tag",
443
+ "createAction": "Criar tag",
444
+ "popularTitle": "Tags mais usadas",
445
+ "selectedTitle": "Tags selecionadas",
446
+ "noTags": "Sem tags",
447
+ "cancel": "Cancelar",
448
+ "apply": "Aplicar",
449
+ "removeTagAria": "Remover tag {tag}",
450
+ "messages": {
451
+ "createSuccess": "Tag criada com sucesso",
452
+ "createError": "Não foi possível criar a tag",
453
+ "updateError": "Não foi possível atualizar as tags"
454
+ }
455
+ },
418
456
  "attachments": {
419
457
  "title": "Anexos",
420
458
  "description": "Documentos relacionados",