@hed-hog/contact 0.0.330 → 0.0.332
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/person/person.controller.d.ts +1 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.service.d.ts +1 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +3 -1
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/frontend/app/_components/person-picker.tsx.ejs +68 -15
- 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/_components/person-form-sheet.tsx.ejs +20 -1
- package/hedhog/frontend/app/person/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +163 -98
- package/hedhog/frontend/messages/en.json +1 -0
- package/hedhog/frontend/messages/pt.json +1 -0
- package/package.json +5 -5
- package/src/person/person.service.ts +2 -0
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from '@/components/ui/sheet';
|
|
42
42
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
43
43
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
44
|
-
import { ChevronsUpDown, Loader2, Plus, X } from 'lucide-react';
|
|
44
|
+
import { ChevronsUpDown, Loader2, Pencil, Plus, X } from 'lucide-react';
|
|
45
45
|
import { useTranslations } from 'next-intl';
|
|
46
46
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
47
47
|
import {
|
|
@@ -60,6 +60,10 @@ type PersonOption = {
|
|
|
60
60
|
avatar_id?: number | null;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
type PersonOptionApi = PersonOption & {
|
|
64
|
+
avatar_id?: number | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
63
67
|
type CreatePersonValues = {
|
|
64
68
|
name: string;
|
|
65
69
|
type: 'individual' | 'company';
|
|
@@ -563,6 +567,8 @@ export function PersonPicker({
|
|
|
563
567
|
entityLabel,
|
|
564
568
|
value,
|
|
565
569
|
onChange,
|
|
570
|
+
onEditSelection,
|
|
571
|
+
onCreateNew,
|
|
566
572
|
selectPlaceholder,
|
|
567
573
|
personTypeFilter = 'all',
|
|
568
574
|
createType = 'individual',
|
|
@@ -570,11 +576,15 @@ export function PersonPicker({
|
|
|
570
576
|
initialSelectedLabel = '',
|
|
571
577
|
disabled = false,
|
|
572
578
|
clearable = true,
|
|
579
|
+
showEditButton = false,
|
|
580
|
+
editAriaLabel,
|
|
573
581
|
}: {
|
|
574
582
|
label: string;
|
|
575
583
|
entityLabel: string;
|
|
576
584
|
value?: string | number | null;
|
|
577
585
|
onChange: (personId: number | null, personName: string) => void;
|
|
586
|
+
onEditSelection?: (personId: number) => void;
|
|
587
|
+
onCreateNew?: (initialName?: string) => void;
|
|
578
588
|
selectPlaceholder: string;
|
|
579
589
|
personTypeFilter?: PersonTypeFilter;
|
|
580
590
|
createType?: Exclude<PersonTypeFilter, 'all'>;
|
|
@@ -582,6 +592,8 @@ export function PersonPicker({
|
|
|
582
592
|
initialSelectedLabel?: string;
|
|
583
593
|
disabled?: boolean;
|
|
584
594
|
clearable?: boolean;
|
|
595
|
+
showEditButton?: boolean;
|
|
596
|
+
editAriaLabel?: string;
|
|
585
597
|
}) {
|
|
586
598
|
const { request } = useApp();
|
|
587
599
|
const t = usePersonPickerTranslations();
|
|
@@ -685,9 +697,11 @@ export function PersonPicker({
|
|
|
685
697
|
}
|
|
686
698
|
|
|
687
699
|
if (payload && 'data' in payload && Array.isArray(payload.data)) {
|
|
688
|
-
|
|
700
|
+
const options = payload.data as PersonOptionApi[];
|
|
701
|
+
|
|
702
|
+
return options.map((p) => ({
|
|
689
703
|
...p,
|
|
690
|
-
avatarId:
|
|
704
|
+
avatarId: p.avatar_id ?? p.avatarId ?? null,
|
|
691
705
|
})) as PersonOption[];
|
|
692
706
|
}
|
|
693
707
|
|
|
@@ -701,6 +715,9 @@ export function PersonPicker({
|
|
|
701
715
|
? String(value)
|
|
702
716
|
: '';
|
|
703
717
|
const hasValue = normalizedValue.length > 0;
|
|
718
|
+
const selectedPersonId = hasValue ? Number(normalizedValue) : Number.NaN;
|
|
719
|
+
const canEditSelection =
|
|
720
|
+
Number.isFinite(selectedPersonId) && selectedPersonId > 0;
|
|
704
721
|
|
|
705
722
|
const selectedPersonFromList = personOptionsData.find(
|
|
706
723
|
(p) => String(p.id) === normalizedValue
|
|
@@ -750,7 +767,7 @@ export function PersonPicker({
|
|
|
750
767
|
<div className="grid min-w-0 gap-2">
|
|
751
768
|
{label ? <Label>{label}</Label> : null}
|
|
752
769
|
|
|
753
|
-
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2 sm:grid-cols-[minmax(0,1fr)
|
|
770
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-2 sm:grid-cols-[minmax(0,1fr)_auto_auto_auto]">
|
|
754
771
|
<Popover
|
|
755
772
|
open={!disabled && personOpen}
|
|
756
773
|
onOpenChange={(open) => {
|
|
@@ -816,10 +833,15 @@ export function PersonPicker({
|
|
|
816
833
|
personOptionsData.length === 0
|
|
817
834
|
) {
|
|
818
835
|
e.preventDefault();
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
personSearch.trim()
|
|
822
|
-
|
|
836
|
+
if (onCreateNew) {
|
|
837
|
+
setPersonOpen(false);
|
|
838
|
+
onCreateNew(personSearch.trim());
|
|
839
|
+
} else {
|
|
840
|
+
openCreateWithPrefill(
|
|
841
|
+
e.currentTarget,
|
|
842
|
+
personSearch.trim()
|
|
843
|
+
);
|
|
844
|
+
}
|
|
823
845
|
}
|
|
824
846
|
}}
|
|
825
847
|
/>
|
|
@@ -837,10 +859,15 @@ export function PersonPicker({
|
|
|
837
859
|
variant="outline"
|
|
838
860
|
className="w-full"
|
|
839
861
|
onClick={(event) => {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
personSearch.trim()
|
|
843
|
-
|
|
862
|
+
if (onCreateNew) {
|
|
863
|
+
setPersonOpen(false);
|
|
864
|
+
onCreateNew(personSearch.trim());
|
|
865
|
+
} else {
|
|
866
|
+
openCreateWithPrefill(
|
|
867
|
+
event.currentTarget,
|
|
868
|
+
personSearch.trim()
|
|
869
|
+
);
|
|
870
|
+
}
|
|
844
871
|
}}
|
|
845
872
|
>
|
|
846
873
|
{t('actions.createNew')}
|
|
@@ -914,6 +941,27 @@ export function PersonPicker({
|
|
|
914
941
|
</Button>
|
|
915
942
|
) : null}
|
|
916
943
|
|
|
944
|
+
{showEditButton ? (
|
|
945
|
+
<Button
|
|
946
|
+
type="button"
|
|
947
|
+
variant="outline"
|
|
948
|
+
size="icon"
|
|
949
|
+
className="shrink-0"
|
|
950
|
+
disabled={disabled || !canEditSelection}
|
|
951
|
+
onClick={() => {
|
|
952
|
+
if (!canEditSelection || !onEditSelection) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
onEditSelection(selectedPersonId);
|
|
956
|
+
}}
|
|
957
|
+
aria-label={
|
|
958
|
+
editAriaLabel ?? t('actions.createEntityAria', { entityLabel })
|
|
959
|
+
}
|
|
960
|
+
>
|
|
961
|
+
<Pencil className="h-4 w-4" />
|
|
962
|
+
</Button>
|
|
963
|
+
) : null}
|
|
964
|
+
|
|
917
965
|
<Button
|
|
918
966
|
type="button"
|
|
919
967
|
variant="outline"
|
|
@@ -921,9 +969,14 @@ export function PersonPicker({
|
|
|
921
969
|
className="shrink-0"
|
|
922
970
|
disabled={disabled}
|
|
923
971
|
onClick={(event) => {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
972
|
+
if (onCreateNew) {
|
|
973
|
+
setPersonOpen(false);
|
|
974
|
+
onCreateNew(personSearch.trim());
|
|
975
|
+
} else {
|
|
976
|
+
captureParentScrollPosition(event.currentTarget);
|
|
977
|
+
setPersonOpen(false);
|
|
978
|
+
setCreatePersonOpen(true);
|
|
979
|
+
}
|
|
927
980
|
}}
|
|
928
981
|
aria-label={t('actions.createEntityAria', { entityLabel })}
|
|
929
982
|
>
|
|
@@ -801,7 +801,7 @@ export default function AccountsPage() {
|
|
|
801
801
|
<Page>
|
|
802
802
|
<PageHeader
|
|
803
803
|
breadcrumbs={[
|
|
804
|
-
{ label: '
|
|
804
|
+
{ label: crmT('breadcrumbs.home'), href: '/' },
|
|
805
805
|
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
806
806
|
{ label: t('title') },
|
|
807
807
|
]}
|
|
@@ -311,7 +311,7 @@ export default function CrmActivitiesPage() {
|
|
|
311
311
|
<Page>
|
|
312
312
|
<PageHeader
|
|
313
313
|
breadcrumbs={[
|
|
314
|
-
{ label: '
|
|
314
|
+
{ label: crmT('breadcrumbs.home'), href: '/' },
|
|
315
315
|
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
316
316
|
{ label: t('title') },
|
|
317
317
|
]}
|
|
@@ -619,7 +619,7 @@ export default function CrmFollowupsPage() {
|
|
|
619
619
|
<Page>
|
|
620
620
|
<PageHeader
|
|
621
621
|
breadcrumbs={[
|
|
622
|
-
{ label: '
|
|
622
|
+
{ label: crmT('breadcrumbs.home'), href: '/' },
|
|
623
623
|
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
624
624
|
{ label: t('title') },
|
|
625
625
|
]}
|
|
@@ -1146,6 +1146,11 @@ export function PersonFormSheet({
|
|
|
1146
1146
|
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${fileId}`
|
|
1147
1147
|
: '/placeholder.png';
|
|
1148
1148
|
|
|
1149
|
+
const getUserPhotoUrl = (photoId?: number | null) =>
|
|
1150
|
+
typeof photoId === 'number' && photoId > 0
|
|
1151
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
|
|
1152
|
+
: undefined;
|
|
1153
|
+
|
|
1149
1154
|
const resolveApiUrl = (url: string) =>
|
|
1150
1155
|
/^https?:\/\//i.test(url)
|
|
1151
1156
|
? url
|
|
@@ -2508,6 +2513,15 @@ export function PersonFormSheet({
|
|
|
2508
2513
|
'')
|
|
2509
2514
|
: ''
|
|
2510
2515
|
);
|
|
2516
|
+
const userPhotoId = (
|
|
2517
|
+
opt as { photo_id?: number | null } | null
|
|
2518
|
+
)?.photo_id;
|
|
2519
|
+
if (val != null && userPhotoId && !avatarId) {
|
|
2520
|
+
setAvatarId(userPhotoId);
|
|
2521
|
+
setAvatarPreviewUrl(
|
|
2522
|
+
getUserPhotoUrl(userPhotoId) ?? '/placeholder.png'
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2511
2525
|
}}
|
|
2512
2526
|
placeholder={t('selectLinkedUser')}
|
|
2513
2527
|
entityLabel={t('user')}
|
|
@@ -2518,7 +2532,12 @@ export function PersonFormSheet({
|
|
|
2518
2532
|
const params = new URLSearchParams();
|
|
2519
2533
|
if (search) params.set('search', search);
|
|
2520
2534
|
const response = await request<
|
|
2521
|
-
Array<{
|
|
2535
|
+
Array<{
|
|
2536
|
+
id: number;
|
|
2537
|
+
name: string;
|
|
2538
|
+
email?: string;
|
|
2539
|
+
photo_id?: number | null;
|
|
2540
|
+
}>
|
|
2522
2541
|
>({
|
|
2523
2542
|
url: `/person/linked-user-options?${params.toString()}`,
|
|
2524
2543
|
method: 'GET',
|
|
@@ -791,7 +791,7 @@ export default function PeoplePage() {
|
|
|
791
791
|
<Page>
|
|
792
792
|
<PageHeader
|
|
793
793
|
breadcrumbs={[
|
|
794
|
-
{ label: '
|
|
794
|
+
{ label: crmT('breadcrumbs.home'), href: '/' },
|
|
795
795
|
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
796
796
|
{
|
|
797
797
|
label: allowCompanyRegistration
|
|
@@ -1,50 +1,50 @@
|
|
|
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
|
+
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';
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
20
21
|
} from '@/components/ui/dropdown-menu';
|
|
21
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
22
22
|
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
23
23
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
Table,
|
|
26
|
+
TableBody,
|
|
27
|
+
TableCell,
|
|
28
|
+
TableHead,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
31
|
} from '@/components/ui/table';
|
|
32
32
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
33
33
|
import { cn } from '@/lib/utils';
|
|
34
34
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
35
35
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
CheckCircle2,
|
|
37
|
+
Clock3,
|
|
38
|
+
FileCheck2,
|
|
39
|
+
FileText,
|
|
40
|
+
LayoutGrid,
|
|
41
|
+
List,
|
|
42
|
+
Loader2,
|
|
43
|
+
MoreHorizontal,
|
|
44
|
+
Pencil,
|
|
45
|
+
RefreshCcw,
|
|
46
|
+
Send,
|
|
47
|
+
XCircle,
|
|
48
48
|
} from 'lucide-react';
|
|
49
49
|
import { useTranslations } from 'next-intl';
|
|
50
50
|
import { useEffect, useMemo, useState } from 'react';
|
|
@@ -53,8 +53,8 @@ import { toast } from 'sonner';
|
|
|
53
53
|
import type { PaginatedResult } from '../../person/_components/person-types';
|
|
54
54
|
import { ProposalFormSheet } from './proposal-form-sheet';
|
|
55
55
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
openStoredFile,
|
|
57
|
+
type GenerateProposalDocumentResponse,
|
|
58
58
|
} from './proposal-types';
|
|
59
59
|
|
|
60
60
|
type ProposalStatus =
|
|
@@ -161,7 +161,9 @@ function getAvatarInitials(name?: string | null) {
|
|
|
161
161
|
if (!name) return '?';
|
|
162
162
|
const parts = name.trim().split(/\s+/);
|
|
163
163
|
if (parts.length === 1) return (parts[0] ?? '').slice(0, 2).toUpperCase();
|
|
164
|
-
return (
|
|
164
|
+
return (
|
|
165
|
+
(parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')
|
|
166
|
+
).toUpperCase();
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
function canSubmitProposal(status?: ProposalStatus | null) {
|
|
@@ -199,7 +201,9 @@ export function ProposalsManagementPage({
|
|
|
199
201
|
const [viewMode, setViewMode] = useState<ProposalViewMode>('table');
|
|
200
202
|
const [actionKey, setActionKey] = useState<string | null>(null);
|
|
201
203
|
const [editSheetOpen, setEditSheetOpen] = useState(false);
|
|
202
|
-
const [editingProposalId, setEditingProposalId] = useState<number | null>(
|
|
204
|
+
const [editingProposalId, setEditingProposalId] = useState<number | null>(
|
|
205
|
+
null
|
|
206
|
+
);
|
|
203
207
|
const [editingPersonId, setEditingPersonId] = useState<number | null>(null);
|
|
204
208
|
|
|
205
209
|
useEffect(() => {
|
|
@@ -452,7 +456,7 @@ export function ProposalsManagementPage({
|
|
|
452
456
|
<Page>
|
|
453
457
|
<PageHeader
|
|
454
458
|
breadcrumbs={[
|
|
455
|
-
{ label: '
|
|
459
|
+
{ label: crmT('breadcrumbs.home'), href: '/' },
|
|
456
460
|
{ label: crmT('breadcrumbs.crm'), href: '/contact/dashboard' },
|
|
457
461
|
{ label: pageTitle },
|
|
458
462
|
]}
|
|
@@ -615,7 +619,9 @@ export function ProposalsManagementPage({
|
|
|
615
619
|
<TableCell>
|
|
616
620
|
<div className="flex items-center gap-3">
|
|
617
621
|
<Avatar className="h-8 w-8 shrink-0">
|
|
618
|
-
<AvatarImage
|
|
622
|
+
<AvatarImage
|
|
623
|
+
src={getPersonAvatarUrl(proposal.person?.avatar_id)}
|
|
624
|
+
/>
|
|
619
625
|
<AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
|
|
620
626
|
{getAvatarInitials(customerName)}
|
|
621
627
|
</AvatarFallback>
|
|
@@ -671,66 +677,101 @@ export function ProposalsManagementPage({
|
|
|
671
677
|
</TableCell>
|
|
672
678
|
|
|
673
679
|
<TableCell className="text-right">
|
|
674
|
-
<
|
|
675
|
-
|
|
676
|
-
<Button
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
680
|
+
<div className="flex items-center justify-end gap-2">
|
|
681
|
+
{canEditProposal(proposal.status) ? (
|
|
682
|
+
<Button
|
|
683
|
+
variant="outline"
|
|
684
|
+
size="sm"
|
|
685
|
+
className="gap-1.5"
|
|
686
|
+
onClick={() => handleEdit(proposal)}
|
|
687
|
+
>
|
|
688
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
689
|
+
{proposalT('actions.edit')}
|
|
682
690
|
</Button>
|
|
683
|
-
|
|
684
|
-
<
|
|
685
|
-
|
|
686
|
-
<
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
) : null}
|
|
691
|
-
|
|
692
|
-
{canSubmitProposal(proposal.status) ? (
|
|
693
|
-
<DropdownMenuItem
|
|
694
|
-
disabled={actionKey === `submit-${proposal.id}`}
|
|
695
|
-
onClick={() => void handleStatusAction(proposal, 'submit')}
|
|
691
|
+
) : null}
|
|
692
|
+
<DropdownMenu>
|
|
693
|
+
<DropdownMenuTrigger asChild>
|
|
694
|
+
<Button
|
|
695
|
+
variant="ghost"
|
|
696
|
+
size="icon"
|
|
697
|
+
className="h-8 w-8"
|
|
696
698
|
>
|
|
697
|
-
|
|
698
|
-
{
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
699
|
+
{actionKey?.startsWith(`${proposal.id}-`) ||
|
|
700
|
+
actionKey === `generate-pdf-${proposal.id}` ||
|
|
701
|
+
actionKey === `submit-${proposal.id}` ||
|
|
702
|
+
actionKey === `approve-${proposal.id}` ||
|
|
703
|
+
actionKey === `reject-${proposal.id}` ? (
|
|
704
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
705
|
+
) : (
|
|
706
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
707
|
+
)}
|
|
708
|
+
</Button>
|
|
709
|
+
</DropdownMenuTrigger>
|
|
710
|
+
<DropdownMenuContent align="end">
|
|
711
|
+
{canEditProposal(proposal.status) ? (
|
|
705
712
|
<DropdownMenuItem
|
|
706
|
-
|
|
707
|
-
onClick={() => void handleStatusAction(proposal, 'approve')}
|
|
713
|
+
onClick={() => handleEdit(proposal)}
|
|
708
714
|
>
|
|
709
|
-
<
|
|
710
|
-
{proposalT('actions.
|
|
715
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
716
|
+
{proposalT('actions.edit')}
|
|
711
717
|
</DropdownMenuItem>
|
|
718
|
+
) : null}
|
|
719
|
+
|
|
720
|
+
{canSubmitProposal(proposal.status) ? (
|
|
712
721
|
<DropdownMenuItem
|
|
713
|
-
disabled={actionKey === `
|
|
714
|
-
|
|
715
|
-
|
|
722
|
+
disabled={actionKey === `submit-${proposal.id}`}
|
|
723
|
+
onClick={() =>
|
|
724
|
+
void handleStatusAction(proposal, 'submit')
|
|
725
|
+
}
|
|
716
726
|
>
|
|
717
|
-
<
|
|
718
|
-
{proposalT('actions.
|
|
727
|
+
<Send className="mr-2 h-4 w-4" />
|
|
728
|
+
{proposalT('actions.submit')}
|
|
719
729
|
</DropdownMenuItem>
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
730
|
+
) : null}
|
|
731
|
+
|
|
732
|
+
{proposal.status === 'pending_approval' &&
|
|
733
|
+
!proposal.current_user_has_approved ? (
|
|
734
|
+
<>
|
|
735
|
+
<DropdownMenuItem
|
|
736
|
+
disabled={
|
|
737
|
+
actionKey === `approve-${proposal.id}`
|
|
738
|
+
}
|
|
739
|
+
onClick={() =>
|
|
740
|
+
void handleStatusAction(proposal, 'approve')
|
|
741
|
+
}
|
|
742
|
+
>
|
|
743
|
+
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
744
|
+
{proposalT('actions.approve')}
|
|
745
|
+
</DropdownMenuItem>
|
|
746
|
+
<DropdownMenuItem
|
|
747
|
+
disabled={
|
|
748
|
+
actionKey === `reject-${proposal.id}`
|
|
749
|
+
}
|
|
750
|
+
className="text-destructive focus:text-destructive"
|
|
751
|
+
onClick={() =>
|
|
752
|
+
void handleStatusAction(proposal, 'reject')
|
|
753
|
+
}
|
|
754
|
+
>
|
|
755
|
+
<XCircle className="mr-2 h-4 w-4" />
|
|
756
|
+
{proposalT('actions.reject')}
|
|
757
|
+
</DropdownMenuItem>
|
|
758
|
+
</>
|
|
759
|
+
) : null}
|
|
760
|
+
|
|
761
|
+
<DropdownMenuSeparator />
|
|
724
762
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
763
|
+
<DropdownMenuItem
|
|
764
|
+
disabled={
|
|
765
|
+
actionKey === `generate-pdf-${proposal.id}`
|
|
766
|
+
}
|
|
767
|
+
onClick={() => void handleGeneratePdf(proposal)}
|
|
768
|
+
>
|
|
769
|
+
<FileText className="mr-2 h-4 w-4" />
|
|
770
|
+
{proposalT('actions.generatePdf')}
|
|
771
|
+
</DropdownMenuItem>
|
|
772
|
+
</DropdownMenuContent>
|
|
773
|
+
</DropdownMenu>
|
|
774
|
+
</div>
|
|
734
775
|
</TableCell>
|
|
735
776
|
</TableRow>
|
|
736
777
|
);
|
|
@@ -755,11 +796,13 @@ export function ProposalsManagementPage({
|
|
|
755
796
|
<div className="flex items-start justify-between gap-3">
|
|
756
797
|
<div className="flex min-w-0 items-start gap-2.5">
|
|
757
798
|
<Avatar className="mt-0.5 h-8 w-8 shrink-0">
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
799
|
+
<AvatarImage
|
|
800
|
+
src={getPersonAvatarUrl(proposal.person?.avatar_id)}
|
|
801
|
+
/>
|
|
802
|
+
<AvatarFallback className="bg-primary/10 text-[11px] font-semibold text-primary">
|
|
803
|
+
{getAvatarInitials(customerName)}
|
|
804
|
+
</AvatarFallback>
|
|
805
|
+
</Avatar>
|
|
763
806
|
<div className="min-w-0 space-y-1">
|
|
764
807
|
<p className="line-clamp-2 text-sm font-semibold text-foreground">
|
|
765
808
|
{proposal.title}
|
|
@@ -824,11 +867,25 @@ export function ProposalsManagementPage({
|
|
|
824
867
|
</div>
|
|
825
868
|
</div>
|
|
826
869
|
|
|
827
|
-
<div className="mt-auto flex justify-end">
|
|
870
|
+
<div className="mt-auto flex justify-end gap-2">
|
|
871
|
+
{canEditProposal(proposal.status) ? (
|
|
872
|
+
<Button
|
|
873
|
+
variant="outline"
|
|
874
|
+
size="sm"
|
|
875
|
+
className="gap-1.5"
|
|
876
|
+
onClick={() => handleEdit(proposal)}
|
|
877
|
+
>
|
|
878
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
879
|
+
{proposalT('actions.edit')}
|
|
880
|
+
</Button>
|
|
881
|
+
) : null}
|
|
828
882
|
<DropdownMenu>
|
|
829
883
|
<DropdownMenuTrigger asChild>
|
|
830
884
|
<Button variant="outline" size="sm" className="gap-1.5">
|
|
831
|
-
{actionKey === `generate-pdf-${proposal.id}` ||
|
|
885
|
+
{actionKey === `generate-pdf-${proposal.id}` ||
|
|
886
|
+
actionKey === `submit-${proposal.id}` ||
|
|
887
|
+
actionKey === `approve-${proposal.id}` ||
|
|
888
|
+
actionKey === `reject-${proposal.id}` ? (
|
|
832
889
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
833
890
|
) : (
|
|
834
891
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
@@ -838,7 +895,9 @@ export function ProposalsManagementPage({
|
|
|
838
895
|
</DropdownMenuTrigger>
|
|
839
896
|
<DropdownMenuContent align="end">
|
|
840
897
|
{canEditProposal(proposal.status) ? (
|
|
841
|
-
<DropdownMenuItem
|
|
898
|
+
<DropdownMenuItem
|
|
899
|
+
onClick={() => handleEdit(proposal)}
|
|
900
|
+
>
|
|
842
901
|
<Pencil className="mr-2 h-4 w-4" />
|
|
843
902
|
{proposalT('actions.edit')}
|
|
844
903
|
</DropdownMenuItem>
|
|
@@ -847,7 +906,9 @@ export function ProposalsManagementPage({
|
|
|
847
906
|
{canSubmitProposal(proposal.status) ? (
|
|
848
907
|
<DropdownMenuItem
|
|
849
908
|
disabled={actionKey === `submit-${proposal.id}`}
|
|
850
|
-
onClick={() =>
|
|
909
|
+
onClick={() =>
|
|
910
|
+
void handleStatusAction(proposal, 'submit')
|
|
911
|
+
}
|
|
851
912
|
>
|
|
852
913
|
<Send className="mr-2 h-4 w-4" />
|
|
853
914
|
{proposalT('actions.submit')}
|
|
@@ -859,7 +920,9 @@ export function ProposalsManagementPage({
|
|
|
859
920
|
<>
|
|
860
921
|
<DropdownMenuItem
|
|
861
922
|
disabled={actionKey === `approve-${proposal.id}`}
|
|
862
|
-
onClick={() =>
|
|
923
|
+
onClick={() =>
|
|
924
|
+
void handleStatusAction(proposal, 'approve')
|
|
925
|
+
}
|
|
863
926
|
>
|
|
864
927
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
|
865
928
|
{proposalT('actions.approve')}
|
|
@@ -867,7 +930,9 @@ export function ProposalsManagementPage({
|
|
|
867
930
|
<DropdownMenuItem
|
|
868
931
|
disabled={actionKey === `reject-${proposal.id}`}
|
|
869
932
|
className="text-destructive focus:text-destructive"
|
|
870
|
-
onClick={() =>
|
|
933
|
+
onClick={() =>
|
|
934
|
+
void handleStatusAction(proposal, 'reject')
|
|
935
|
+
}
|
|
871
936
|
>
|
|
872
937
|
<XCircle className="mr-2 h-4 w-4" />
|
|
873
938
|
{proposalT('actions.reject')}
|