@hed-hog/contact 0.0.329 → 0.0.330

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.
@@ -1,43 +1,61 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- EmptyState,
5
- Page,
6
- PageHeader,
7
- PaginationFooter,
8
- SearchBar,
9
- type SearchBarControl,
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
9
+ type SearchBarControl,
10
10
  } from '@/components/entity-list';
11
11
  import { Badge } from '@/components/ui/badge';
12
12
  import { Button } from '@/components/ui/button';
13
13
  import { Card, CardContent } from '@/components/ui/card';
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuSeparator,
19
+ DropdownMenuTrigger,
20
+ } from '@/components/ui/dropdown-menu';
21
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
14
22
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
15
23
  import { Skeleton } from '@/components/ui/skeleton';
16
- import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
17
24
  import {
18
- Table,
19
- TableBody,
20
- TableCell,
21
- TableHead,
22
- TableHeader,
23
- TableRow,
25
+ Table,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
24
31
  } from '@/components/ui/table';
32
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
25
33
  import { cn } from '@/lib/utils';
26
34
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
27
35
  import {
28
- CheckCircle2,
29
- Clock3,
30
- FileCheck2,
31
- FileText,
32
- LayoutGrid,
33
- List,
34
- RefreshCcw,
36
+ CheckCircle2,
37
+ Clock3,
38
+ FileCheck2,
39
+ FileText,
40
+ LayoutGrid,
41
+ List,
42
+ Loader2,
43
+ MoreHorizontal,
44
+ Pencil,
45
+ RefreshCcw,
46
+ Send,
47
+ XCircle,
35
48
  } from 'lucide-react';
36
49
  import { useTranslations } from 'next-intl';
37
50
  import { useEffect, useMemo, useState } from 'react';
38
51
  import { toast } from 'sonner';
39
52
 
40
53
  import type { PaginatedResult } from '../../person/_components/person-types';
54
+ import { ProposalFormSheet } from './proposal-form-sheet';
55
+ import {
56
+ openStoredFile,
57
+ type GenerateProposalDocumentResponse,
58
+ } from './proposal-types';
41
59
 
42
60
  type ProposalStatus =
43
61
  | 'draft'
@@ -56,6 +74,7 @@ type ProposalPerson = {
56
74
  trade_name?: string | null;
57
75
  email?: string | null;
58
76
  phone?: string | null;
77
+ avatar_id?: number | null;
59
78
  };
60
79
 
61
80
  type ProposalRecord = {
@@ -70,6 +89,9 @@ type ProposalRecord = {
70
89
  updated_at?: string | null;
71
90
  approved_at?: string | null;
72
91
  current_revision_number?: number | null;
92
+ approval_count?: number | null;
93
+ required_approvals?: number | null;
94
+ current_user_has_approved?: boolean | null;
73
95
  person?: ProposalPerson | null;
74
96
  };
75
97
 
@@ -135,6 +157,13 @@ function getStatusBadgeClassName(status?: string | null) {
135
157
  }
136
158
  }
137
159
 
