@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.
- package/README.md +1 -22
- package/dist/finance-installments.controller.d.ts +132 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +52 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance-statements.controller.d.ts +8 -0
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.js +40 -0
- package/dist/finance-statements.controller.js.map +1 -1
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +1 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +160 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +626 -8
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +80 -4
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +736 -13
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1 -3
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
- package/hedhog/frontend/messages/en.json +1 -0
- package/hedhog/frontend/messages/pt.json +1 -0
- package/hedhog/query/constraints.sql +86 -0
- package/hedhog/table/bank_account.yaml +0 -8
- package/hedhog/table/financial_title.yaml +1 -9
- package/hedhog/table/settlement.yaml +0 -8
- package/package.json +6 -6
- package/src/finance-installments.controller.ts +70 -10
- package/src/finance-statements.controller.ts +61 -2
- package/src/finance.module.ts +2 -1
- package/src/finance.service.ts +868 -12
- 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 {
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
!['
|
|
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
|
|
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>
|