@hed-hog/finance 0.0.252 → 0.0.256

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 (32) hide show
  1. package/dist/dto/reverse-settlement.dto.d.ts +1 -0
  2. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -1
  3. package/dist/dto/reverse-settlement.dto.js +5 -0
  4. package/dist/dto/reverse-settlement.dto.js.map +1 -1
  5. package/dist/finance-installments.controller.d.ts +106 -4
  6. package/dist/finance-installments.controller.d.ts.map +1 -1
  7. package/dist/finance-installments.controller.js +38 -2
  8. package/dist/finance-installments.controller.js.map +1 -1
  9. package/dist/finance.service.d.ts +104 -2
  10. package/dist/finance.service.d.ts.map +1 -1
  11. package/dist/finance.service.js +366 -121
  12. package/dist/finance.service.js.map +1 -1
  13. package/hedhog/data/route.yaml +27 -0
  14. package/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +572 -0
  15. package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +244 -0
  16. package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +143 -51
  17. package/hedhog/frontend/app/_lib/title-action-rules.ts.ejs +36 -0
  18. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +449 -293
  19. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1189 -545
  20. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +176 -133
  21. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1459 -312
  22. package/hedhog/frontend/app/page.tsx.ejs +15 -4
  23. package/hedhog/frontend/messages/en.json +294 -5
  24. package/hedhog/frontend/messages/pt.json +294 -5
  25. package/hedhog/query/settlement-auditability.sql +175 -0
  26. package/hedhog/table/bank_reconciliation.yaml +11 -0
  27. package/hedhog/table/settlement.yaml +17 -1
  28. package/hedhog/table/settlement_allocation.yaml +3 -0
  29. package/package.json +7 -7
  30. package/src/dto/reverse-settlement.dto.ts +4 -0
  31. package/src/finance-installments.controller.ts +45 -12
  32. package/src/finance.service.ts +521 -146
@@ -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 { useEffect, useState } from 'react';
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?.('success', `${entityLabel} criado com sucesso`);
243
+ showToastHandler?.(
244
+ 'success',
245
+ t('messages.createdSuccess', { entityLabel })
246
+ );
242
247
  } catch {
243
- showToastHandler?.('error', `Não foi possível criar ${entityLabel}`);
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 className="w-full overflow-y-auto sm:max-w-xl">
262
+ <SheetContent
263
+ className="w-full overflow-y-auto sm:max-w-xl"
264
+ onCloseAutoFocus={(event) => event.preventDefault()}
265
+ >
258
266
  <SheetHeader>
259
- <SheetTitle>Novo {entityLabel}</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
- <form
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>Nome</FormLabel>
278
+ <FormLabel>{t('fields.name')}</FormLabel>
277
279
  <FormControl>
278
- <Input placeholder={`Nome do ${entityLabel}`} {...field} />
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>Tipo</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="Selecione" />
299
+ <SelectValue placeholder={t('common.select')} />
295
300
  </SelectTrigger>
296
301
  </FormControl>
297
302
  <SelectContent>
298
- <SelectItem value="individual">Pessoa Física</SelectItem>
299
- <SelectItem value="company">Pessoa Jurídica</SelectItem>
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
- ? 'CPF (opcional)'
316
- : 'CNPJ (opcional)'}
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>E-mail (opcional)</FormLabel>
348
+ <FormLabel>{t('fields.emailOptional')}</FormLabel>
340
349
  <FormControl>
341
350
  <Input
342
- placeholder={`${entityLabel}@empresa.com`}
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>Telefone (opcional)</FormLabel>
367
+ <FormLabel>{t('fields.phoneOptional')}</FormLabel>
359
368
  <FormControl>
360
369
  <Input
361
- placeholder="(11) 99999-9999"
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">Endereço (opcional)</p>
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="Rua, número, complemento"
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="Cidade"
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="UF"
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
- Cancelar
444
+ {t('actions.cancel')}
434
445
  </Button>
435
- <Button type="submit" disabled={form.formState.isSubmitting}>
436
- Salvar {entityLabel}
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
- </form>
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-full justify-between"
587
+ className="flex-1 min-w-0 justify-between overflow-hidden"
525
588
  >
526
- {field.value
527
- ? (personOptionsData.find(
528
- (person) =>
529
- String(person.id) === String(field.value)
530
- )?.name ??
531
- selectedPersonLabel ??
532
- `ID #${String(field.value)}`)
533
- : selectPlaceholder}
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="Digite para buscar pessoa..."
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
- 'Buscando pessoas...'
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
- Nenhuma pessoa encontrada
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
- Criar novo cadastro
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={setCreatePersonOpen}
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: true,
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
+ };