@hed-hog/finance 0.0.239 → 0.0.244

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 (34) hide show
  1. package/README.md +1 -22
  2. package/dist/finance-installments.controller.d.ts +132 -0
  3. package/dist/finance-installments.controller.d.ts.map +1 -1
  4. package/dist/finance-installments.controller.js +52 -0
  5. package/dist/finance-installments.controller.js.map +1 -1
  6. package/dist/finance-statements.controller.d.ts +8 -0
  7. package/dist/finance-statements.controller.d.ts.map +1 -1
  8. package/dist/finance-statements.controller.js +40 -0
  9. package/dist/finance-statements.controller.js.map +1 -1
  10. package/dist/finance.module.d.ts.map +1 -1
  11. package/dist/finance.module.js +1 -0
  12. package/dist/finance.module.js.map +1 -1
  13. package/dist/finance.service.d.ts +160 -2
  14. package/dist/finance.service.d.ts.map +1 -1
  15. package/dist/finance.service.js +626 -8
  16. package/dist/finance.service.js.map +1 -1
  17. package/hedhog/data/route.yaml +54 -0
  18. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +80 -4
  19. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +736 -13
  20. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +1 -1
  21. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1 -3
  22. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
  23. package/hedhog/frontend/messages/en.json +1 -0
  24. package/hedhog/frontend/messages/pt.json +1 -0
  25. package/hedhog/query/constraints.sql +86 -0
  26. package/hedhog/table/bank_account.yaml +0 -8
  27. package/hedhog/table/financial_title.yaml +1 -9
  28. package/hedhog/table/settlement.yaml +0 -8
  29. package/package.json +6 -6
  30. package/src/finance-installments.controller.ts +70 -10
  31. package/src/finance-statements.controller.ts +61 -2
  32. package/src/finance.module.ts +2 -1
  33. package/src/finance.service.ts +868 -12
  34. package/hedhog/table/branch.yaml +0 -18
@@ -66,7 +66,8 @@ import {
66
66
  } from 'lucide-react';
67
67
  import { useTranslations } from 'next-intl';
68
68
  import Link from 'next/link';
69
- import { useEffect, useRef, useState } from 'react';
69
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
70
+ import { useEffect, useMemo, useRef, useState } from 'react';
70
71
  import { useFieldArray, useForm } from 'react-hook-form';
71
72
  import { z } from 'zod';
72
73
  import { formatarData } from '../../_lib/formatters';
@@ -1055,9 +1056,641 @@ function NovoTituloSheet({
1055
1056
  );
1056
1057
  }
1057
1058
 
