@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.
- 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 +71 -16
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +7 -2
- package/hedhog/frontend/app/accounts/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/activities/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/person/page.tsx.ejs +1 -1
- 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 +316 -113
- package/hedhog/frontend/messages/en.json +21 -2
- package/hedhog/frontend/messages/pt.json +21 -2
- package/package.json +7 -6
- package/src/proposal/proposal.controller.ts +7 -5
- 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: '
|
|
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="
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
<
|
|
587
|
-
|
|
679
|
+
<TableCell className="text-right">
|
|
680
|
+
<DropdownMenu>
|
|
681
|
+
<DropdownMenuTrigger asChild>
|
|
588
682
|
<Button
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
onClick={() =>
|
|
593
|
-
void handleStatusAction(proposal, 'submit')
|
|
594
|
-
}
|
|
683
|
+
variant="ghost"
|
|
684
|
+
size="icon"
|
|
685
|
+
className="h-8 w-8"
|
|
595
686
|
>
|
|
596
|
-
{
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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, '
|
|
712
|
+
void handleStatusAction(proposal, 'submit')
|
|
617
713
|
}
|
|
618
714
|
>
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
647
|
-
<
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
708
|
-
|
|
709
|
-
<
|
|
710
|
-
size="sm"
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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": "
|
|
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
|
}
|