@hed-hog/contact 0.0.329 → 0.0.331

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 (26) hide show
  1. package/dist/proposal/proposal.controller.d.ts +2 -2
  2. package/dist/proposal/proposal.controller.d.ts.map +1 -1
  3. package/dist/proposal/proposal.controller.js +8 -6
  4. package/dist/proposal/proposal.controller.js.map +1 -1
  5. package/dist/proposal/proposal.service.d.ts +8 -2
  6. package/dist/proposal/proposal.service.d.ts.map +1 -1
  7. package/dist/proposal/proposal.service.js +595 -162
  8. package/dist/proposal/proposal.service.js.map +1 -1
  9. package/hedhog/data/role.yaml +9 -1
  10. package/hedhog/data/route.yaml +4 -1
  11. package/hedhog/data/setting_group.yaml +16 -5
  12. package/hedhog/frontend/app/_components/person-picker.tsx.ejs +71 -16
  13. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +7 -2
  14. package/hedhog/frontend/app/accounts/page.tsx.ejs +1 -1
  15. package/hedhog/frontend/app/activities/page.tsx.ejs +1 -1
  16. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +1 -1
  17. package/hedhog/frontend/app/person/page.tsx.ejs +1 -1
  18. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +103 -1302
  19. package/hedhog/frontend/app/proposals/_components/proposal-form-sheet.tsx.ejs +1306 -0
  20. package/hedhog/frontend/app/proposals/_components/proposal-types.ts.ejs +172 -0
  21. package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +316 -113
  22. package/hedhog/frontend/messages/en.json +21 -2
  23. package/hedhog/frontend/messages/pt.json +21 -2
  24. package/package.json +7 -6
  25. package/src/proposal/proposal.controller.ts +7 -5
  26. package/src/proposal/proposal.service.ts +662 -192
@@ -8,12 +8,19 @@ import {
8
8
  SearchBar,
9
9
  type SearchBarControl,
10
10
  } from '@/components/entity-list';
11
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
11
12
  import { Badge } from '@/components/ui/badge';
12
13
  import { Button } from '@/components/ui/button';
13
14
  import { Card, CardContent } from '@/components/ui/card';
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuSeparator,
20
+ DropdownMenuTrigger,
21
+ } from '@/components/ui/dropdown-menu';
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
25
  Table,
19
26
  TableBody,
