@hed-hog/finance 0.0.253 → 0.0.257
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/dist/finance-installments.controller.d.ts +66 -2
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance.service.d.ts +66 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +63 -7
- package/dist/finance.service.js.map +1 -1
- package/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +572 -0
- package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +244 -0
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +143 -51
- package/hedhog/frontend/app/_lib/title-action-rules.ts.ejs +36 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +189 -242
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1189 -545
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +176 -133
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1459 -312
- package/hedhog/frontend/app/page.tsx.ejs +15 -4
- package/hedhog/frontend/messages/en.json +294 -5
- package/hedhog/frontend/messages/pt.json +294 -5
- package/package.json +5 -5
- package/src/finance.service.ts +85 -10
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogCancel,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
} from '@/components/ui/alert-dialog';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
} from '@/components/ui/dropdown-menu';
|
|
20
|
+
import { Input } from '@/components/ui/input';
|
|
21
|
+
import {
|
|
22
|
+
Sheet,
|
|
23
|
+
SheetContent,
|
|
24
|
+
SheetDescription,
|
|
25
|
+
SheetHeader,
|
|
26
|
+
SheetTitle,
|
|
27
|
+
} from '@/components/ui/sheet';
|
|
28
|
+
import {
|
|
29
|
+
CheckCircle,
|
|
30
|
+
Download,
|
|
31
|
+
Edit,
|
|
32
|
+
Eye,
|
|
33
|
+
MoreHorizontal,
|
|
34
|
+
Undo,
|
|
35
|
+
XCircle,
|
|
36
|
+
} from 'lucide-react';
|
|
37
|
+
import Link from 'next/link';
|
|
38
|
+
import { useState } from 'react';
|
|
39
|
+
|
|
40
|
+
type FinanceTitleActionsMenuLabels = {
|
|
41
|
+
menu: string;
|
|
42
|
+
srActions: string;
|
|
43
|
+
viewDetails?: string;
|
|
44
|
+
edit: string;
|
|
45
|
+
approve: string;
|
|
46
|
+
settle: string;
|
|
47
|
+
reverse: string;
|
|
48
|
+
cancel: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type FinanceTitleActionsMenuDialogLabels = {
|
|
52
|
+
cancelTitle: string;
|
|
53
|
+
cancelDescription: string;
|
|
54
|
+
cancelButton: string;
|
|
55
|
+
confirmCancelButton: string;
|
|
56
|
+
reverseTitle: string;
|
|
57
|
+
reverseDescription: string;
|
|
58
|
+
reverseReasonLabel: string;
|
|
59
|
+
reverseReasonPlaceholder: string;
|
|
60
|
+
reverseButton: string;
|
|
61
|
+
confirmReverseButton: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type FinanceTitleActionsMenuProps = {
|
|
65
|
+
detailHref?: string;
|
|
66
|
+
triggerVariant?: 'ghost' | 'outline';
|
|
67
|
+
canEdit: boolean;
|
|
68
|
+
canApprove: boolean;
|
|
69
|
+
canSettle: boolean;
|
|
70
|
+
canReverse: boolean;
|
|
71
|
+
canCancel: boolean;
|
|
72
|
+
isApproving?: boolean;
|
|
73
|
+
isReversing?: boolean;
|
|
74
|
+
isCanceling?: boolean;
|
|
75
|
+
labels: FinanceTitleActionsMenuLabels;
|
|
76
|
+
dialogs: FinanceTitleActionsMenuDialogLabels;
|
|
77
|
+
onEdit: () => void;
|
|
78
|
+
onApprove: () => void;
|
|
79
|
+
onSettle: () => void;
|
|
80
|
+
onReverse: (reason?: string) => Promise<void> | void;
|
|
81
|
+
onCancel: () => Promise<void> | void;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function FinanceTitleActionsMenu({
|
|
85
|
+
detailHref,
|
|
86
|
+
triggerVariant = 'outline',
|
|
87
|
+
canEdit,
|
|
88
|
+
canApprove,
|
|
89
|
+
canSettle,
|
|
90
|
+
canReverse,
|
|
91
|
+
canCancel,
|
|
92
|
+
isApproving,
|
|
93
|
+
isReversing,
|
|
94
|
+
isCanceling,
|
|
95
|
+
labels,
|
|
96
|
+
dialogs,
|
|
97
|
+
onEdit,
|
|
98
|
+
onApprove,
|
|
99
|
+
onSettle,
|
|
100
|
+
onReverse,
|
|
101
|
+
onCancel,
|
|
102
|
+
}: FinanceTitleActionsMenuProps) {
|
|
103
|
+
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
|
104
|
+
const [isReverseSheetOpen, setIsReverseSheetOpen] = useState(false);
|
|
105
|
+
const [reverseReason, setReverseReason] = useState('');
|
|
106
|
+
|
|
107
|
+
const confirmReverse = () => {
|
|
108
|
+
void Promise.resolve(onReverse(reverseReason.trim() || undefined)).finally(
|
|
109
|
+
() => {
|
|
110
|
+
setIsReverseSheetOpen(false);
|
|
111
|
+
setReverseReason('');
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const confirmCancel = () => {
|
|
117
|
+
void Promise.resolve(onCancel()).finally(() => {
|
|
118
|
+
setIsCancelDialogOpen(false);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<DropdownMenu>
|
|
125
|
+
<DropdownMenuTrigger asChild>
|
|
126
|
+
<Button
|
|
127
|
+
variant={triggerVariant}
|
|
128
|
+
size={triggerVariant === 'ghost' ? 'icon' : 'default'}
|
|
129
|
+
>
|
|
130
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
131
|
+
{triggerVariant === 'ghost' ? (
|
|
132
|
+
<span className="sr-only">{labels.srActions}</span>
|
|
133
|
+
) : (
|
|
134
|
+
<span>{labels.menu}</span>
|
|
135
|
+
)}
|
|
136
|
+
</Button>
|
|
137
|
+
</DropdownMenuTrigger>
|
|
138
|
+
<DropdownMenuContent align="end">
|
|
139
|
+
{detailHref ? (
|
|
140
|
+
<DropdownMenuItem asChild>
|
|
141
|
+
<Link href={detailHref}>
|
|
142
|
+
<Eye className="mr-2 h-4 w-4" />
|
|
143
|
+
{labels.viewDetails}
|
|
144
|
+
</Link>
|
|
145
|
+
</DropdownMenuItem>
|
|
146
|
+
) : null}
|
|
147
|
+
<DropdownMenuItem disabled={!canEdit} onClick={onEdit}>
|
|
148
|
+
<Edit className="mr-2 h-4 w-4" />
|
|
149
|
+
{labels.edit}
|
|
150
|
+
</DropdownMenuItem>
|
|
151
|
+
<DropdownMenuItem
|
|
152
|
+
disabled={!canApprove || !!isApproving}
|
|
153
|
+
onClick={onApprove}
|
|
154
|
+
>
|
|
155
|
+
<CheckCircle className="mr-2 h-4 w-4" />
|
|
156
|
+
{labels.approve}
|
|
157
|
+
</DropdownMenuItem>
|
|
158
|
+
<DropdownMenuItem disabled={!canSettle} onClick={onSettle}>
|
|
159
|
+
<Download className="mr-2 h-4 w-4" />
|
|
160
|
+
{labels.settle}
|
|
161
|
+
</DropdownMenuItem>
|
|
162
|
+
<DropdownMenuItem
|
|
163
|
+
disabled={!canReverse || !!isReversing}
|
|
164
|
+
onClick={() => {
|
|
165
|
+
setReverseReason('');
|
|
166
|
+
setIsReverseSheetOpen(true);
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
170
|
+
{labels.reverse}
|
|
171
|
+
</DropdownMenuItem>
|
|
172
|
+
<DropdownMenuSeparator />
|
|
173
|
+
<DropdownMenuItem
|
|
174
|
+
className="text-destructive"
|
|
175
|
+
disabled={!canCancel || !!isCanceling}
|
|
176
|
+
onClick={() => setIsCancelDialogOpen(true)}
|
|
177
|
+
>
|
|
178
|
+
<XCircle className="mr-2 h-4 w-4" />
|
|
179
|
+
{labels.cancel}
|
|
180
|
+
</DropdownMenuItem>
|
|
181
|
+
</DropdownMenuContent>
|
|
182
|
+
</DropdownMenu>
|
|
183
|
+
|
|
184
|
+
<Sheet open={isReverseSheetOpen} onOpenChange={setIsReverseSheetOpen}>
|
|
185
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
186
|
+
<SheetHeader>
|
|
187
|
+
<SheetTitle>{dialogs.reverseTitle}</SheetTitle>
|
|
188
|
+
<SheetDescription>{dialogs.reverseDescription}</SheetDescription>
|
|
189
|
+
</SheetHeader>
|
|
190
|
+
|
|
191
|
+
<div className="space-y-4 px-4">
|
|
192
|
+
<div className="space-y-2">
|
|
193
|
+
<p className="text-sm font-medium">
|
|
194
|
+
{dialogs.reverseReasonLabel}
|
|
195
|
+
</p>
|
|
196
|
+
<Input
|
|
197
|
+
value={reverseReason}
|
|
198
|
+
onChange={(event) => setReverseReason(event.target.value)}
|
|
199
|
+
placeholder={dialogs.reverseReasonPlaceholder}
|
|
200
|
+
maxLength={255}
|
|
201
|
+
disabled={!!isReversing}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="flex flex-col gap-2">
|
|
206
|
+
<Button disabled={!!isReversing} onClick={confirmReverse}>
|
|
207
|
+
{dialogs.confirmReverseButton}
|
|
208
|
+
</Button>
|
|
209
|
+
<Button
|
|
210
|
+
variant="outline"
|
|
211
|
+
disabled={!!isReversing}
|
|
212
|
+
onClick={() => setIsReverseSheetOpen(false)}
|
|
213
|
+
>
|
|
214
|
+
{dialogs.reverseButton}
|
|
215
|
+
</Button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</SheetContent>
|
|
219
|
+
</Sheet>
|
|
220
|
+
|
|
221
|
+
<AlertDialog
|
|
222
|
+
open={isCancelDialogOpen}
|
|
223
|
+
onOpenChange={setIsCancelDialogOpen}
|
|
224
|
+
>
|
|
225
|
+
<AlertDialogContent>
|
|
226
|
+
<AlertDialogHeader>
|
|
227
|
+
<AlertDialogTitle>{dialogs.cancelTitle}</AlertDialogTitle>
|
|
228
|
+
<AlertDialogDescription>
|
|
229
|
+
{dialogs.cancelDescription}
|
|
230
|
+
</AlertDialogDescription>
|
|
231
|
+
</AlertDialogHeader>
|
|
232
|
+
<AlertDialogFooter>
|
|
233
|
+
<AlertDialogCancel disabled={!!isCanceling}>
|
|
234
|
+
{dialogs.cancelButton}
|
|
235
|
+
</AlertDialogCancel>
|
|
236
|
+
<Button disabled={!!isCanceling} onClick={confirmCancel}>
|
|
237
|
+
{dialogs.confirmCancelButton}
|
|
238
|
+
</Button>
|
|
239
|
+
</AlertDialogFooter>
|
|
240
|
+
</AlertDialogContent>
|
|
241
|
+
</AlertDialog>
|
|
242
|
+
</>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -39,8 +39,9 @@ import {
|
|
|
39
39
|
} from '@/components/ui/sheet';
|
|
40
40
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
41
41
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
42
|
-
import { ChevronsUpDown, X } from 'lucide-react';
|
|
43
|
-
import {
|
|
42
|
+
import { ChevronsUpDown, Plus, X } from 'lucide-react';
|
|
43
|
+
import { useTranslations } from 'next-intl';
|
|
44
|
+
import { useEffect, useRef, useState } from 'react';
|
|
44
45
|
import { FieldValues, Path, UseFormReturn, useForm } from 'react-hook-form';
|
|
45
46
|
import { z } from 'zod';
|
|
46
47
|
|
|
@@ -79,6 +80,7 @@ function CreatePersonSheet({
|
|
|
79
80
|
entityLabel: string;
|
|
80
81
|
}) {
|
|
81
82
|
const { request, showToastHandler, currentLocaleCode } = useApp();
|
|
83
|
+
const t = useTranslations('finance.PersonFieldWithCreate');
|
|
82
84
|
|
|
83
85
|
const form = useForm<CreatePersonValues>({
|
|
84
86
|
resolver: zodResolver(createPersonSchema),
|
|
@@ -238,9 +240,12 @@ function CreatePersonSheet({
|
|
|
238
240
|
onCreated({ id: personId, name: values.name });
|
|
239
241
|
form.reset();
|
|
240
242
|
onOpenChange(false);
|
|
241
|
-
showToastHandler?.(
|
|
243
|
+
showToastHandler?.(
|
|
244
|
+
'success',
|
|
245
|
+
t('messages.createdSuccess', { entityLabel })
|
|
246
|
+
);
|
|
242
247
|
} catch {
|
|
243
|
-
showToastHandler?.('error',
|
|
248
|
+
showToastHandler?.('error', t('messages.createdError', { entityLabel }));
|
|
244
249
|
}
|
|
245
250
|
};
|
|
246
251
|
|
|
@@ -254,28 +259,28 @@ function CreatePersonSheet({
|
|
|
254
259
|
}
|
|
255
260
|
}}
|
|
256
261
|
>
|
|
257
|
-
<SheetContent
|
|
262
|
+
<SheetContent
|
|
263
|
+
className="w-full overflow-y-auto sm:max-w-xl"
|
|
264
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
265
|
+
>
|
|
258
266
|
<SheetHeader>
|
|
259
|
-
<SheetTitle>
|
|
260
|
-
<SheetDescription>
|
|
261
|
-
Apenas nome e tipo são obrigatórios. Você pode completar os demais
|
|
262
|
-
dados agora ou depois.
|
|
263
|
-
</SheetDescription>
|
|
267
|
+
<SheetTitle>{t('sheet.title', { entityLabel })}</SheetTitle>
|
|
268
|
+
<SheetDescription>{t('sheet.description')}</SheetDescription>
|
|
264
269
|
</SheetHeader>
|
|
265
270
|
|
|
266
271
|
<Form {...form}>
|
|
267
|
-
<
|
|
268
|
-
className="space-y-4 p-4"
|
|
269
|
-
onSubmit={form.handleSubmit(handleSubmit)}
|
|
270
|
-
>
|
|
272
|
+
<div className="space-y-4 p-4">
|
|
271
273
|
<FormField
|
|
272
274
|
control={form.control}
|
|
273
275
|
name="name"
|
|
274
276
|
render={({ field }) => (
|
|
275
277
|
<FormItem>
|
|
276
|
-
<FormLabel>
|
|
278
|
+
<FormLabel>{t('fields.name')}</FormLabel>
|
|
277
279
|
<FormControl>
|
|
278
|
-
<Input
|
|
280
|
+
<Input
|
|
281
|
+
placeholder={t('placeholders.name', { entityLabel })}
|
|
282
|
+
{...field}
|
|
283
|
+
/>
|
|
279
284
|
</FormControl>
|
|
280
285
|
<FormMessage />
|
|
281
286
|
</FormItem>
|
|
@@ -287,16 +292,20 @@ function CreatePersonSheet({
|
|
|
287
292
|
name="type"
|
|
288
293
|
render={({ field }) => (
|
|
289
294
|
<FormItem>
|
|
290
|
-
<FormLabel>
|
|
295
|
+
<FormLabel>{t('fields.type')}</FormLabel>
|
|
291
296
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
292
297
|
<FormControl>
|
|
293
298
|
<SelectTrigger>
|
|
294
|
-
<SelectValue placeholder=
|
|
299
|
+
<SelectValue placeholder={t('common.select')} />
|
|
295
300
|
</SelectTrigger>
|
|
296
301
|
</FormControl>
|
|
297
302
|
<SelectContent>
|
|
298
|
-
<SelectItem value="individual">
|
|
299
|
-
|
|
303
|
+
<SelectItem value="individual">
|
|
304
|
+
{t('types.individual')}
|
|
305
|
+
</SelectItem>
|
|
306
|
+
<SelectItem value="company">
|
|
307
|
+
{t('types.company')}
|
|
308
|
+
</SelectItem>
|
|
300
309
|
</SelectContent>
|
|
301
310
|
</Select>
|
|
302
311
|
<FormMessage />
|
|
@@ -312,8 +321,8 @@ function CreatePersonSheet({
|
|
|
312
321
|
<FormItem>
|
|
313
322
|
<FormLabel>
|
|
314
323
|
{selectedType === 'individual'
|
|
315
|
-
? '
|
|
316
|
-
: '
|
|
324
|
+
? t('fields.documentIndividualOptional')
|
|
325
|
+
: t('fields.documentCompanyOptional')}
|
|
317
326
|
</FormLabel>
|
|
318
327
|
<FormControl>
|
|
319
328
|
<Input
|
|
@@ -336,10 +345,10 @@ function CreatePersonSheet({
|
|
|
336
345
|
name="email"
|
|
337
346
|
render={({ field }) => (
|
|
338
347
|
<FormItem>
|
|
339
|
-
<FormLabel>
|
|
348
|
+
<FormLabel>{t('fields.emailOptional')}</FormLabel>
|
|
340
349
|
<FormControl>
|
|
341
350
|
<Input
|
|
342
|
-
placeholder={
|
|
351
|
+
placeholder={t('placeholders.email', { entityLabel })}
|
|
343
352
|
{...field}
|
|
344
353
|
value={field.value || ''}
|
|
345
354
|
/>
|
|
@@ -355,10 +364,10 @@ function CreatePersonSheet({
|
|
|
355
364
|
name="phone"
|
|
356
365
|
render={({ field }) => (
|
|
357
366
|
<FormItem>
|
|
358
|
-
<FormLabel>
|
|
367
|
+
<FormLabel>{t('fields.phoneOptional')}</FormLabel>
|
|
359
368
|
<FormControl>
|
|
360
369
|
<Input
|
|
361
|
-
placeholder=
|
|
370
|
+
placeholder={t('placeholders.phone')}
|
|
362
371
|
{...field}
|
|
363
372
|
value={field.value || ''}
|
|
364
373
|
/>
|
|
@@ -369,7 +378,9 @@ function CreatePersonSheet({
|
|
|
369
378
|
/>
|
|
370
379
|
|
|
371
380
|
<div className="rounded-md border p-3">
|
|
372
|
-
<p className="mb-3 text-sm font-medium">
|
|
381
|
+
<p className="mb-3 text-sm font-medium">
|
|
382
|
+
{t('fields.addressOptional')}
|
|
383
|
+
</p>
|
|
373
384
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
374
385
|
<FormField
|
|
375
386
|
control={form.control}
|
|
@@ -378,7 +389,7 @@ function CreatePersonSheet({
|
|
|
378
389
|
<FormItem className="sm:col-span-2">
|
|
379
390
|
<FormControl>
|
|
380
391
|
<Input
|
|
381
|
-
placeholder=
|
|
392
|
+
placeholder={t('placeholders.addressLine1')}
|
|
382
393
|
{...field}
|
|
383
394
|
value={field.value || ''}
|
|
384
395
|
/>
|
|
@@ -395,7 +406,7 @@ function CreatePersonSheet({
|
|
|
395
406
|
<FormItem>
|
|
396
407
|
<FormControl>
|
|
397
408
|
<Input
|
|
398
|
-
placeholder=
|
|
409
|
+
placeholder={t('placeholders.city')}
|
|
399
410
|
{...field}
|
|
400
411
|
value={field.value || ''}
|
|
401
412
|
/>
|
|
@@ -412,7 +423,7 @@ function CreatePersonSheet({
|
|
|
412
423
|
<FormItem>
|
|
413
424
|
<FormControl>
|
|
414
425
|
<Input
|
|
415
|
-
placeholder=
|
|
426
|
+
placeholder={t('placeholders.state')}
|
|
416
427
|
{...field}
|
|
417
428
|
value={field.value || ''}
|
|
418
429
|
/>
|
|
@@ -430,13 +441,19 @@ function CreatePersonSheet({
|
|
|
430
441
|
variant="outline"
|
|
431
442
|
onClick={() => onOpenChange(false)}
|
|
432
443
|
>
|
|
433
|
-
|
|
444
|
+
{t('actions.cancel')}
|
|
434
445
|
</Button>
|
|
435
|
-
<Button
|
|
436
|
-
|
|
446
|
+
<Button
|
|
447
|
+
type="button"
|
|
448
|
+
disabled={form.formState.isSubmitting}
|
|
449
|
+
onClick={() => {
|
|
450
|
+
void form.handleSubmit(handleSubmit)();
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
{t('actions.saveEntity', { entityLabel })}
|
|
437
454
|
</Button>
|
|
438
455
|
</div>
|
|
439
|
-
</
|
|
456
|
+
</div>
|
|
440
457
|
</Form>
|
|
441
458
|
</SheetContent>
|
|
442
459
|
</Sheet>
|
|
@@ -457,11 +474,14 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
457
474
|
selectPlaceholder: string;
|
|
458
475
|
}) {
|
|
459
476
|
const { request } = useApp();
|
|
477
|
+
const t = useTranslations('finance.PersonFieldWithCreate');
|
|
460
478
|
const [personOpen, setPersonOpen] = useState(false);
|
|
461
479
|
const [personSearch, setPersonSearch] = useState('');
|
|
462
480
|
const [debouncedPersonSearch, setDebouncedPersonSearch] = useState('');
|
|
463
481
|
const [createPersonOpen, setCreatePersonOpen] = useState(false);
|
|
464
482
|
const [selectedPersonLabel, setSelectedPersonLabel] = useState('');
|
|
483
|
+
const parentScrollContainerRef = useRef<HTMLElement | null>(null);
|
|
484
|
+
const parentScrollTopRef = useRef(0);
|
|
465
485
|
|
|
466
486
|
useEffect(() => {
|
|
467
487
|
const timeout = setTimeout(() => {
|
|
@@ -471,6 +491,49 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
471
491
|
return () => clearTimeout(timeout);
|
|
472
492
|
}, [personSearch]);
|
|
473
493
|
|
|
494
|
+
const captureParentScrollPosition = (trigger: HTMLElement) => {
|
|
495
|
+
const parentSheetContent = trigger.closest(
|
|
496
|
+
'[data-radix-dialog-content]'
|
|
497
|
+
) as HTMLElement | null;
|
|
498
|
+
|
|
499
|
+
if (!parentSheetContent) {
|
|
500
|
+
parentScrollContainerRef.current = null;
|
|
501
|
+
parentScrollTopRef.current = 0;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
parentScrollContainerRef.current = parentSheetContent;
|
|
506
|
+
parentScrollTopRef.current = parentSheetContent.scrollTop;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const restoreParentScrollPosition = () => {
|
|
510
|
+
const fallbackOpenDialog = (
|
|
511
|
+
Array.from(
|
|
512
|
+
document.querySelectorAll(
|
|
513
|
+
'[data-radix-dialog-content][data-state="open"]'
|
|
514
|
+
)
|
|
515
|
+
) as HTMLElement[]
|
|
516
|
+
).at(-1);
|
|
517
|
+
|
|
518
|
+
const container =
|
|
519
|
+
parentScrollContainerRef.current &&
|
|
520
|
+
document.body.contains(parentScrollContainerRef.current)
|
|
521
|
+
? parentScrollContainerRef.current
|
|
522
|
+
: fallbackOpenDialog || null;
|
|
523
|
+
|
|
524
|
+
if (!container) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const restore = () => {
|
|
529
|
+
container.scrollTop = parentScrollTopRef.current;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
requestAnimationFrame(restore);
|
|
533
|
+
setTimeout(restore, 0);
|
|
534
|
+
setTimeout(restore, 120);
|
|
535
|
+
};
|
|
536
|
+
|
|
474
537
|
const { data: personOptionsData = [], isLoading: isLoadingPersons } =
|
|
475
538
|
useQuery<PersonOption[]>({
|
|
476
539
|
queryKey: [
|
|
@@ -513,7 +576,7 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
513
576
|
render={({ field }) => (
|
|
514
577
|
<FormItem>
|
|
515
578
|
<FormLabel>{label}</FormLabel>
|
|
516
|
-
<div className="flex items-center gap-2">
|
|
579
|
+
<div className="flex w-full min-w-0 items-center gap-2">
|
|
517
580
|
<Popover open={personOpen} onOpenChange={setPersonOpen}>
|
|
518
581
|
<PopoverTrigger asChild>
|
|
519
582
|
<FormControl>
|
|
@@ -521,16 +584,18 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
521
584
|
type="button"
|
|
522
585
|
variant="outline"
|
|
523
586
|
role="combobox"
|
|
524
|
-
className="w-
|
|
587
|
+
className="flex-1 min-w-0 justify-between overflow-hidden"
|
|
525
588
|
>
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
589
|
+
<span className="truncate text-left">
|
|
590
|
+
{field.value
|
|
591
|
+
? (personOptionsData.find(
|
|
592
|
+
(person) =>
|
|
593
|
+
String(person.id) === String(field.value)
|
|
594
|
+
)?.name ??
|
|
595
|
+
selectedPersonLabel ??
|
|
596
|
+
`ID #${String(field.value)}`)
|
|
597
|
+
: selectPlaceholder}
|
|
598
|
+
</span>
|
|
534
599
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
535
600
|
</Button>
|
|
536
601
|
</FormControl>
|
|
@@ -541,29 +606,32 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
541
606
|
>
|
|
542
607
|
<Command shouldFilter={false}>
|
|
543
608
|
<CommandInput
|
|
544
|
-
placeholder=
|
|
609
|
+
placeholder={t('search.placeholder')}
|
|
545
610
|
value={personSearch}
|
|
546
611
|
onValueChange={setPersonSearch}
|
|
547
612
|
/>
|
|
548
613
|
<CommandList>
|
|
549
614
|
<CommandEmpty>
|
|
550
615
|
{isLoadingPersons ? (
|
|
551
|
-
'
|
|
616
|
+
t('search.loading')
|
|
552
617
|
) : (
|
|
553
618
|
<div className="space-y-2 p-2 text-center">
|
|
554
619
|
<p className="text-sm text-muted-foreground">
|
|
555
|
-
|
|
620
|
+
{t('search.noResults')}
|
|
556
621
|
</p>
|
|
557
622
|
<Button
|
|
558
623
|
type="button"
|
|
559
624
|
variant="outline"
|
|
560
625
|
className="w-full"
|
|
561
|
-
onClick={() => {
|
|
626
|
+
onClick={(event) => {
|
|
627
|
+
captureParentScrollPosition(
|
|
628
|
+
event.currentTarget
|
|
629
|
+
);
|
|
562
630
|
setPersonOpen(false);
|
|
563
631
|
setCreatePersonOpen(true);
|
|
564
632
|
}}
|
|
565
633
|
>
|
|
566
|
-
|
|
634
|
+
{t('actions.createNew')}
|
|
567
635
|
</Button>
|
|
568
636
|
</div>
|
|
569
637
|
)}
|
|
@@ -593,16 +661,33 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
593
661
|
type="button"
|
|
594
662
|
variant="outline"
|
|
595
663
|
size="icon"
|
|
664
|
+
className="shrink-0"
|
|
596
665
|
onClick={() => {
|
|
597
666
|
field.onChange('');
|
|
598
667
|
setPersonSearch('');
|
|
599
668
|
setSelectedPersonLabel('');
|
|
600
669
|
setPersonOpen(false);
|
|
601
670
|
}}
|
|
671
|
+
aria-label={t('actions.clearSelection')}
|
|
602
672
|
>
|
|
603
673
|
<X className="h-4 w-4" />
|
|
604
674
|
</Button>
|
|
605
675
|
) : null}
|
|
676
|
+
|
|
677
|
+
<Button
|
|
678
|
+
type="button"
|
|
679
|
+
variant="outline"
|
|
680
|
+
size="icon"
|
|
681
|
+
className="shrink-0"
|
|
682
|
+
onClick={(event) => {
|
|
683
|
+
captureParentScrollPosition(event.currentTarget);
|
|
684
|
+
setPersonOpen(false);
|
|
685
|
+
setCreatePersonOpen(true);
|
|
686
|
+
}}
|
|
687
|
+
aria-label={t('actions.createEntityAria', { entityLabel })}
|
|
688
|
+
>
|
|
689
|
+
<Plus className="h-4 w-4" />
|
|
690
|
+
</Button>
|
|
606
691
|
</div>
|
|
607
692
|
<FormMessage />
|
|
608
693
|
</FormItem>
|
|
@@ -611,11 +696,18 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
|
|
|
611
696
|
|
|
612
697
|
<CreatePersonSheet
|
|
613
698
|
open={createPersonOpen}
|
|
614
|
-
onOpenChange={
|
|
699
|
+
onOpenChange={(nextOpen) => {
|
|
700
|
+
setCreatePersonOpen(nextOpen);
|
|
701
|
+
if (!nextOpen) {
|
|
702
|
+
restoreParentScrollPosition();
|
|
703
|
+
}
|
|
704
|
+
}}
|
|
615
705
|
entityLabel={entityLabel}
|
|
616
706
|
onCreated={(person) => {
|
|
617
707
|
form.setValue(name, String(person.id) as any, {
|
|
618
|
-
shouldValidate:
|
|
708
|
+
shouldValidate: false,
|
|
709
|
+
shouldDirty: true,
|
|
710
|
+
shouldTouch: true,
|
|
619
711
|
});
|
|
620
712
|
setSelectedPersonLabel(person.name);
|
|
621
713
|
setPersonSearch(person.name);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const canEditTitle = (status?: string | null) => status === 'rascunho';
|
|
2
|
+
|
|
3
|
+
export const canApproveTitle = (status?: string | null) =>
|
|
4
|
+
status === 'rascunho';
|
|
5
|
+
|
|
6
|
+
export const canSettleTitle = (status?: string | null) =>
|
|
7
|
+
['aberto', 'parcial', 'vencido'].includes(String(status || ''));
|
|
8
|
+
|
|
9
|
+
export const canCancelTitle = (status?: string | null) =>
|
|
10
|
+
!['cancelado', 'liquidado'].includes(String(status || ''));
|
|
11
|
+
|
|
12
|
+
export const canReverseTitle = (status?: string | null) =>
|
|
13
|
+
String(status || '') === 'liquidado';
|
|
14
|
+
|
|
15
|
+
export const getFirstActiveSettlementId = (title: any): string | null => {
|
|
16
|
+
const settlements = (title?.parcelas || []).flatMap(
|
|
17
|
+
(installment: any) => installment?.liquidacoes || []
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const activeSettlement = [...settlements]
|
|
21
|
+
.reverse()
|
|
22
|
+
.find((settlement: any) => {
|
|
23
|
+
const settlementStatus = String(settlement?.status || '').toLowerCase();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
!!settlement?.settlementId &&
|
|
27
|
+
settlementStatus !== 'reversed' &&
|
|
28
|
+
settlementStatus !== 'estornado' &&
|
|
29
|
+
settlementStatus !== 'reversed_settlement'
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return activeSettlement?.settlementId
|
|
34
|
+
? String(activeSettlement.settlementId)
|
|
35
|
+
: null;
|
|
36
|
+
};
|