160
+ function getAvatarInitials(name?: string | null) {
161
+ if (!name) return '?';
162
+ const parts = name.trim().split(/\s+/);
163
+ if (parts.length === 1) return (parts[0] ?? '').slice(0, 2).toUpperCase();
164
+ return ((parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')).toUpperCase();
165
+ }
166
+
138
167
  function canSubmitProposal(status?: ProposalStatus | null) {
139
168
  return (
140
169
  status !== 'approved' &&
@@ -144,6 +173,15 @@ function canSubmitProposal(status?: ProposalStatus | null) {
144
173
  );
145
174
  }
146
175
 
176
+ function canEditProposal(status?: ProposalStatus | null) {
177
+ return status !== 'approved' && status !== 'contract_generated';
178
+ }
179
+
180
+ function getPersonAvatarUrl(avatarId?: number | null) {
181
+ if (!avatarId) return undefined;
182
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`;
183
+ }
184
+
147
185
  export function ProposalsManagementPage({
148
186
  defaultStatus = 'all',
149
187
  }: ProposalsManagementPageProps) {
@@ -160,6 +198,9 @@ export function ProposalsManagementPage({
160
198
  const [pageSize, setPageSize] = useState(12);
161
199
  const [viewMode, setViewMode] = useState<ProposalViewMode>('table');
162
200
  const [actionKey, setActionKey] = useState<string | null>(null);
201
+ const [editSheetOpen, setEditSheetOpen] = useState(false);
202
+ const [editingProposalId, setEditingProposalId] = useState<number | null>(null);
203
+ const [editingPersonId, setEditingPersonId] = useState<number | null>(null);
163
204
 
164
205
  useEffect(() => {
165
206
  setStatusFilter(defaultStatus);
@@ -343,6 +384,35 @@ export function ProposalsManagementPage({
343
384
  }
344
385
  };
345
386
 
387
+ const handleEdit = (proposal: ProposalRecord) => {
388
+ setEditingProposalId(proposal.id);
389
+ setEditingPersonId(proposal.person_id);
390
+ setEditSheetOpen(true);
391
+ };
392
+
393
+ const handleGeneratePdf = async (proposal: ProposalRecord) => {
394
+ try {
395
+ setActionKey(`generate-pdf-${proposal.id}`);
396
+ const response = await request<GenerateProposalDocumentResponse>({
397
+ url: `/proposal/${proposal.id}/generate-pdf`,
398
+ method: 'POST',
399
+ data: {},
400
+ });
401
+ const fileId = response.data?.fileId;
402
+ toast.success(proposalT('toasts.generatePdfSuccess'));
403
+ if (fileId) openStoredFile(fileId);
404
+ void refetch();
405
+ } catch (error) {
406
+ toast.error(
407
+ error instanceof Error
408
+ ? error.message
409
+ : proposalT('toasts.generatePdfError')
410
+ );
411
+ } finally {
412
+ setActionKey(null);
413
+ }
414
+ };
415
+
346
416
  const statsCards = [
347
417
  {
348
418
  key: 'total',
@@ -543,28 +613,46 @@ export function ProposalsManagementPage({
543
613
  </TableCell>
544
614
 
545
615
  <TableCell>
546
- <div className="space-y-1">
547
- <div className="font-medium text-foreground">
548
- {customerName}
549
- </div>
550
- <div className="text-xs text-muted-foreground">
551
- {proposal.person?.email ||
552
- proposal.person?.phone ||
553
- '—'}
616
+ <div className="flex items-center gap-3">
617
+ <Avatar className="h-8 w-8 shrink-0">
618
+ <AvatarImage src={getPersonAvatarUrl(proposal.person?.avatar_id)} />
619
+ <AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
620
+ {getAvatarInitials(customerName)}
621
+ </AvatarFallback>
622
+ </Avatar>
623
+ <div className="min-w-0 space-y-0.5">
624
+ <div className="truncate font-medium text-foreground">
625
+ {customerName}
626
+ </div>
627
+ <div className="text-xs text-muted-foreground">
628
+ {proposal.person?.email ||
629
+ proposal.person?.phone ||
630
+ '—'}
631
+ </div>
554
632
  </div>
555
633
  </div>
556
634
  </TableCell>
557
635
 
558
636
  <TableCell>
559
- <Badge
560
- variant="outline"
561
- className={cn(
562
- 'font-medium',
563
- getStatusBadgeClassName(proposal.status)
564
- )}
565
- >
566
- {getStatusLabel(proposal.status)}
567
- </Badge>
637
+ <div className="flex flex-col gap-1">
638
+ <Badge
639
+ variant="outline"
640
+ className={cn(
641
+ 'font-medium',
642
+ getStatusBadgeClassName(proposal.status)
643
+ )}
644
+ >
645
+ {getStatusLabel(proposal.status)}
646
+ </Badge>
647
+ {proposal.status === 'pending_approval' ? (
648
+ <span className="text-xs text-muted-foreground">
649
+ {t('approval.progress', {
650
+ count: proposal.approval_count ?? 0,
651
+ required: proposal.required_approvals ?? 1,
652
+ })}
653
+ </span>
654
+ ) : null}
655
+ </div>
568
656
  </TableCell>
569
657
 
570
658
  <TableCell className="text-right font-medium">
@@ -582,45 +670,67 @@ export function ProposalsManagementPage({
582
670
  {formatShortDate(proposal.updated_at, locale)}
583
671
  </TableCell>
584
672
 
585
- <TableCell>
586
- <div className="flex flex-wrap justify-end gap-2">
587
- {canSubmitProposal(proposal.status) ? (
588
- <Button
589
- size="sm"
590
- variant="outline"
591
- disabled={actionKey === `submit-${proposal.id}`}
592
- onClick={() =>
593
- void handleStatusAction(proposal, 'submit')
594
- }
595
- >
596
- {proposalT('actions.submit')}
673
+ <TableCell className="text-right">
674
+ <DropdownMenu>
675
+ <DropdownMenuTrigger asChild>
676
+ <Button variant="ghost" size="icon" className="h-8 w-8">
677
+ {actionKey?.startsWith(`${proposal.id}-`) || actionKey === `generate-pdf-${proposal.id}` || actionKey === `submit-${proposal.id}` || actionKey === `approve-${proposal.id}` || actionKey === `reject-${proposal.id}` ? (
678
+ <Loader2 className="h-4 w-4 animate-spin" />
679
+ ) : (
680
+ <MoreHorizontal className="h-4 w-4" />
681
+ )}
597
682
  </Button>
598
- ) : null}
599
-
600
- {proposal.status === 'pending_approval' ? (
601
- <>
602
- <Button
603
- size="sm"
604
- disabled={actionKey === `approve-${proposal.id}`}
605
- onClick={() =>
606
- void handleStatusAction(proposal, 'approve')
607
- }
608
- >
609
- {proposalT('actions.approve')}
610
- </Button>
611
- <Button
612
- size="sm"
613
- variant="destructive"
614
- disabled={actionKey === `reject-${proposal.id}`}
615
- onClick={() =>
616
- void handleStatusAction(proposal, 'reject')
617
- }
683
+ </DropdownMenuTrigger>
684
+ <DropdownMenuContent align="end">
685
+ {canEditProposal(proposal.status) ? (
686
+ <DropdownMenuItem onClick={() => handleEdit(proposal)}>
687
+ <Pencil className="mr-2 h-4 w-4" />
688
+ {proposalT('actions.edit')}
689
+ </DropdownMenuItem>
690
+ ) : null}
691
+
692
+ {canSubmitProposal(proposal.status) ? (
693
+ <DropdownMenuItem
694
+ disabled={actionKey === `submit-${proposal.id}`}
695
+ onClick={() => void handleStatusAction(proposal, 'submit')}
618
696
  >
619
- {proposalT('actions.reject')}
620
- </Button>
621
- </>
622
- ) : null}
623
- </div>
697
+ <Send className="mr-2 h-4 w-4" />
698
+ {proposalT('actions.submit')}
699
+ </DropdownMenuItem>
700
+ ) : null}
701
+
702
+ {proposal.status === 'pending_approval' &&
703
+ !proposal.current_user_has_approved ? (
704
+ <>
705
+ <DropdownMenuItem
706
+ disabled={actionKey === `approve-${proposal.id}`}
707
+ onClick={() => void handleStatusAction(proposal, 'approve')}
708
+ >
709
+ <CheckCircle2 className="mr-2 h-4 w-4" />
710
+ {proposalT('actions.approve')}
711
+ </DropdownMenuItem>
712
+ <DropdownMenuItem
713
+ disabled={actionKey === `reject-${proposal.id}`}
714
+ className="text-destructive focus:text-destructive"
715
+ onClick={() => void handleStatusAction(proposal, 'reject')}
716
+ >
717
+ <XCircle className="mr-2 h-4 w-4" />
718
+ {proposalT('actions.reject')}
719
+ </DropdownMenuItem>
720
+ </>
721
+ ) : null}
722
+
723
+ <DropdownMenuSeparator />
724
+
725
+ <DropdownMenuItem
726
+ disabled={actionKey === `generate-pdf-${proposal.id}`}
727
+ onClick={() => void handleGeneratePdf(proposal)}
728
+ >
729
+ <FileText className="mr-2 h-4 w-4" />
730
+ {proposalT('actions.generatePdf')}
731
+ </DropdownMenuItem>
732
+ </DropdownMenuContent>
733
+ </DropdownMenu>
624
734
  </TableCell>
625
735
  </TableRow>
626
736
  );
@@ -643,37 +753,47 @@ export function ProposalsManagementPage({
643
753
  >
644
754
  <CardContent className="flex h-full flex-col gap-3 p-4">
645
755
  <div className="flex items-start justify-between gap-3">
646
- <div className="min-w-0 space-y-1">
647
- <p className="line-clamp-2 text-sm font-semibold text-foreground">
648
- {proposal.title}
649
- </p>
650
- <p className="text-xs text-muted-foreground">
651
- {proposal.code || `#${proposal.id}`}
652
- {proposal.current_revision_number
653
- ? ` · v${proposal.current_revision_number}`
654
- : ''}
655
- </p>
756
+ <div className="flex min-w-0 items-start gap-2.5">
757
+ <Avatar className="mt-0.5 h-8 w-8 shrink-0">
758
+ <AvatarImage src={getPersonAvatarUrl(proposal.person?.avatar_id)} />
759
+ <AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
760
+ {getAvatarInitials(customerName)}
761
+ </AvatarFallback>
762
+ </Avatar>
763
+ <div className="min-w-0 space-y-1">
764
+ <p className="line-clamp-2 text-sm font-semibold text-foreground">
765
+ {proposal.title}
766
+ </p>
767
+ <p className="text-xs text-muted-foreground">
768
+ {customerName} · {proposal.code || `#${proposal.id}`}
769
+ {proposal.current_revision_number
770
+ ? ` · v${proposal.current_revision_number}`
771
+ : ''}
772
+ </p>
773
+ </div>
774
+ </div>
775
+ <div className="flex shrink-0 flex-col items-end gap-1">
776
+ <Badge
777
+ variant="outline"
778
+ className={cn(
779
+ 'font-medium',
780
+ getStatusBadgeClassName(proposal.status)
781
+ )}
782
+ >
783
+ {getStatusLabel(proposal.status)}
784
+ </Badge>
785
+ {proposal.status === 'pending_approval' ? (
786
+ <span className="text-xs text-muted-foreground">
787
+ {t('approval.progress', {
788
+ count: proposal.approval_count ?? 0,
789
+ required: proposal.required_approvals ?? 1,
790
+ })}
791
+ </span>
792
+ ) : null}
656
793
  </div>
657
- <Badge
658
- variant="outline"
659
- className={cn(
660
- 'shrink-0 font-medium',
661
- getStatusBadgeClassName(proposal.status)
662
- )}
663
- >
664
- {getStatusLabel(proposal.status)}
665
- </Badge>
666
794
  </div>
667
795
 
668
796
  <div className="grid gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-xs">
669
- <div className="flex items-center justify-between gap-2">
670
- <span className="text-muted-foreground">
671
- {t('columns.customer')}
672
- </span>
673
- <span className="truncate font-medium text-foreground">
674
- {customerName}
675
- </span>
676
- </div>
677
797
  <div className="flex items-center justify-between gap-2">
678
798
  <span className="text-muted-foreground">
679
799
  {t('columns.total')}
@@ -704,43 +824,68 @@ export function ProposalsManagementPage({
704
824
  </div>
705
825
  </div>
706
826
 
707
- <div className="mt-auto flex flex-wrap justify-end gap-2">
708
- {canSubmitProposal(proposal.status) ? (
709
- <Button
710
- size="sm"
711
- variant="outline"
712
- disabled={actionKey === `submit-${proposal.id}`}
713
- onClick={() =>
714
- void handleStatusAction(proposal, 'submit')
715
- }
716
- >
717
- {proposalT('actions.submit')}
718
- </Button>
719
- ) : null}
720
-
721
- {proposal.status === 'pending_approval' ? (
722
- <>
723
- <Button
724
- size="sm"
725
- disabled={actionKey === `approve-${proposal.id}`}
726
- onClick={() =>
727
- void handleStatusAction(proposal, 'approve')
728
- }
729
- >
730
- {proposalT('actions.approve')}
827
+ <div className="mt-auto flex justify-end">
828
+ <DropdownMenu>
829
+ <DropdownMenuTrigger asChild>
830
+ <Button variant="outline" size="sm" className="gap-1.5">
831
+ {actionKey === `generate-pdf-${proposal.id}` || actionKey === `submit-${proposal.id}` || actionKey === `approve-${proposal.id}` || actionKey === `reject-${proposal.id}` ? (
832
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
833
+ ) : (
834
+ <MoreHorizontal className="h-3.5 w-3.5" />
835
+ )}
836
+ {t('columns.actions')}
731
837
  </Button>
732
- <Button
733
- size="sm"
734
- variant="destructive"
735
- disabled={actionKey === `reject-${proposal.id}`}
736
- onClick={() =>
737
- void handleStatusAction(proposal, 'reject')
738
- }
838
+ </DropdownMenuTrigger>
839
+ <DropdownMenuContent align="end">
840
+ {canEditProposal(proposal.status) ? (
841
+ <DropdownMenuItem onClick={() => handleEdit(proposal)}>
842
+ <Pencil className="mr-2 h-4 w-4" />
843
+ {proposalT('actions.edit')}
844
+ </DropdownMenuItem>
845
+ ) : null}
846
+
847
+ {canSubmitProposal(proposal.status) ? (
848
+ <DropdownMenuItem
849
+ disabled={actionKey === `submit-${proposal.id}`}
850
+ onClick={() => void handleStatusAction(proposal, 'submit')}
851
+ >
852
+ <Send className="mr-2 h-4 w-4" />
853
+ {proposalT('actions.submit')}
854
+ </DropdownMenuItem>
855
+ ) : null}
856
+
857
+ {proposal.status === 'pending_approval' &&
858
+ !proposal.current_user_has_approved ? (
859
+ <>
860
+ <DropdownMenuItem
861
+ disabled={actionKey === `approve-${proposal.id}`}
862
+ onClick={() => void handleStatusAction(proposal, 'approve')}
863
+ >
864
+ <CheckCircle2 className="mr-2 h-4 w-4" />
865
+ {proposalT('actions.approve')}
866
+ </DropdownMenuItem>
867
+ <DropdownMenuItem
868
+ disabled={actionKey === `reject-${proposal.id}`}
869
+ className="text-destructive focus:text-destructive"
870
+ onClick={() => void handleStatusAction(proposal, 'reject')}
871
+ >
872
+ <XCircle className="mr-2 h-4 w-4" />
873
+ {proposalT('actions.reject')}
874
+ </DropdownMenuItem>
875
+ </>
876
+ ) : null}
877
+
878
+ <DropdownMenuSeparator />
879
+
880
+ <DropdownMenuItem
881
+ disabled={actionKey === `generate-pdf-${proposal.id}`}
882
+ onClick={() => void handleGeneratePdf(proposal)}
739
883
  >
740
- {proposalT('actions.reject')}
741
- </Button>
742
- </>
743
- ) : null}
884
+ <FileText className="mr-2 h-4 w-4" />
885
+ {proposalT('actions.generatePdf')}
886
+ </DropdownMenuItem>
887
+ </DropdownMenuContent>
888
+ </DropdownMenu>
744
889
  </div>
745
890
  </CardContent>
746
891
  </Card>
@@ -768,6 +913,25 @@ export function ProposalsManagementPage({
768
913
  pageSizeOptions={[12, 24, 36, 48]}
769
914
  />
770
915
  </div>
916
+
917
+ {editSheetOpen && editingPersonId !== null ? (
918
+ <ProposalFormSheet
919
+ proposalId={editingProposalId}
920
+ personId={editingPersonId}
921
+ open={editSheetOpen}
922
+ onClose={() => {
923
+ setEditSheetOpen(false);
924
+ setEditingProposalId(null);
925
+ setEditingPersonId(null);
926
+ }}
927
+ onSaved={async () => {
928
+ setEditSheetOpen(false);
929
+ setEditingProposalId(null);
930
+ setEditingPersonId(null);
931
+ void refetch();
932
+ }}
933
+ />
934
+ ) : null}
771
935
  </Page>
772
936
  );
773
937
  }
@@ -819,6 +819,9 @@
819
819
  "refresh": "Refresh",
820
820
  "viewPending": "View pending",
821
821
  "showAll": "Show all"
822
+ },
823
+ "approval": {
824
+ "progress": "{count}/{required} approvals"
822
825
  }
823
826
  },
824
827
  "CrmPipeline": {
@@ -977,7 +980,7 @@
977
980
  "submit": "Submit for approval",
978
981
  "approve": "Approve",
979
982
  "reject": "Reject",
980
- "generatePdf": "Generate PDF",
983
+ "generatePdf": "Export as PDF",
981
984
  "openPdf": "Open PDF",
982
985
  "delete": "Delete"
983
986
  },
@@ -1035,7 +1038,11 @@
1035
1038
  "summary": "Summary",
1036
1039
  "summaryPlaceholder": "Write a short commercial summary for this proposal...",
1037
1040
  "notes": "Internal notes",
1038
- "notesPlaceholder": "Notes for the team..."
1041
+ "notesPlaceholder": "Notes for the team...",
1042
+ "client": "Client",
1043
+ "clientEntity": "Client",
1044
+ "clientPlaceholder": "Search or select a client...",
1045
+ "editClient": "Edit client"
1039
1046
  },
1040
1047
  "toasts": {
1041
1048
  "createSuccess": "Proposal created successfully",
@@ -1052,6 +1059,9 @@
1052
1059
  "generatePdfError": "Failed to generate proposal PDF",
1053
1060
  "deleteSuccess": "Proposal deleted successfully",
1054
1061
  "deleteError": "Failed to delete proposal"
1062
+ },
1063
+ "approval": {
1064
+ "progress": "{count}/{required} approvals"
1055
1065
  }
1056
1066
  },
1057
1067
  "tooltips": {
@@ -1385,5 +1395,13 @@
1385
1395
  "stateMaxLength": "State must be at most 2 characters"
1386
1396
  }
1387
1397
  }
1398
+ },
1399
+ "components": {
1400
+ "entityPicker": {
1401
+ "collaborators": {
1402
+ "empty": "No collaborators found.",
1403
+ "loading": "Loading collaborators..."
1404
+ }
1405
+ }
1388
1406
  }