@@ -22,6 +29,7 @@ import {
22
29
  TableHeader,
23
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 {
@@ -31,13 +39,23 @@ import {
31
39
  FileText,
32
40
  LayoutGrid,
33
41
  List,
42
+ Loader2,
43
+ MoreHorizontal,
44
+ Pencil,
34
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,15 @@ 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 (
165
+ (parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')
166
+ ).toUpperCase();
167
+ }
168
+
138
169
  function canSubmitProposal(status?: ProposalStatus | null) {
139
170
  return (
140
171
  status !== 'approved' &&
@@ -144,6 +175,15 @@ function canSubmitProposal(status?: ProposalStatus | null) {
144
175
  );
145
176
  }
146
177
 
178
+ function canEditProposal(status?: ProposalStatus | null) {
179
+ return status !== 'approved' && status !== 'contract_generated';
180
+ }
181
+
182
+ function getPersonAvatarUrl(avatarId?: number | null) {
183
+ if (!avatarId) return undefined;
184
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`;
185
+ }
186
+
147
187
  export function ProposalsManagementPage({
148
188
  defaultStatus = 'all',
149
189
  }: ProposalsManagementPageProps) {
@@ -160,6 +200,11 @@ export function ProposalsManagementPage({
160
200
  const [pageSize, setPageSize] = useState(12);
161
201
  const [viewMode, setViewMode] = useState<ProposalViewMode>('table');
162
202
  const [actionKey, setActionKey] = useState<string | null>(null);
203
+ const [editSheetOpen, setEditSheetOpen] = useState(false);
204
+ const [editingProposalId, setEditingProposalId] = useState<number | null>(
205
+ null
206
+ );
207
+ const [editingPersonId, setEditingPersonId] = useState<number | null>(null);
163
208
 
164
209
  useEffect(() => {
165
210
  setStatusFilter(defaultStatus);
@@ -343,6 +388,35 @@ export function ProposalsManagementPage({
343
388
  }
344
389
  };
345
390
 
391
+ const handleEdit = (proposal: ProposalRecord) => {
392
+ setEditingProposalId(proposal.id);
393
+ setEditingPersonId(proposal.person_id);
394
+ setEditSheetOpen(true);
395
+ };
396
+
397
+ const handleGeneratePdf = async (proposal: ProposalRecord) => {
398
+ try {
399
+ setActionKey(`generate-pdf-${proposal.id}`);
400
+ const response = await request<GenerateProposalDocumentResponse>({
401
+ url: `/proposal/${proposal.id}/generate-pdf`,
402
+ method: 'POST',
403
+ data: {},
404
+ });
405
+ const fileId = response.data?.fileId;
406
+ toast.success(proposalT('toasts.generatePdfSuccess'));
407
+ if (fileId) openStoredFile(fileId);
408
+ void refetch();
409
+ } catch (error) {
410
+ toast.error(
411
+ error instanceof Error
412
+ ? error.message
413
+ : proposalT('toasts.generatePdfError')
414
+ );
415
+ } finally {
416
+ setActionKey(null);
417
+ }
418
+ };
419
+
346
420
  const statsCards = [
347
421
  {
348
422
  key: 'total',
@@ -382,7 +456,7 @@ export function ProposalsManagementPage({
382
456
  <Page>
383
457
  <PageHeader
384
458
  breadcrumbs={[
385
- { label: 'Home', href: '/' },
459
+ { label: crmT('breadcrumbs.home'), href: '/' },
386
460
  { label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
387
461
  { label: pageTitle },
388
462
  ]}
@@ -543,28 +617,48 @@ export function ProposalsManagementPage({
543
617
  </TableCell>
544
618
 
545
619
  <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
- '—'}
620
+ <div className="flex items-center gap-3">
621
+ <Avatar className="h-8 w-8 shrink-0">
622
+ <AvatarImage
623
+ src={getPersonAvatarUrl(proposal.person?.avatar_id)}
624
+ />
625
+ <AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
626
+ {getAvatarInitials(customerName)}
627
+ </AvatarFallback>
628
+ </Avatar>
629
+ <div className="min-w-0 space-y-0.5">
630
+ <div className="truncate font-medium text-foreground">
631
+ {customerName}
632
+ </div>
633
+ <div className="text-xs text-muted-foreground">
634
+ {proposal.person?.email ||
635
+ proposal.person?.phone ||
636
+ '—'}
637
+ </div>
554
638
  </div>
555
639
  </div>
556
640
  </TableCell>
557
641
 
558
642
  <TableCell>
559
- <Badge
560
- variant="outline"
561
- className={cn(
562
- 'font-medium',
563
- getStatusBadgeClassName(proposal.status)
564
- )}
565
- >
566
- {getStatusLabel(proposal.status)}
567
- </Badge>
643
+ <div className="flex flex-col gap-1">
644
+ <Badge
645
+ variant="outline"
646
+ className={cn(
647
+ 'font-medium',
648
+ getStatusBadgeClassName(proposal.status)
649
+ )}
650
+ >
651
+ {getStatusLabel(proposal.status)}
652
+ </Badge>
653
+ {proposal.status === 'pending_approval' ? (
654
+ <span className="text-xs text-muted-foreground">
655
+ {t('approval.progress', {
656
+ count: proposal.approval_count ?? 0,
657
+ required: proposal.required_approvals ?? 1,
658
+ })}
659
+ </span>
660
+ ) : null}
661
+ </div>
568
662
  </TableCell>
569
663
 
570
664
  <TableCell className="text-right font-medium">
@@ -582,45 +676,87 @@ export function ProposalsManagementPage({
582
676
  {formatShortDate(proposal.updated_at, locale)}
583
677
  </TableCell>
584
678
 
585
- <TableCell>
586
- <div className="flex flex-wrap justify-end gap-2">
587
- {canSubmitProposal(proposal.status) ? (
679
+ <TableCell className="text-right">
680
+ <DropdownMenu>
681
+ <DropdownMenuTrigger asChild>
588
682
  <Button
589
- size="sm"
590
- variant="outline"
591
- disabled={actionKey === `submit-${proposal.id}`}
592
- onClick={() =>
593
- void handleStatusAction(proposal, 'submit')
594
- }
683
+ variant="ghost"
684
+ size="icon"
685
+ className="h-8 w-8"
595
686
  >
596
- {proposalT('actions.submit')}
687
+ {actionKey?.startsWith(`${proposal.id}-`) ||
688
+ actionKey === `generate-pdf-${proposal.id}` ||
689
+ actionKey === `submit-${proposal.id}` ||
690
+ actionKey === `approve-${proposal.id}` ||
691
+ actionKey === `reject-${proposal.id}` ? (
692
+ <Loader2 className="h-4 w-4 animate-spin" />
693
+ ) : (
694
+ <MoreHorizontal className="h-4 w-4" />
695
+ )}
597
696
  </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
- }
697
+ </DropdownMenuTrigger>
698
+ <DropdownMenuContent align="end">
699
+ {canEditProposal(proposal.status) ? (
700
+ <DropdownMenuItem
701
+ onClick={() => handleEdit(proposal)}
608
702
  >
609
- {proposalT('actions.approve')}
610
- </Button>
611
- <Button
612
- size="sm"
613
- variant="destructive"
614
- disabled={actionKey === `reject-${proposal.id}`}
703
+ <Pencil className="mr-2 h-4 w-4" />
704
+ {proposalT('actions.edit')}
705
+ </DropdownMenuItem>
706
+ ) : null}
707
+
708
+ {canSubmitProposal(proposal.status) ? (
709
+ <DropdownMenuItem
710
+ disabled={actionKey === `submit-${proposal.id}`}
615
711
  onClick={() =>
616
- void handleStatusAction(proposal, 'reject')
712
+ void handleStatusAction(proposal, 'submit')
617
713
  }
618
714
  >
619
- {proposalT('actions.reject')}
620
- </Button>
621
- </>
622
- ) : null}
623
- </div>
715
+ <Send className="mr-2 h-4 w-4" />
716
+ {proposalT('actions.submit')}
717
+ </DropdownMenuItem>
718
+ ) : null}
719
+
720
+ {proposal.status === 'pending_approval' &&
721
+ !proposal.current_user_has_approved ? (
722
+ <>
723
+ <DropdownMenuItem
724
+ disabled={
725
+ actionKey === `approve-${proposal.id}`
726
+ }
727
+ onClick={() =>
728
+ void handleStatusAction(proposal, 'approve')
729
+ }
730
+ >
731
+ <CheckCircle2 className="mr-2 h-4 w-4" />
732
+ {proposalT('actions.approve')}
733
+ </DropdownMenuItem>
734
+ <DropdownMenuItem
735
+ disabled={actionKey === `reject-${proposal.id}`}
736
+ className="text-destructive focus:text-destructive"
737
+ onClick={() =>
738
+ void handleStatusAction(proposal, 'reject')
739
+ }
740
+ >
741
+ <XCircle className="mr-2 h-4 w-4" />
742
+ {proposalT('actions.reject')}
743
+ </DropdownMenuItem>
744
+ </>
745
+ ) : null}
746
+
747
+ <DropdownMenuSeparator />
748
+
749
+ <DropdownMenuItem
750
+ disabled={
751
+ actionKey === `generate-pdf-${proposal.id}`
752
+ }
753
+ onClick={() => void handleGeneratePdf(proposal)}
754
+ >
755
+ <FileText className="mr-2 h-4 w-4" />
756
+ {proposalT('actions.generatePdf')}
757
+ </DropdownMenuItem>
758
+ </DropdownMenuContent>
759
+ </DropdownMenu>
624
760
  </TableCell>
625
761
  </TableRow>
626
762
  );
@@ -643,37 +779,49 @@ export function ProposalsManagementPage({
643
779
  >
644
780
  <CardContent className="flex h-full flex-col gap-3 p-4">
645
781
  <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>
782
+ <div className="flex min-w-0 items-start gap-2.5">
783
+ <Avatar className="mt-0.5 h-8 w-8 shrink-0">
784
+ <AvatarImage
785
+ src={getPersonAvatarUrl(proposal.person?.avatar_id)}
786
+ />
787
+ <AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
788
+ {getAvatarInitials(customerName)}
789
+ </AvatarFallback>
790
+ </Avatar>
791
+ <div className="min-w-0 space-y-1">
792
+ <p className="line-clamp-2 text-sm font-semibold text-foreground">
793
+ {proposal.title}
794
+ </p>
795
+ <p className="text-xs text-muted-foreground">
796
+ {customerName} · {proposal.code || `#${proposal.id}`}
797
+ {proposal.current_revision_number
798
+ ? ` · v${proposal.current_revision_number}`
799
+ : ''}
800
+ </p>
801
+ </div>
802
+ </div>
803
+ <div className="flex shrink-0 flex-col items-end gap-1">
804
+ <Badge
805
+ variant="outline"
806
+ className={cn(
807
+ 'font-medium',
808
+ getStatusBadgeClassName(proposal.status)
809
+ )}
810
+ >
811
+ {getStatusLabel(proposal.status)}
812
+ </Badge>
813
+ {proposal.status === 'pending_approval' ? (
814
+ <span className="text-xs text-muted-foreground">
815
+ {t('approval.progress', {
816
+ count: proposal.approval_count ?? 0,
817
+ required: proposal.required_approvals ?? 1,
818
+ })}
819
+ </span>
820
+ ) : null}
656
821
  </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
822
  </div>
667
823
 
668
824
  <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
825
  <div className="flex items-center justify-between gap-2">
678
826
  <span className="text-muted-foreground">
679
827
  {t('columns.total')}
@@ -704,43 +852,79 @@ export function ProposalsManagementPage({
704
852
  </div>
705
853
  </div>
706
854
 
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')}
855
+ <div className="mt-auto flex justify-end">
856
+ <DropdownMenu>
857
+ <DropdownMenuTrigger asChild>
858
+ <Button variant="outline" size="sm" className="gap-1.5">
859
+ {actionKey === `generate-pdf-${proposal.id}` ||
860
+ actionKey === `submit-${proposal.id}` ||
861
+ actionKey === `approve-${proposal.id}` ||
862
+ actionKey === `reject-${proposal.id}` ? (
863
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
864
+ ) : (
865
+ <MoreHorizontal className="h-3.5 w-3.5" />
866
+ )}
867
+ {t('columns.actions')}
731
868
  </Button>
732
- <Button
733
- size="sm"
734
- variant="destructive"
735
- disabled={actionKey === `reject-${proposal.id}`}
736
- onClick={() =>
737
- void handleStatusAction(proposal, 'reject')
738
- }
869
+ </DropdownMenuTrigger>
870
+ <DropdownMenuContent align="end">
871
+ {canEditProposal(proposal.status) ? (
872
+ <DropdownMenuItem
873
+ onClick={() => handleEdit(proposal)}
874
+ >
875
+ <Pencil className="mr-2 h-4 w-4" />
876
+ {proposalT('actions.edit')}
877
+ </DropdownMenuItem>
878
+ ) : null}
879
+
880
+ {canSubmitProposal(proposal.status) ? (
881
+ <DropdownMenuItem
882
+ disabled={actionKey === `submit-${proposal.id}`}
883
+ onClick={() =>
884
+ void handleStatusAction(proposal, 'submit')
885
+ }
886
+ >
887
+ <Send className="mr-2 h-4 w-4" />
888
+ {proposalT('actions.submit')}
889
+ </DropdownMenuItem>
890
+ ) : null}
891
+
892
+ {proposal.status === 'pending_approval' &&
893
+ !proposal.current_user_has_approved ? (
894
+ <>
895
+ <DropdownMenuItem
896
+ disabled={actionKey === `approve-${proposal.id}`}
897
+ onClick={() =>
898
+ void handleStatusAction(proposal, 'approve')
899
+ }
900
+ >
901
+ <CheckCircle2 className="mr-2 h-4 w-4" />
902
+ {proposalT('actions.approve')}
903
+ </DropdownMenuItem>
904
+ <DropdownMenuItem
905
+ disabled={actionKey === `reject-${proposal.id}`}
906
+ className="text-destructive focus:text-destructive"
907
+ onClick={() =>
908
+ void handleStatusAction(proposal, 'reject')
909
+ }
910
+ >
911
+ <XCircle className="mr-2 h-4 w-4" />
912
+ {proposalT('actions.reject')}
913
+ </DropdownMenuItem>
914
+ </>
915
+ ) : null}
916
+
917
+ <DropdownMenuSeparator />
918
+
919
+ <DropdownMenuItem
920
+ disabled={actionKey === `generate-pdf-${proposal.id}`}
921
+ onClick={() => void handleGeneratePdf(proposal)}
739
922
  >
740
- {proposalT('actions.reject')}
741
- </Button>
742
- </>
743
- ) : null}
923
+ <FileText className="mr-2 h-4 w-4" />
924
+ {proposalT('actions.generatePdf')}
925
+ </DropdownMenuItem>
926
+ </DropdownMenuContent>
927
+ </DropdownMenu>
744
928
  </div>
745
929
  </CardContent>
746
930
  </Card>
@@ -768,6 +952,25 @@ export function ProposalsManagementPage({
768
952
  pageSizeOptions={[12, 24, 36, 48]}
769
953
  />
770
954
  </div>
955
+
956
+ {editSheetOpen && editingPersonId !== null ? (
957
+ <ProposalFormSheet
958
+ proposalId={editingProposalId}
959
+ personId={editingPersonId}
960
+ open={editSheetOpen}
961
+ onClose={() => {
962
+ setEditSheetOpen(false);
963
+ setEditingProposalId(null);
964
+ setEditingPersonId(null);
965
+ }}
966
+ onSaved={async () => {
967
+ setEditSheetOpen(false);
968
+ setEditingProposalId(null);
969
+ setEditingPersonId(null);
970
+ void refetch();
971
+ }}
972
+ />
973
+ ) : null}
771
974
  </Page>
772
975
  );
773
976
  }
@@ -501,6 +501,7 @@
501
501
  },
502
502
  "CrmMenu": {
503
503
  "breadcrumbs": {
504
+ "home": "Home",
504
505
  "crm": "CRM"
505
506
  },
506
507
  "current": "Current",
@@ -819,6 +820,9 @@
819
820
  "refresh": "Refresh",
820
821
  "viewPending": "View pending",
821
822
  "showAll": "Show all"
823
+ },
824
+ "approval": {
825
+ "progress": "{count}/{required} approvals"
822
826
  }
823
827
  },
824
828
  "CrmPipeline": {
@@ -977,7 +981,7 @@
977
981
  "submit": "Submit for approval",
978
982
  "approve": "Approve",
979
983
  "reject": "Reject",
980
- "generatePdf": "Generate PDF",
984
+ "generatePdf": "Export as PDF",
981
985
  "openPdf": "Open PDF",
982
986
  "delete": "Delete"
983
987
  },
@@ -1035,7 +1039,11 @@
1035
1039
  "summary": "Summary",
1036
1040
  "summaryPlaceholder": "Write a short commercial summary for this proposal...",
1037
1041
  "notes": "Internal notes",
1038
- "notesPlaceholder": "Notes for the team..."
1042
+ "notesPlaceholder": "Notes for the team...",
1043
+ "client": "Client",
1044
+ "clientEntity": "Client",
1045
+ "clientPlaceholder": "Search or select a client...",
1046
+ "editClient": "Edit client"
1039
1047
  },
1040
1048
  "toasts": {
1041
1049
  "createSuccess": "Proposal created successfully",
@@ -1052,6 +1060,9 @@
1052
1060
  "generatePdfError": "Failed to generate proposal PDF",
1053
1061
  "deleteSuccess": "Proposal deleted successfully",
1054
1062
  "deleteError": "Failed to delete proposal"
1063
+ },
1064
+ "approval": {
1065
+ "progress": "{count}/{required} approvals"
1055
1066
  }
1056
1067
  },
1057
1068
  "tooltips": {
@@ -1385,5 +1396,13 @@
1385
1396
  "stateMaxLength": "State must be at most 2 characters"
1386
1397
  }
1387
1398
  }
1399
+ },
1400
+ "components": {
1401
+ "entityPicker": {
1402
+ "collaborators": {
1403
+ "empty": "No collaborators found.",
1404
+ "loading": "Loading collaborators..."
1405
+ }
1406
+ }
1388
1407
  }
1389
1408
  }