1059
+ function EditarTituloSheet({
1060
+ open,
1061
+ onOpenChange,
1062
+ titulo,
1063
+ pessoas,
1064
+ categorias,
1065
+ centrosCusto,
1066
+ t,
1067
+ onUpdated,
1068
+ }: {
1069
+ open: boolean;
1070
+ onOpenChange: (open: boolean) => void;
1071
+ titulo?: any;
1072
+ pessoas: any[];
1073
+ categorias: any[];
1074
+ centrosCusto: any[];
1075
+ t: ReturnType<typeof useTranslations>;
1076
+ onUpdated: () => Promise<any> | void;
1077
+ }) {
1078
+ const { request, showToastHandler } = useApp();
1079
+ const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
1080
+ const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
1081
+ useState(true);
1082
+ const redistributionTimeoutRef = useRef<
1083
+ Record<number, ReturnType<typeof setTimeout>>
1084
+ >({});
1085
+
1086
+ const form = useForm<NewTitleFormValues>({
1087
+ resolver: zodResolver(newTitleFormSchema),
1088
+ defaultValues: {
1089
+ documento: '',
1090
+ fornecedorId: '',
1091
+ competencia: '',
1092
+ vencimento: '',
1093
+ valor: 0,
1094
+ installmentsCount: 1,
1095
+ installments: [{ dueDate: '', amount: 0 }],
1096
+ categoriaId: '',
1097
+ centroCustoId: '',
1098
+ metodo: '',
1099
+ descricao: '',
1100
+ },
1101
+ });
1102
+
1103
+ const { fields: installmentFields, replace: replaceInstallments } =
1104
+ useFieldArray({
1105
+ control: form.control,
1106
+ name: 'installments',
1107
+ });
1108
+
1109
+ const watchedInstallmentsCount = form.watch('installmentsCount');
1110
+ const watchedTotalValue = form.watch('valor');
1111
+ const watchedDueDate = form.watch('vencimento');
1112
+ const watchedInstallments = form.watch('installments');
1113
+
1114
+ const toDateInput = (value?: string) => {
1115
+ if (!value) {
1116
+ return '';
1117
+ }
1118
+
1119
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1120
+ return value;
1121
+ }
1122
+
1123
+ const parsed = new Date(value);
1124
+ if (Number.isNaN(parsed.getTime())) {
1125
+ return '';
1126
+ }
1127
+
1128
+ return parsed.toISOString().slice(0, 10);
1129
+ };
1130
+
1131
+ useEffect(() => {
1132
+ if (!open || !titulo) {
1133
+ return;
1134
+ }
1135
+
1136
+ const installments = Array.isArray(titulo.parcelas) ? titulo.parcelas : [];
1137
+ const normalizedInstallments =
1138
+ installments.length > 0
1139
+ ? installments.map((installment: any) => ({
1140
+ dueDate: toDateInput(installment.vencimento),
1141
+ amount: Number(installment.valor || 0),
1142
+ }))
1143
+ : [
1144
+ {
1145
+ dueDate: toDateInput(titulo?.vencimento),
1146
+ amount: Number(titulo?.valorTotal || 0),
1147
+ },
1148
+ ];
1149
+
1150
+ form.reset({
1151
+ documento: titulo.documento || '',
1152
+ fornecedorId: titulo.fornecedorId || '',
1153
+ competencia: titulo.competencia || '',
1154
+ vencimento:
1155
+ normalizedInstallments[0]?.dueDate || toDateInput(titulo?.vencimento),
1156
+ valor: Number(titulo.valorTotal || 0),
1157
+ installmentsCount: normalizedInstallments.length,
1158
+ installments: normalizedInstallments,
1159
+ categoriaId: titulo.categoriaId || '',
1160
+ centroCustoId: titulo.centroCustoId || '',
1161
+ metodo: installments[0]?.metodoPagamento || '',
1162
+ descricao: titulo.descricao || '',
1163
+ });
1164
+
1165
+ setIsInstallmentsEdited(true);
1166
+ }, [form, open, titulo]);
1167
+
1168
+ useEffect(() => {
1169
+ if (isInstallmentsEdited || !open) {
1170
+ return;
1171
+ }
1172
+
1173
+ replaceInstallments(
1174
+ buildEqualInstallments(
1175
+ watchedInstallmentsCount,
1176
+ watchedTotalValue,
1177
+ watchedDueDate
1178
+ )
1179
+ );
1180
+ }, [
1181
+ isInstallmentsEdited,
1182
+ open,
1183
+ replaceInstallments,
1184
+ watchedDueDate,
1185
+ watchedInstallmentsCount,
1186
+ watchedTotalValue,
1187
+ ]);
1188
+
1189
+ const installmentsTotal = (watchedInstallments || []).reduce(
1190
+ (acc, installment) => acc + Number(installment?.amount || 0),
1191
+ 0
1192
+ );
1193
+ const installmentsDiffCents = Math.abs(
1194
+ Math.round(installmentsTotal * 100) -
1195
+ Math.round((watchedTotalValue || 0) * 100)
1196
+ );
1197
+
1198
+ const clearScheduledRedistribution = (index: number) => {
1199
+ const timeout = redistributionTimeoutRef.current[index];
1200
+ if (!timeout) {
1201
+ return;
1202
+ }
1203
+
1204
+ clearTimeout(timeout);
1205
+ delete redistributionTimeoutRef.current[index];
1206
+ };
1207
+
1208
+ const runInstallmentRedistribution = (index: number) => {
1209
+ if (!autoRedistributeInstallments) {
1210
+ return;
1211
+ }
1212
+
1213
+ const currentInstallments = form.getValues('installments');
1214
+ const editedInstallmentAmount = Number(
1215
+ currentInstallments[index]?.amount || 0
1216
+ );
1217
+
1218
+ const redistributedInstallments = redistributeRemainingInstallments(
1219
+ currentInstallments,
1220
+ index,
1221
+ editedInstallmentAmount,
1222
+ form.getValues('valor')
1223
+ );
1224
+
1225
+ replaceInstallments(redistributedInstallments);
1226
+ };
1227
+
1228
+ const scheduleInstallmentRedistribution = (index: number) => {
1229
+ clearScheduledRedistribution(index);
1230
+
1231
+ redistributionTimeoutRef.current[index] = setTimeout(() => {
1232
+ runInstallmentRedistribution(index);
1233
+ delete redistributionTimeoutRef.current[index];
1234
+ }, INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS);
1235
+ };
1236
+
1237
+ useEffect(() => {
1238
+ if (autoRedistributeInstallments) {
1239
+ return;
1240
+ }
1241
+
1242
+ Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
1243
+ redistributionTimeoutRef.current = {};
1244
+ }, [autoRedistributeInstallments]);
1245
+
1246
+ useEffect(() => {
1247
+ return () => {
1248
+ Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
1249
+ redistributionTimeoutRef.current = {};
1250
+ };
1251
+ }, []);
1252
+
1253
+ const handleSubmit = async (values: NewTitleFormValues) => {
1254
+ if (!titulo?.id) {
1255
+ showToastHandler?.('error', 'Título inválido para edição');
1256
+ return;
1257
+ }
1258
+
1259
+ try {
1260
+ await request({
1261
+ url: `/finance/accounts-payable/installments/${titulo.id}`,
1262
+ method: 'PATCH',
1263
+ data: {
1264
+ document_number: values.documento,
1265
+ person_id: Number(values.fornecedorId),
1266
+ competence_date: values.competencia
1267
+ ? `${values.competencia}-01`
1268
+ : undefined,
1269
+ due_date: values.vencimento,
1270
+ total_amount: values.valor,
1271
+ finance_category_id: values.categoriaId
1272
+ ? Number(values.categoriaId)
1273
+ : undefined,
1274
+ cost_center_id: values.centroCustoId
1275
+ ? Number(values.centroCustoId)
1276
+ : undefined,
1277
+ payment_channel: values.metodo || undefined,
1278
+ description: values.descricao?.trim() || undefined,
1279
+ installments: values.installments.map((installment, index) => ({
1280
+ installment_number: index + 1,
1281
+ due_date: installment.dueDate || values.vencimento,
1282
+ amount: installment.amount,
1283
+ })),
1284
+ },
1285
+ });
1286
+
1287
+ await onUpdated();
1288
+ showToastHandler?.('success', 'Título atualizado com sucesso');
1289
+ onOpenChange(false);
1290
+ } catch {
1291
+ showToastHandler?.('error', 'Não foi possível atualizar o título');
1292
+ }
1293
+ };
1294
+
1295
+ return (
1296
+ <Sheet open={open} onOpenChange={onOpenChange}>
1297
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
1298
+ <SheetHeader>
1299
+ <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1300
+ <SheetDescription>
1301
+ Edite os dados do título enquanto estiver em rascunho.
1302
+ </SheetDescription>
1303
+ </SheetHeader>
1304
+ <Form {...form}>
1305
+ <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
1306
+ <div className="grid gap-4">
1307
+ <FormField
1308
+ control={form.control}
1309
+ name="documento"
1310
+ render={({ field }) => (
1311
+ <FormItem>
1312
+ <FormLabel>{t('fields.document')}</FormLabel>
1313
+ <FormControl>
1314
+ <Input placeholder="NF-00000" {...field} />
1315
+ </FormControl>
1316
+ <FormMessage />
1317
+ </FormItem>
1318
+ )}
1319
+ />
1320
+
1321
+ <FormField
1322
+ control={form.control}
1323
+ name="fornecedorId"
1324
+ render={({ field }) => (
1325
+ <FormItem>
1326
+ <FormLabel>{t('fields.supplier')}</FormLabel>
1327
+ <Select value={field.value} onValueChange={field.onChange}>
1328
+ <FormControl>
1329
+ <SelectTrigger className="w-full">
1330
+ <SelectValue placeholder={t('common.select')} />
1331
+ </SelectTrigger>
1332
+ </FormControl>
1333
+ <SelectContent>
1334
+ {pessoas.map((person) => (
1335
+ <SelectItem key={person.id} value={String(person.id)}>
1336
+ {person.nome}
1337
+ </SelectItem>
1338
+ ))}
1339
+ </SelectContent>
1340
+ </Select>
1341
+ <FormMessage />
1342
+ </FormItem>
1343
+ )}
1344
+ />
1345
+
1346
+ <div className="grid grid-cols-2 gap-4">
1347
+ <FormField
1348
+ control={form.control}
1349
+ name="competencia"
1350
+ render={({ field }) => (
1351
+ <FormItem>
1352
+ <FormLabel>{t('fields.competency')}</FormLabel>
1353
+ <FormControl>
1354
+ <Input
1355
+ type="month"
1356
+ {...field}
1357
+ value={field.value || ''}
1358
+ />
1359
+ </FormControl>
1360
+ <FormMessage />
1361
+ </FormItem>
1362
+ )}
1363
+ />
1364
+
1365
+ <FormField
1366
+ control={form.control}
1367
+ name="vencimento"
1368
+ render={({ field }) => (
1369
+ <FormItem>
1370
+ <FormLabel>{t('fields.dueDate')}</FormLabel>
1371
+ <FormControl>
1372
+ <Input
1373
+ type="date"
1374
+ {...field}
1375
+ value={field.value || ''}
1376
+ />
1377
+ </FormControl>
1378
+ <FormMessage />
1379
+ </FormItem>
1380
+ )}
1381
+ />
1382
+ </div>
1383
+
1384
+ <FormField
1385
+ control={form.control}
1386
+ name="valor"
1387
+ render={({ field }) => (
1388
+ <FormItem>
1389
+ <FormLabel>{t('fields.totalValue')}</FormLabel>
1390
+ <FormControl>
1391
+ <InputMoney
1392
+ ref={field.ref}
1393
+ name={field.name}
1394
+ value={field.value}
1395
+ onBlur={field.onBlur}
1396
+ onValueChange={(value) => field.onChange(value ?? 0)}
1397
+ placeholder="0,00"
1398
+ />
1399
+ </FormControl>
1400
+ <FormMessage />
1401
+ </FormItem>
1402
+ )}
1403
+ />
1404
+
1405
+ <FormField
1406
+ control={form.control}
1407
+ name="installmentsCount"
1408
+ render={({ field }) => (
1409
+ <FormItem>
1410
+ <FormLabel>Quantidade de Parcelas</FormLabel>
1411
+ <FormControl>
1412
+ <Input
1413
+ type="number"
1414
+ min={1}
1415
+ max={120}
1416
+ value={field.value}
1417
+ onChange={(event) => {
1418
+ const nextValue = Number(event.target.value || 1);
1419
+ field.onChange(
1420
+ Number.isNaN(nextValue) ? 1 : nextValue
1421
+ );
1422
+ setIsInstallmentsEdited(false);
1423
+ }}
1424
+ />
1425
+ </FormControl>
1426
+ <FormMessage />
1427
+ </FormItem>
1428
+ )}
1429
+ />
1430
+
1431
+ <div className="space-y-3 rounded-md border p-3">
1432
+ <div className="flex items-center justify-between gap-2">
1433
+ <p className="text-sm font-medium">Parcelas</p>
1434
+ <Button
1435
+ type="button"
1436
+ variant="outline"
1437
+ size="sm"
1438
+ onClick={() => {
1439
+ setIsInstallmentsEdited(false);
1440
+ replaceInstallments(
1441
+ buildEqualInstallments(
1442
+ form.getValues('installmentsCount'),
1443
+ form.getValues('valor'),
1444
+ form.getValues('vencimento')
1445
+ )
1446
+ );
1447
+ }}
1448
+ >
1449
+ Recalcular automaticamente
1450
+ </Button>
1451
+ </div>
1452
+
1453
+ <div className="flex items-center gap-2">
1454
+ <Checkbox
1455
+ id="auto-redistribute-installments-edit-payable"
1456
+ checked={autoRedistributeInstallments}
1457
+ onCheckedChange={(checked) =>
1458
+ setAutoRedistributeInstallments(checked === true)
1459
+ }
1460
+ />
1461
+ <Label
1462
+ htmlFor="auto-redistribute-installments-edit-payable"
1463
+ className="text-xs text-muted-foreground"
1464
+ >
1465
+ Redistribuir automaticamente o restante ao editar parcela
1466
+ </Label>
1467
+ </div>
1468
+
1469
+ <div className="space-y-2">
1470
+ {installmentFields.map((installment, index) => (
1471
+ <div
1472
+ key={installment.id}
1473
+ className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
1474
+ >
1475
+ <div className="flex items-center text-sm text-muted-foreground">
1476
+ #{index + 1}
1477
+ </div>
1478
+
1479
+ <FormField
1480
+ control={form.control}
1481
+ name={`installments.${index}.dueDate` as const}
1482
+ render={({ field }) => (
1483
+ <FormItem>
1484
+ <FormLabel className="text-xs">
1485
+ Vencimento
1486
+ </FormLabel>
1487
+ <FormControl>
1488
+ <Input
1489
+ type="date"
1490
+ {...field}
1491
+ value={field.value || ''}
1492
+ onChange={(event) => {
1493
+ setIsInstallmentsEdited(true);
1494
+ field.onChange(event);
1495
+ }}
1496
+ />
1497
+ </FormControl>
1498
+ <FormMessage />
1499
+ </FormItem>
1500
+ )}
1501
+ />
1502
+
1503
+ <FormField
1504
+ control={form.control}
1505
+ name={`installments.${index}.amount` as const}
1506
+ render={({ field }) => (
1507
+ <FormItem>
1508
+ <FormLabel className="text-xs">Valor</FormLabel>
1509
+ <FormControl>
1510
+ <InputMoney
1511
+ ref={field.ref}
1512
+ name={field.name}
1513
+ value={field.value}
1514
+ onBlur={() => {
1515
+ field.onBlur();
1516
+
1517
+ if (!autoRedistributeInstallments) {
1518
+ return;
1519
+ }
1520
+
1521
+ clearScheduledRedistribution(index);
1522
+ runInstallmentRedistribution(index);
1523
+ }}
1524
+ onValueChange={(value) => {
1525
+ setIsInstallmentsEdited(true);
1526
+ field.onChange(value ?? 0);
1527
+
1528
+ if (!autoRedistributeInstallments) {
1529
+ return;
1530
+ }
1531
+
1532
+ scheduleInstallmentRedistribution(index);
1533
+ }}
1534
+ placeholder="0,00"
1535
+ />
1536
+ </FormControl>
1537
+ <FormMessage />
1538
+ </FormItem>
1539
+ )}
1540
+ />
1541
+ </div>
1542
+ ))}
1543
+ </div>
1544
+
1545
+ <p
1546
+ className={`text-xs ${
1547
+ installmentsDiffCents === 0
1548
+ ? 'text-muted-foreground'
1549
+ : 'text-destructive'
1550
+ }`}
1551
+ >
1552
+ Soma das parcelas: {installmentsTotal.toFixed(2)}
1553
+ {installmentsDiffCents > 0 && ' (ajuste necessário)'}
1554
+ </p>
1555
+ {form.formState.errors.installments?.message && (
1556
+ <p className="text-xs text-destructive">
1557
+ {form.formState.errors.installments.message}
1558
+ </p>
1559
+ )}
1560
+ </div>
1561
+
1562
+ <FormField
1563
+ control={form.control}
1564
+ name="categoriaId"
1565
+ render={({ field }) => (
1566
+ <FormItem>
1567
+ <FormLabel>{t('fields.category')}</FormLabel>
1568
+ <Select value={field.value} onValueChange={field.onChange}>
1569
+ <FormControl>
1570
+ <SelectTrigger className="w-full">
1571
+ <SelectValue placeholder={t('common.select')} />
1572
+ </SelectTrigger>
1573
+ </FormControl>
1574
+ <SelectContent>
1575
+ {categorias
1576
+ .filter((c) => c.natureza === 'despesa')
1577
+ .map((c) => (
1578
+ <SelectItem key={c.id} value={String(c.id)}>
1579
+ {c.codigo} - {c.nome}
1580
+ </SelectItem>
1581
+ ))}
1582
+ </SelectContent>
1583
+ </Select>
1584
+ <FormMessage />
1585
+ </FormItem>
1586
+ )}
1587
+ />
1588
+
1589
+ <FormField
1590
+ control={form.control}
1591
+ name="centroCustoId"
1592
+ render={({ field }) => (
1593
+ <FormItem>
1594
+ <FormLabel>{t('fields.costCenter')}</FormLabel>
1595
+ <Select value={field.value} onValueChange={field.onChange}>
1596
+ <FormControl>
1597
+ <SelectTrigger className="w-full">
1598
+ <SelectValue placeholder={t('common.select')} />
1599
+ </SelectTrigger>
1600
+ </FormControl>
1601
+ <SelectContent>
1602
+ {centrosCusto.map((c) => (
1603
+ <SelectItem key={c.id} value={String(c.id)}>
1604
+ {c.codigo} - {c.nome}
1605
+ </SelectItem>
1606
+ ))}
1607
+ </SelectContent>
1608
+ </Select>
1609
+ <FormMessage />
1610
+ </FormItem>
1611
+ )}
1612
+ />
1613
+
1614
+ <FormField
1615
+ control={form.control}
1616
+ name="metodo"
1617
+ render={({ field }) => (
1618
+ <FormItem>
1619
+ <FormLabel>{t('fields.paymentMethod')}</FormLabel>
1620
+ <Select value={field.value} onValueChange={field.onChange}>
1621
+ <FormControl>
1622
+ <SelectTrigger className="w-full">
1623
+ <SelectValue placeholder={t('common.select')} />
1624
+ </SelectTrigger>
1625
+ </FormControl>
1626
+ <SelectContent>
1627
+ <SelectItem value="boleto">
1628
+ {t('paymentMethods.boleto')}
1629
+ </SelectItem>
1630
+ <SelectItem value="pix">PIX</SelectItem>
1631
+ <SelectItem value="transferencia">
1632
+ {t('paymentMethods.transfer')}
1633
+ </SelectItem>
1634
+ <SelectItem value="cartao">
1635
+ {t('paymentMethods.card')}
1636
+ </SelectItem>
1637
+ <SelectItem value="dinheiro">
1638
+ {t('paymentMethods.cash')}
1639
+ </SelectItem>
1640
+ <SelectItem value="cheque">
1641
+ {t('paymentMethods.check')}
1642
+ </SelectItem>
1643
+ </SelectContent>
1644
+ </Select>
1645
+ <FormMessage />
1646
+ </FormItem>
1647
+ )}
1648
+ />
1649
+
1650
+ <FormField
1651
+ control={form.control}
1652
+ name="descricao"
1653
+ render={({ field }) => (
1654
+ <FormItem>
1655
+ <FormLabel>{t('fields.description')}</FormLabel>
1656
+ <FormControl>
1657
+ <Textarea
1658
+ placeholder={t('newTitle.descriptionPlaceholder')}
1659
+ {...field}
1660
+ value={field.value || ''}
1661
+ />
1662
+ </FormControl>
1663
+ <FormMessage />
1664
+ </FormItem>
1665
+ )}
1666
+ />
1667
+ </div>
1668
+
1669
+ <div className="flex justify-end gap-2 pt-4">
1670
+ <Button
1671
+ type="button"
1672
+ variant="outline"
1673
+ onClick={() => onOpenChange(false)}
1674
+ >
1675
+ {t('common.cancel')}
1676
+ </Button>
1677
+ <Button type="submit" disabled={form.formState.isSubmitting}>
1678
+ {t('common.save')}
1679
+ </Button>
1680
+ </div>
1681
+ </form>
1682
+ </Form>
1683
+ </SheetContent>
1684
+ </Sheet>
1685
+ );
1686
+ }
1687
+
1058
1688
  export default function TitulosPagarPage() {
1059
1689
  const t = useTranslations('finance.PayableInstallmentsPage');
1060
1690
  const { request, currentLocaleCode, showToastHandler } = useApp();
1691
+ const pathname = usePathname();
1692
+ const router = useRouter();
1693
+ const searchParams = useSearchParams();
1061
1694
  const { data, refetch } = useFinanceData();
1062
1695
  const { titulosPagar, pessoas } = data;
1063
1696
 
@@ -1095,6 +1728,76 @@ export default function TitulosPagarPage() {
1095
1728
 
1096
1729
  const [search, setSearch] = useState('');
1097
1730
  const [statusFilter, setStatusFilter] = useState<string>('');
1731
+ const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
1732
+ const [cancelingTitleId, setCancelingTitleId] = useState<string | null>(null);
1733
+
1734
+ const editingTitle = useMemo(
1735
+ () => titulosPagar.find((item) => item.id === editingTitleId),
1736
+ [editingTitleId, titulosPagar]
1737
+ );
1738
+
1739
+ useEffect(() => {
1740
+ const editId = searchParams.get('editId');
1741
+ if (!editId || editingTitleId) {
1742
+ return;
1743
+ }
1744
+
1745
+ const foundTitle = titulosPagar.find((item) => item.id === editId);
1746
+ if (!foundTitle) {
1747
+ return;
1748
+ }
1749
+
1750
+ if (foundTitle.status !== 'rascunho') {
1751
+ showToastHandler?.(
1752
+ 'error',
1753
+ 'Apenas títulos em rascunho podem ser editados'
1754
+ );
1755
+ router.replace(pathname, { scroll: false });
1756
+ return;
1757
+ }
1758
+
1759
+ setEditingTitleId(editId);
1760
+ }, [
1761
+ editingTitleId,
1762
+ pathname,
1763
+ router,
1764
+ searchParams,
1765
+ showToastHandler,
1766
+ titulosPagar,
1767
+ ]);
1768
+
1769
+ const closeEditSheet = (open: boolean) => {
1770
+ if (open) {
1771
+ return;
1772
+ }
1773
+
1774
+ setEditingTitleId(null);
1775
+ if (searchParams.get('editId')) {
1776
+ router.replace(pathname, { scroll: false });
1777
+ }
1778
+ };
1779
+
1780
+ const handleCancelTitle = async (titleId: string) => {
1781
+ if (!titleId || cancelingTitleId) {
1782
+ return;
1783
+ }
1784
+
1785
+ setCancelingTitleId(titleId);
1786
+ try {
1787
+ await request({
1788
+ url: `/finance/accounts-payable/installments/${titleId}/cancel`,
1789
+ method: 'PATCH',
1790
+ data: {},
1791
+ });
1792
+
1793
+ await refetch();
1794
+ showToastHandler?.('success', 'Título cancelado com sucesso');
1795
+ } catch {
1796
+ showToastHandler?.('error', 'Não foi possível cancelar o título');
1797
+ } finally {
1798
+ setCancelingTitleId(null);
1799
+ }
1800
+ };
1098
1801
 
1099
1802
  const filteredTitulos = titulosPagar.filter((titulo) => {
1100
1803
  const matchesSearch =
@@ -1142,12 +1845,24 @@ export default function TitulosPagarPage() {
1142
1845
  { label: t('breadcrumbs.current') },
1143
1846
  ]}
1144
1847
  actions={
1145
- <NovoTituloSheet
1146
- categorias={categorias}
1147
- centrosCusto={centrosCusto}
1148
- t={t}
1149
- onCreated={refetch}
1150
- />
1848
+ <>
1849
+ <NovoTituloSheet
1850
+ categorias={categorias}
1851
+ centrosCusto={centrosCusto}
1852
+ t={t}
1853
+ onCreated={refetch}
1854
+ />
1855
+ <EditarTituloSheet
1856
+ open={!!editingTitleId && !!editingTitle}
1857
+ onOpenChange={closeEditSheet}
1858
+ titulo={editingTitle}
1859
+ pessoas={pessoas}
1860
+ categorias={categorias}
1861
+ centrosCusto={centrosCusto}
1862
+ t={t}
1863
+ onUpdated={refetch}
1864
+ />
1865
+ </>
1151
1866
  }
1152
1867
  />
1153
1868
 
@@ -1164,7 +1879,6 @@ export default function TitulosPagarPage() {
1164
1879
  options: [
1165
1880
  { value: 'all', label: t('statuses.all') },
1166
1881
  { value: 'rascunho', label: t('statuses.rascunho') },
1167
- { value: 'aprovado', label: t('statuses.aprovado') },
1168
1882
  { value: 'aberto', label: t('statuses.aberto') },
1169
1883
  { value: 'parcial', label: t('statuses.parcial') },
1170
1884
  { value: 'liquidado', label: t('statuses.liquidado') },
@@ -1262,7 +1976,10 @@ export default function TitulosPagarPage() {
1262
1976
  {t('table.actions.viewDetails')}
1263
1977
  </Link>
1264
1978
  </DropdownMenuItem>
1265
- <DropdownMenuItem>
1979
+ <DropdownMenuItem
1980
+ disabled={titulo.status !== 'rascunho'}
1981
+ onClick={() => setEditingTitleId(titulo.id)}
1982
+ >
1266
1983
  <Edit className="mr-2 h-4 w-4" />
1267
1984
  {t('table.actions.edit')}
1268
1985
  </DropdownMenuItem>
@@ -1275,9 +1992,7 @@ export default function TitulosPagarPage() {
1275
1992
  </DropdownMenuItem>
1276
1993
  <DropdownMenuItem
1277
1994
  disabled={
1278
- !['aprovado', 'aberto', 'parcial'].includes(
1279
- titulo.status
1280
- )
1995
+ !['aberto', 'parcial'].includes(titulo.status)
1281
1996
  }
1282
1997
  >
1283
1998
  <Download className="mr-2 h-4 w-4" />
@@ -1292,7 +2007,15 @@ export default function TitulosPagarPage() {
1292
2007
  {t('table.actions.reverse')}
1293
2008
  </DropdownMenuItem>
1294
2009
  <DropdownMenuSeparator />
1295
- <DropdownMenuItem className="text-destructive">
2010
+ <DropdownMenuItem
2011
+ className="text-destructive"
2012
+ disabled={
2013
+ ['cancelado', 'liquidado'].includes(
2014
+ titulo.status
2015
+ ) || cancelingTitleId === titulo.id
2016
+ }
2017
+ onClick={() => void handleCancelTitle(titulo.id)}
2018
+ >
1296
2019
  <XCircle className="mr-2 h-4 w-4" />
1297
2020
  {t('table.actions.cancel')}
1298
2021
  </DropdownMenuItem>