1389
1407
  }
@@ -818,6 +818,9 @@
818
818
  "refresh": "Atualizar",
819
819
  "viewPending": "Ver pendentes",
820
820
  "showAll": "Mostrar todas"
821
+ },
822
+ "approval": {
823
+ "progress": "{count}/{required} aprovações"
821
824
  }
822
825
  },
823
826
  "CrmPipeline": {
@@ -976,7 +979,7 @@
976
979
  "submit": "Enviar para aprovação",
977
980
  "approve": "Aprovar",
978
981
  "reject": "Rejeitar",
979
- "generatePdf": "Gerar PDF",
982
+ "generatePdf": "Exportar em PDF",
980
983
  "openPdf": "Abrir PDF",
981
984
  "delete": "Excluir"
982
985
  },
@@ -1034,7 +1037,11 @@
1034
1037
  "summary": "Resumo",
1035
1038
  "summaryPlaceholder": "Escreva um resumo comercial desta proposta...",
1036
1039
  "notes": "Notas internas",
1037
- "notesPlaceholder": "Observações para a equipe..."
1040
+ "notesPlaceholder": "Observações para a equipe...",
1041
+ "client": "Cliente",
1042
+ "clientEntity": "Cliente",
1043
+ "clientPlaceholder": "Pesquisar ou selecionar cliente...",
1044
+ "editClient": "Editar cliente"
1038
1045
  },
1039
1046
  "toasts": {
1040
1047
  "createSuccess": "Proposta criada com sucesso",
@@ -1051,6 +1058,9 @@
1051
1058
  "generatePdfError": "Falha ao gerar o PDF da proposta",
1052
1059
  "deleteSuccess": "Proposta excluída com sucesso",
1053
1060
  "deleteError": "Falha ao excluir proposta"
1061
+ },
1062
+ "approval": {
1063
+ "progress": "{count}/{required} aprovações"
1054
1064
  }
1055
1065
  },
1056
1066
  "tooltips": {
@@ -1384,5 +1394,13 @@
1384
1394
  "stateMaxLength": "O estado deve ter no maximo 2 caracteres"
1385
1395
  }
1386
1396
  }
1397
+ },
1398
+ "components": {
1399
+ "entityPicker": {
1400
+ "collaborators": {
1401
+ "empty": "Nenhum colaborador encontrado.",
1402
+ "loading": "Carregando colaboradores..."
1403
+ }
1404
+ }
1387
1405
  }
1388
1406
  }