@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.
- package/dist/proposal/proposal.controller.d.ts +2 -2
- package/dist/proposal/proposal.controller.d.ts.map +1 -1
- package/dist/proposal/proposal.controller.js +8 -6
- package/dist/proposal/proposal.controller.js.map +1 -1
- package/dist/proposal/proposal.service.d.ts +8 -2
- package/dist/proposal/proposal.service.d.ts.map +1 -1
- package/dist/proposal/proposal.service.js +595 -162
- package/dist/proposal/proposal.service.js.map +1 -1
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +4 -1
- package/hedhog/data/setting_group.yaml +16 -5
- package/hedhog/frontend/app/_components/person-picker.tsx.ejs +3 -1
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +7 -2
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +103 -1302
- package/hedhog/frontend/app/proposals/_components/proposal-form-sheet.tsx.ejs +1306 -0
- package/hedhog/frontend/app/proposals/_components/proposal-types.ts.ejs +172 -0
- package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +300 -136
- package/hedhog/frontend/messages/en.json +20 -2
- package/hedhog/frontend/messages/pt.json +20 -2
- package/package.json +8 -7
- package/src/proposal/proposal.controller.ts +7 -5
- package/src/proposal/proposal.service.ts +662 -192
|
@@ -1,43 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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="
|
|
547
|
-
<
|
|
548
|
-
{
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
<
|
|
587
|
-
|
|
588
|
-
<Button
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
{
|
|
610
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
647
|
-
<
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
708
|
-
|
|
709
|
-
<
|
|
710
|
-
size="sm"
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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": "
|
|
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": "
|
|
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
|
}
|