@hed-hog/contact 0.0.303 → 0.0.305
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/README.md +225 -17
- package/dist/person/dto/account.dto.d.ts +5 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -1
- package/dist/person/dto/account.dto.js +29 -0
- package/dist/person/dto/account.dto.js.map +1 -1
- package/dist/person/dto/import-preview.dto.d.ts +7 -0
- package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
- package/dist/person/dto/import-preview.dto.js +7 -0
- package/dist/person/dto/import-preview.dto.js.map +1 -0
- package/dist/person/dto/import.dto.d.ts +15 -0
- package/dist/person/dto/import.dto.d.ts.map +1 -0
- package/dist/person/dto/import.dto.js +51 -0
- package/dist/person/dto/import.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +14 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +53 -0
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +19 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +481 -67
- package/dist/person/person.service.js.map +1 -1
- package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
- package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
- package/hedhog/data/route.yaml +6 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
- package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
- package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
- package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
- package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +696 -82
- package/hedhog/frontend/messages/en.json +140 -2
- package/hedhog/frontend/messages/pt.json +147 -9
- package/package.json +5 -5
- package/src/person/dto/account.dto.ts +31 -0
- package/src/person/dto/import-preview.dto.ts +6 -0
- package/src/person/dto/import.dto.ts +61 -0
- package/src/person/person.controller.ts +74 -12
- package/src/person/person.service.ts +615 -68
|
@@ -51,9 +51,13 @@ import {
|
|
|
51
51
|
SheetTitle,
|
|
52
52
|
} from '@/components/ui/sheet';
|
|
53
53
|
import { Textarea } from '@/components/ui/textarea';
|
|
54
|
+
import { useFormDraft } from '@/hooks/use-form-draft';
|
|
55
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
54
56
|
import { cn } from '@/lib/utils';
|
|
55
57
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
56
58
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
59
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
60
|
+
import { enUS, ptBR } from 'date-fns/locale';
|
|
57
61
|
import {
|
|
58
62
|
CheckCircle2,
|
|
59
63
|
Download,
|
|
@@ -68,7 +72,7 @@ import {
|
|
|
68
72
|
} from 'lucide-react';
|
|
69
73
|
import { useTranslations } from 'next-intl';
|
|
70
74
|
import { useEffect, useMemo, useState } from 'react';
|
|
71
|
-
import { useForm } from 'react-hook-form';
|
|
75
|
+
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
|
72
76
|
import { toast } from 'sonner';
|
|
73
77
|
import { z } from 'zod';
|
|
74
78
|
|
|
@@ -112,6 +116,21 @@ type ProposalBillingModel =
|
|
|
112
116
|
| 'monthly_retainer'
|
|
113
117
|
| 'fixed_price';
|
|
114
118
|
|
|
119
|
+
type ProposalItemType =
|
|
120
|
+
| 'service'
|
|
121
|
+
| 'product'
|
|
122
|
+
| 'fee'
|
|
123
|
+
| 'discount'
|
|
124
|
+
| 'note'
|
|
125
|
+
| 'other';
|
|
126
|
+
|
|
127
|
+
type ProposalRecurrence =
|
|
128
|
+
| 'one_time'
|
|
129
|
+
| 'monthly'
|
|
130
|
+
| 'quarterly'
|
|
131
|
+
| 'yearly'
|
|
132
|
+
| 'other';
|
|
133
|
+
|
|
115
134
|
type ProposalItem = {
|
|
116
135
|
id?: number;
|
|
117
136
|
name: string;
|
|
@@ -119,8 +138,12 @@ type ProposalItem = {
|
|
|
119
138
|
quantity?: number | null;
|
|
120
139
|
unit_amount_cents?: number | null;
|
|
121
140
|
total_amount_cents?: number | null;
|
|
122
|
-
item_type?: string | null;
|
|
123
|
-
|
|
141
|
+
item_type?: ProposalItemType | string | null;
|
|
142
|
+
term_type?: string | null;
|
|
143
|
+
recurrence?: ProposalRecurrence | string | null;
|
|
144
|
+
start_date?: string | null;
|
|
145
|
+
end_date?: string | null;
|
|
146
|
+
due_day?: number | null;
|
|
124
147
|
};
|
|
125
148
|
|
|
126
149
|
type ProposalRevision = {
|
|
@@ -196,10 +219,20 @@ type LeadProposalsTabProps = {
|
|
|
196
219
|
onLeadUpdated: (lead: CrmLead) => Promise<void> | void;
|
|
197
220
|
};
|
|
198
221
|
|
|
222
|
+
const proposalItemFormSchema = z.object({
|
|
223
|
+
name: z.string().trim().min(1),
|
|
224
|
+
description: z.string().optional(),
|
|
225
|
+
itemType: z.enum(['service', 'product', 'fee', 'discount', 'note', 'other']),
|
|
226
|
+
quantity: z.coerce.number().min(0),
|
|
227
|
+
unitAmount: z.coerce.number().min(0),
|
|
228
|
+
recurrence: z.enum(['one_time', 'monthly', 'quarterly', 'yearly', 'other']),
|
|
229
|
+
startDate: z.string().optional(),
|
|
230
|
+
endDate: z.string().optional(),
|
|
231
|
+
});
|
|
232
|
+
|
|
199
233
|
const proposalFormSchema = z.object({
|
|
200
234
|
code: z.string().max(40).optional(),
|
|
201
235
|
title: z.string().trim().min(1),
|
|
202
|
-
totalAmount: z.coerce.number().min(0),
|
|
203
236
|
validUntil: z.string().optional(),
|
|
204
237
|
contractCategory: z.enum([
|
|
205
238
|
'employee',
|
|
@@ -230,9 +263,21 @@ const proposalFormSchema = z.object({
|
|
|
230
263
|
]),
|
|
231
264
|
summary: z.string().optional(),
|
|
232
265
|
notes: z.string().optional(),
|
|
266
|
+
items: z.array(proposalItemFormSchema).min(1),
|
|
233
267
|
});
|
|
234
268
|
|
|
235
269
|
type ProposalFormValues = z.infer<typeof proposalFormSchema>;
|
|
270
|
+
type ProposalFormItemValues = z.infer<typeof proposalItemFormSchema>;
|
|
271
|
+
|
|
272
|
+
type ProposalDraftPayload = {
|
|
273
|
+
leadId: number;
|
|
274
|
+
proposalId: number | null;
|
|
275
|
+
mode: 'create' | 'edit';
|
|
276
|
+
values: ProposalFormValues;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const LEAD_PROPOSAL_DRAFT_STORAGE_KEY =
|
|
280
|
+
'contact-pipeline-lead-proposal-form-draft';
|
|
236
281
|
|
|
237
282
|
const CONTRACT_CATEGORY_OPTIONS: ProposalContractCategory[] = [
|
|
238
283
|
'client',
|
|
@@ -264,6 +309,53 @@ const BILLING_MODEL_OPTIONS: ProposalBillingModel[] = [
|
|
|
264
309
|
'time_and_material',
|
|
265
310
|
];
|
|
266
311
|
|
|
312
|
+
const ITEM_TYPE_OPTIONS: ProposalItemType[] = [
|
|
313
|
+
'service',
|
|
314
|
+
'product',
|
|
315
|
+
'fee',
|
|
316
|
+
'discount',
|
|
317
|
+
'note',
|
|
318
|
+
'other',
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const RECURRENCE_OPTIONS: ProposalRecurrence[] = [
|
|
322
|
+
'one_time',
|
|
323
|
+
'monthly',
|
|
324
|
+
'quarterly',
|
|
325
|
+
'yearly',
|
|
326
|
+
'other',
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
function createEmptyProposalItem(
|
|
330
|
+
overrides: Partial<ProposalFormItemValues> = {}
|
|
331
|
+
): ProposalFormItemValues {
|
|
332
|
+
return {
|
|
333
|
+
name: '',
|
|
334
|
+
description: '',
|
|
335
|
+
itemType: 'service',
|
|
336
|
+
quantity: 1,
|
|
337
|
+
unitAmount: 0,
|
|
338
|
+
recurrence: 'one_time',
|
|
339
|
+
startDate: '',
|
|
340
|
+
endDate: '',
|
|
341
|
+
...overrides,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createDefaultProposalFormValues(): ProposalFormValues {
|
|
346
|
+
return {
|
|
347
|
+
code: '',
|
|
348
|
+
title: '',
|
|
349
|
+
validUntil: '',
|
|
350
|
+
contractCategory: 'client',
|
|
351
|
+
contractType: 'service_agreement',
|
|
352
|
+
billingModel: 'fixed_price',
|
|
353
|
+
summary: '',
|
|
354
|
+
notes: '',
|
|
355
|
+
items: [createEmptyProposalItem()],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
267
359
|
function toDateInputValue(value?: string | null) {
|
|
268
360
|
if (!value) return '';
|
|
269
361
|
const parsed = new Date(value);
|
|
@@ -281,6 +373,38 @@ function formatEnumLabel(value?: string | null) {
|
|
|
281
373
|
.join(' ');
|
|
282
374
|
}
|
|
283
375
|
|
|
376
|
+
function toIsoDateFromInputValue(value?: string | null) {
|
|
377
|
+
if (!value) return null;
|
|
378
|
+
return new Date(`${value}T00:00:00`).toISOString();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mapProposalItemToFormValue(
|
|
382
|
+
item?: ProposalItem | null,
|
|
383
|
+
fallback?: {
|
|
384
|
+
name?: string;
|
|
385
|
+
description?: string | null;
|
|
386
|
+
amountInCents?: number | null;
|
|
387
|
+
}
|
|
388
|
+
): ProposalFormItemValues {
|
|
389
|
+
return createEmptyProposalItem({
|
|
390
|
+
name: item?.name ?? fallback?.name ?? '',
|
|
391
|
+
description: item?.description ?? fallback?.description ?? '',
|
|
392
|
+
itemType: (item?.item_type as ProposalItemType | undefined) ?? 'service',
|
|
393
|
+
quantity: Number(item?.quantity ?? 1),
|
|
394
|
+
unitAmount:
|
|
395
|
+
Number(
|
|
396
|
+
item?.unit_amount_cents ??
|
|
397
|
+
item?.total_amount_cents ??
|
|
398
|
+
fallback?.amountInCents ??
|
|
399
|
+
0
|
|
400
|
+
) / 100,
|
|
401
|
+
recurrence:
|
|
402
|
+
(item?.recurrence as ProposalRecurrence | undefined) ?? 'one_time',
|
|
403
|
+
startDate: toDateInputValue(item?.start_date),
|
|
404
|
+
endDate: toDateInputValue(item?.end_date),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
284
408
|
function openStoredFile(fileId?: number | null) {
|
|
285
409
|
if (!fileId) return;
|
|
286
410
|
const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
|
|
@@ -306,7 +430,7 @@ export function LeadProposalsTab({
|
|
|
306
430
|
onLeadUpdated,
|
|
307
431
|
}: LeadProposalsTabProps) {
|
|
308
432
|
const t = useTranslations('contact.CrmPipeline');
|
|
309
|
-
const { request, currentLocaleCode } = useApp();
|
|
433
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
310
434
|
const locale = currentLocaleCode?.startsWith('pt') ? 'pt-BR' : 'en-US';
|
|
311
435
|
|
|
312
436
|
const [selectedProposalId, setSelectedProposalId] = useState<number | null>(
|
|
@@ -322,19 +446,129 @@ export function LeadProposalsTab({
|
|
|
322
446
|
|
|
323
447
|
const form = useForm<ProposalFormValues>({
|
|
324
448
|
resolver: zodResolver(proposalFormSchema),
|
|
325
|
-
defaultValues:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
449
|
+
defaultValues: createDefaultProposalFormValues(),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const {
|
|
453
|
+
fields: itemFields,
|
|
454
|
+
append: appendItem,
|
|
455
|
+
remove: removeItem,
|
|
456
|
+
} = useFieldArray({
|
|
457
|
+
control: form.control,
|
|
458
|
+
name: 'items',
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const watchedValues = useWatch({
|
|
462
|
+
control: form.control,
|
|
463
|
+
});
|
|
464
|
+
const watchedItems = useMemo(
|
|
465
|
+
() => watchedValues.items ?? [],
|
|
466
|
+
[watchedValues.items]
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const {
|
|
470
|
+
clearDraft,
|
|
471
|
+
loadDraft,
|
|
472
|
+
hasDraft,
|
|
473
|
+
savedAt: draftSavedAt,
|
|
474
|
+
} = useFormDraft<ProposalDraftPayload>({
|
|
475
|
+
storageKey: LEAD_PROPOSAL_DRAFT_STORAGE_KEY,
|
|
476
|
+
value: {
|
|
477
|
+
leadId: lead.id,
|
|
478
|
+
proposalId: editingProposal?.id ?? null,
|
|
479
|
+
mode: editingProposal ? 'edit' : 'create',
|
|
480
|
+
values: {
|
|
481
|
+
code: watchedValues.code ?? '',
|
|
482
|
+
title: watchedValues.title ?? '',
|
|
483
|
+
validUntil: watchedValues.validUntil ?? '',
|
|
484
|
+
contractCategory: watchedValues.contractCategory ?? 'client',
|
|
485
|
+
contractType: watchedValues.contractType ?? 'service_agreement',
|
|
486
|
+
billingModel: watchedValues.billingModel ?? 'fixed_price',
|
|
487
|
+
summary: watchedValues.summary ?? '',
|
|
488
|
+
notes: watchedValues.notes ?? '',
|
|
489
|
+
items: watchedValues.items?.map((item) =>
|
|
490
|
+
createEmptyProposalItem(item)
|
|
491
|
+
) ?? [createEmptyProposalItem()],
|
|
492
|
+
},
|
|
335
493
|
},
|
|
494
|
+
hasData:
|
|
495
|
+
(watchedValues.code ?? '').trim().length > 0 ||
|
|
496
|
+
(watchedValues.title ?? '').trim().length > 0 ||
|
|
497
|
+
(watchedValues.validUntil ?? '').trim().length > 0 ||
|
|
498
|
+
(watchedValues.summary ?? '').trim().length > 0 ||
|
|
499
|
+
(watchedValues.notes ?? '').trim().length > 0 ||
|
|
500
|
+
(watchedValues.contractCategory ?? 'client') !== 'client' ||
|
|
501
|
+
(watchedValues.contractType ?? 'service_agreement') !==
|
|
502
|
+
'service_agreement' ||
|
|
503
|
+
(watchedValues.billingModel ?? 'fixed_price') !== 'fixed_price' ||
|
|
504
|
+
(watchedValues.items ?? []).some(
|
|
505
|
+
(item) =>
|
|
506
|
+
(item.name ?? '').trim().length > 0 ||
|
|
507
|
+
(item.description ?? '').trim().length > 0 ||
|
|
508
|
+
Number(item.quantity ?? 1) !== 1 ||
|
|
509
|
+
Number(item.unitAmount ?? 0) !== 0 ||
|
|
510
|
+
(item.recurrence ?? 'one_time') !== 'one_time' ||
|
|
511
|
+
(item.startDate ?? '').trim().length > 0 ||
|
|
512
|
+
(item.endDate ?? '').trim().length > 0 ||
|
|
513
|
+
(item.itemType ?? 'service') !== 'service'
|
|
514
|
+
),
|
|
515
|
+
enabled: formOpen,
|
|
336
516
|
});
|
|
337
517
|
|
|
518
|
+
const draftStatusContent = useMemo(() => {
|
|
519
|
+
if (!hasDraft || !draftSavedAt) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const savedDate = new Date(draftSavedAt);
|
|
524
|
+
if (Number.isNaN(savedDate.getTime())) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const localeValue = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
|
|
529
|
+
const relativeLabel = formatDistanceToNow(savedDate, {
|
|
530
|
+
addSuffix: true,
|
|
531
|
+
locale: localeValue,
|
|
532
|
+
});
|
|
533
|
+
const absoluteLabel = formatDateTime(
|
|
534
|
+
savedDate,
|
|
535
|
+
getSettingValue,
|
|
536
|
+
currentLocaleCode
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
return currentLocaleCode.startsWith('pt')
|
|
540
|
+
? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
|
|
541
|
+
: `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
|
|
542
|
+
}, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
|
|
543
|
+
|
|
544
|
+
const pricingSummary = useMemo(() => {
|
|
545
|
+
const subtotalCents = (watchedItems ?? []).reduce((sum, item) => {
|
|
546
|
+
if (item?.itemType === 'discount') return sum;
|
|
547
|
+
return (
|
|
548
|
+
sum +
|
|
549
|
+
Math.round(
|
|
550
|
+
Number(item?.quantity ?? 0) * Number(item?.unitAmount ?? 0) * 100
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
}, 0);
|
|
554
|
+
|
|
555
|
+
const discountCents = (watchedItems ?? []).reduce((sum, item) => {
|
|
556
|
+
if (item?.itemType !== 'discount') return sum;
|
|
557
|
+
return (
|
|
558
|
+
sum +
|
|
559
|
+
Math.round(
|
|
560
|
+
Number(item?.quantity ?? 0) * Number(item?.unitAmount ?? 0) * 100
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
}, 0);
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
subtotalCents,
|
|
567
|
+
discountCents,
|
|
568
|
+
totalCents: Math.max(subtotalCents - discountCents, 0),
|
|
569
|
+
};
|
|
570
|
+
}, [watchedItems]);
|
|
571
|
+
|
|
338
572
|
const {
|
|
339
573
|
data: proposalPage = { data: [], total: 0, page: 1, pageSize: 20 },
|
|
340
574
|
isLoading: isLoadingProposals,
|
|
@@ -400,50 +634,81 @@ export function LeadProposalsTab({
|
|
|
400
634
|
|
|
401
635
|
useEffect(() => {
|
|
402
636
|
if (!formOpen) {
|
|
637
|
+
form.reset(createDefaultProposalFormValues());
|
|
638
|
+
setEditingProposal(null);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const storedDraft = loadDraft();
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
!editingProposal &&
|
|
646
|
+
storedDraft?.payload.mode === 'create' &&
|
|
647
|
+
storedDraft.payload.leadId === lead.id
|
|
648
|
+
) {
|
|
403
649
|
form.reset({
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
notes: '',
|
|
650
|
+
...createDefaultProposalFormValues(),
|
|
651
|
+
...storedDraft.payload.values,
|
|
652
|
+
items:
|
|
653
|
+
storedDraft.payload.values.items?.length > 0
|
|
654
|
+
? storedDraft.payload.values.items.map((item) =>
|
|
655
|
+
createEmptyProposalItem(item)
|
|
656
|
+
)
|
|
657
|
+
: [createEmptyProposalItem()],
|
|
413
658
|
});
|
|
414
|
-
setEditingProposal(null);
|
|
415
659
|
return;
|
|
416
660
|
}
|
|
417
661
|
|
|
418
|
-
if (
|
|
662
|
+
if (
|
|
663
|
+
editingProposal &&
|
|
664
|
+
storedDraft?.payload.mode === 'edit' &&
|
|
665
|
+
storedDraft.payload.leadId === lead.id &&
|
|
666
|
+
storedDraft.payload.proposalId === editingProposal.id
|
|
667
|
+
) {
|
|
419
668
|
form.reset({
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
notes: '',
|
|
669
|
+
...createDefaultProposalFormValues(),
|
|
670
|
+
...storedDraft.payload.values,
|
|
671
|
+
items:
|
|
672
|
+
storedDraft.payload.values.items?.length > 0
|
|
673
|
+
? storedDraft.payload.values.items.map((item) =>
|
|
674
|
+
createEmptyProposalItem(item)
|
|
675
|
+
)
|
|
676
|
+
: [createEmptyProposalItem()],
|
|
429
677
|
});
|
|
430
678
|
return;
|
|
431
679
|
}
|
|
432
680
|
|
|
681
|
+
if (!editingProposal) {
|
|
682
|
+
form.reset(createDefaultProposalFormValues());
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
433
686
|
const currentRevision = getCurrentRevision(editingProposal);
|
|
687
|
+
const revisionItems = currentRevision?.proposal_item?.length
|
|
688
|
+
? currentRevision.proposal_item.map((item) =>
|
|
689
|
+
mapProposalItemToFormValue(item)
|
|
690
|
+
)
|
|
691
|
+
: [
|
|
692
|
+
mapProposalItemToFormValue(undefined, {
|
|
693
|
+
name: editingProposal.title,
|
|
694
|
+
description:
|
|
695
|
+
currentRevision?.summary ?? editingProposal.notes ?? '',
|
|
696
|
+
amountInCents: editingProposal.total_amount_cents,
|
|
697
|
+
}),
|
|
698
|
+
];
|
|
434
699
|
|
|
435
700
|
form.reset({
|
|
436
701
|
code: editingProposal.code ?? '',
|
|
437
702
|
title: editingProposal.title,
|
|
438
|
-
totalAmount: Number(editingProposal.total_amount_cents ?? 0) / 100,
|
|
439
703
|
validUntil: toDateInputValue(editingProposal.valid_until),
|
|
440
704
|
contractCategory: editingProposal.contract_category ?? 'client',
|
|
441
705
|
contractType: editingProposal.contract_type ?? 'service_agreement',
|
|
442
706
|
billingModel: editingProposal.billing_model ?? 'fixed_price',
|
|
443
707
|
summary: currentRevision?.summary ?? '',
|
|
444
708
|
notes: editingProposal.notes ?? '',
|
|
709
|
+
items: revisionItems,
|
|
445
710
|
});
|
|
446
|
-
}, [editingProposal, form, formOpen]);
|
|
711
|
+
}, [editingProposal, form, formOpen, lead.id, loadDraft]);
|
|
447
712
|
|
|
448
713
|
const selectedProposal = useMemo(() => {
|
|
449
714
|
const matchingDetail =
|
|
@@ -513,7 +778,12 @@ export function LeadProposalsTab({
|
|
|
513
778
|
};
|
|
514
779
|
|
|
515
780
|
const getProposalEnumLabel = (
|
|
516
|
-
group:
|
|
781
|
+
group:
|
|
782
|
+
| 'contractCategory'
|
|
783
|
+
| 'contractType'
|
|
784
|
+
| 'billingModel'
|
|
785
|
+
| 'itemType'
|
|
786
|
+
| 'recurrence',
|
|
517
787
|
value?: string | null
|
|
518
788
|
) => {
|
|
519
789
|
if (!value) return '—';
|
|
@@ -584,7 +854,43 @@ export function LeadProposalsTab({
|
|
|
584
854
|
};
|
|
585
855
|
|
|
586
856
|
const handleSave = async (values: ProposalFormValues) => {
|
|
587
|
-
const
|
|
857
|
+
const normalizedItems = values.items
|
|
858
|
+
.map((item) => {
|
|
859
|
+
const quantity = Number(item.quantity ?? 0);
|
|
860
|
+
const unitAmount = Number(item.unitAmount ?? 0);
|
|
861
|
+
const unitAmountCents = Math.round(unitAmount * 100);
|
|
862
|
+
const totalAmountCents = Math.round(quantity * unitAmount * 100);
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
name: item.name.trim(),
|
|
866
|
+
description: item.description?.trim() || null,
|
|
867
|
+
quantity,
|
|
868
|
+
unit_amount_cents: unitAmountCents,
|
|
869
|
+
total_amount_cents: totalAmountCents,
|
|
870
|
+
item_type: item.itemType,
|
|
871
|
+
term_type: 'value',
|
|
872
|
+
recurrence: item.recurrence,
|
|
873
|
+
start_date: toIsoDateFromInputValue(item.startDate),
|
|
874
|
+
end_date: toIsoDateFromInputValue(item.endDate),
|
|
875
|
+
};
|
|
876
|
+
})
|
|
877
|
+
.filter((item) => item.name.length > 0);
|
|
878
|
+
|
|
879
|
+
const subtotalAmountCents = normalizedItems.reduce((sum, item) => {
|
|
880
|
+
if (item.item_type === 'discount') return sum;
|
|
881
|
+
return sum + Number(item.total_amount_cents ?? 0);
|
|
882
|
+
}, 0);
|
|
883
|
+
|
|
884
|
+
const discountAmountCents = normalizedItems.reduce((sum, item) => {
|
|
885
|
+
if (item.item_type !== 'discount') return sum;
|
|
886
|
+
return sum + Number(item.total_amount_cents ?? 0);
|
|
887
|
+
}, 0);
|
|
888
|
+
|
|
889
|
+
const totalAmountCents = Math.max(
|
|
890
|
+
subtotalAmountCents - discountAmountCents,
|
|
891
|
+
0
|
|
892
|
+
);
|
|
893
|
+
|
|
588
894
|
const payload = {
|
|
589
895
|
person_id: lead.id,
|
|
590
896
|
code: values.code?.trim() || undefined,
|
|
@@ -593,29 +899,13 @@ export function LeadProposalsTab({
|
|
|
593
899
|
contract_type: values.contractType,
|
|
594
900
|
billing_model: values.billingModel,
|
|
595
901
|
currency_code: 'BRL',
|
|
596
|
-
valid_until: values.validUntil
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
total_amount_cents: amountInCents,
|
|
902
|
+
valid_until: toIsoDateFromInputValue(values.validUntil),
|
|
903
|
+
subtotal_amount_cents: subtotalAmountCents,
|
|
904
|
+
discount_amount_cents: discountAmountCents,
|
|
905
|
+
total_amount_cents: totalAmountCents,
|
|
601
906
|
summary: values.summary?.trim() || null,
|
|
602
907
|
notes: values.notes?.trim() || null,
|
|
603
|
-
items:
|
|
604
|
-
amountInCents > 0
|
|
605
|
-
? [
|
|
606
|
-
{
|
|
607
|
-
name: values.title.trim(),
|
|
608
|
-
description:
|
|
609
|
-
values.summary?.trim() || values.notes?.trim() || undefined,
|
|
610
|
-
quantity: 1,
|
|
611
|
-
unit_amount_cents: amountInCents,
|
|
612
|
-
total_amount_cents: amountInCents,
|
|
613
|
-
item_type: 'service',
|
|
614
|
-
term_type: 'value',
|
|
615
|
-
recurrence: 'one_time',
|
|
616
|
-
},
|
|
617
|
-
]
|
|
618
|
-
: [],
|
|
908
|
+
items: normalizedItems,
|
|
619
909
|
};
|
|
620
910
|
|
|
621
911
|
try {
|
|
@@ -647,6 +937,7 @@ export function LeadProposalsTab({
|
|
|
647
937
|
await refreshAll(createdProposal.id);
|
|
648
938
|
}
|
|
649
939
|
|
|
940
|
+
clearDraft();
|
|
650
941
|
setFormOpen(false);
|
|
651
942
|
} catch (error) {
|
|
652
943
|
const message =
|
|
@@ -1250,8 +1541,33 @@ export function LeadProposalsTab({
|
|
|
1250
1541
|
{item.description}
|
|
1251
1542
|
</p>
|
|
1252
1543
|
) : null}
|
|
1544
|
+
|
|
1545
|
+
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
1546
|
+
{item.item_type ? (
|
|
1547
|
+
<Badge variant="secondary">
|
|
1548
|
+
{getProposalEnumLabel(
|
|
1549
|
+
'itemType',
|
|
1550
|
+
item.item_type
|
|
1551
|
+
)}
|
|
1552
|
+
</Badge>
|
|
1553
|
+
) : null}
|
|
1554
|
+
{Number(item.quantity ?? 0) > 0 ? (
|
|
1555
|
+
<span>
|
|
1556
|
+
{`${item.quantity} × ${formatCurrency(item.unit_amount_cents, selectedProposal.currency_code)}`}
|
|
1557
|
+
</span>
|
|
1558
|
+
) : null}
|
|
1559
|
+
{item.recurrence ? (
|
|
1560
|
+
<Badge variant="outline">
|
|
1561
|
+
{getProposalEnumLabel(
|
|
1562
|
+
'recurrence',
|
|
1563
|
+
item.recurrence
|
|
1564
|
+
)}
|
|
1565
|
+
</Badge>
|
|
1566
|
+
) : null}
|
|
1567
|
+
</div>
|
|
1253
1568
|
</div>
|
|
1254
1569
|
<span className="text-sm font-semibold text-foreground">
|
|
1570
|
+
{item.item_type === 'discount' ? '- ' : ''}
|
|
1255
1571
|
{formatCurrency(
|
|
1256
1572
|
item.total_amount_cents,
|
|
1257
1573
|
selectedProposal.currency_code
|
|
@@ -1413,28 +1729,321 @@ export function LeadProposalsTab({
|
|
|
1413
1729
|
)}
|
|
1414
1730
|
/>
|
|
1415
1731
|
|
|
1416
|
-
<
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
<
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
<
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1732
|
+
<div className="space-y-3 lg:col-span-2">
|
|
1733
|
+
<div className="flex flex-col gap-3 rounded-xl border border-border/60 bg-muted/10 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
1734
|
+
<div className="space-y-1">
|
|
1735
|
+
<p className="text-sm font-medium text-foreground">
|
|
1736
|
+
{t('proposals.form.items')}
|
|
1737
|
+
</p>
|
|
1738
|
+
<p className="text-xs text-muted-foreground">
|
|
1739
|
+
{t('proposals.form.itemsDescription')}
|
|
1740
|
+
</p>
|
|
1741
|
+
</div>
|
|
1742
|
+
|
|
1743
|
+
<Button
|
|
1744
|
+
type="button"
|
|
1745
|
+
variant="outline"
|
|
1746
|
+
size="sm"
|
|
1747
|
+
className="gap-2 self-start"
|
|
1748
|
+
onClick={() => appendItem(createEmptyProposalItem())}
|
|
1749
|
+
>
|
|
1750
|
+
<Plus className="size-4" />
|
|
1751
|
+
{t('proposals.form.addItem')}
|
|
1752
|
+
</Button>
|
|
1753
|
+
</div>
|
|
1754
|
+
|
|
1755
|
+
<div className="space-y-3">
|
|
1756
|
+
{itemFields.map((itemField, index) => {
|
|
1757
|
+
const watchedItem = watchedItems?.[index];
|
|
1758
|
+
const lineTotalCents = Math.round(
|
|
1759
|
+
Number(watchedItem?.quantity ?? 0) *
|
|
1760
|
+
Number(watchedItem?.unitAmount ?? 0) *
|
|
1761
|
+
100
|
|
1762
|
+
);
|
|
1763
|
+
const isDiscountItem =
|
|
1764
|
+
watchedItem?.itemType === 'discount';
|
|
1765
|
+
|
|
1766
|
+
return (
|
|
1767
|
+
<div
|
|
1768
|
+
key={itemField.id}
|
|
1769
|
+
className="rounded-xl border border-border/60 bg-background p-3 shadow-xs"
|
|
1770
|
+
>
|
|
1771
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
1772
|
+
<div>
|
|
1773
|
+
<p className="text-sm font-semibold text-foreground">
|
|
1774
|
+
{t('proposals.form.itemLabel', {
|
|
1775
|
+
number: index + 1,
|
|
1776
|
+
})}
|
|
1777
|
+
</p>
|
|
1778
|
+
<p className="text-xs text-muted-foreground">
|
|
1779
|
+
{isDiscountItem
|
|
1780
|
+
? t('proposals.form.discountHint')
|
|
1781
|
+
: t('proposals.form.itemHint')}
|
|
1782
|
+
</p>
|
|
1783
|
+
</div>
|
|
1784
|
+
|
|
1785
|
+
<Button
|
|
1786
|
+
type="button"
|
|
1787
|
+
variant="ghost"
|
|
1788
|
+
size="sm"
|
|
1789
|
+
className="gap-2 text-muted-foreground"
|
|
1790
|
+
onClick={() => removeItem(index)}
|
|
1791
|
+
disabled={itemFields.length === 1}
|
|
1792
|
+
>
|
|
1793
|
+
<Trash2 className="size-4" />
|
|
1794
|
+
{t('proposals.form.removeItem')}
|
|
1795
|
+
</Button>
|
|
1796
|
+
</div>
|
|
1797
|
+
|
|
1798
|
+
<div className="mt-3 grid grid-cols-1 gap-3 xl:grid-cols-6">
|
|
1799
|
+
<FormField
|
|
1800
|
+
control={form.control}
|
|
1801
|
+
name={`items.${index}.name` as const}
|
|
1802
|
+
render={({ field }) => (
|
|
1803
|
+
<FormItem className="xl:col-span-2">
|
|
1804
|
+
<FormLabel>
|
|
1805
|
+
{t('proposals.form.itemName')}
|
|
1806
|
+
</FormLabel>
|
|
1807
|
+
<FormControl>
|
|
1808
|
+
<Input
|
|
1809
|
+
{...field}
|
|
1810
|
+
value={field.value ?? ''}
|
|
1811
|
+
placeholder={t(
|
|
1812
|
+
'proposals.form.itemNamePlaceholder'
|
|
1813
|
+
)}
|
|
1814
|
+
/>
|
|
1815
|
+
</FormControl>
|
|
1816
|
+
<FormMessage />
|
|
1817
|
+
</FormItem>
|
|
1818
|
+
)}
|
|
1819
|
+
/>
|
|
1820
|
+
|
|
1821
|
+
<FormField
|
|
1822
|
+
control={form.control}
|
|
1823
|
+
name={`items.${index}.itemType` as const}
|
|
1824
|
+
render={({ field }) => (
|
|
1825
|
+
<FormItem>
|
|
1826
|
+
<FormLabel>
|
|
1827
|
+
{t('proposals.form.itemType')}
|
|
1828
|
+
</FormLabel>
|
|
1829
|
+
<Select
|
|
1830
|
+
onValueChange={field.onChange}
|
|
1831
|
+
value={field.value}
|
|
1832
|
+
>
|
|
1833
|
+
<FormControl>
|
|
1834
|
+
<SelectTrigger className="w-full">
|
|
1835
|
+
<SelectValue />
|
|
1836
|
+
</SelectTrigger>
|
|
1837
|
+
</FormControl>
|
|
1838
|
+
<SelectContent>
|
|
1839
|
+
{ITEM_TYPE_OPTIONS.map((option) => (
|
|
1840
|
+
<SelectItem
|
|
1841
|
+
key={option}
|
|
1842
|
+
value={option}
|
|
1843
|
+
>
|
|
1844
|
+
{getProposalEnumLabel(
|
|
1845
|
+
'itemType',
|
|
1846
|
+
option
|
|
1847
|
+
)}
|
|
1848
|
+
</SelectItem>
|
|
1849
|
+
))}
|
|
1850
|
+
</SelectContent>
|
|
1851
|
+
</Select>
|
|
1852
|
+
<FormMessage />
|
|
1853
|
+
</FormItem>
|
|
1854
|
+
)}
|
|
1855
|
+
/>
|
|
1856
|
+
|
|
1857
|
+
<FormField
|
|
1858
|
+
control={form.control}
|
|
1859
|
+
name={`items.${index}.recurrence` as const}
|
|
1860
|
+
render={({ field }) => (
|
|
1861
|
+
<FormItem>
|
|
1862
|
+
<FormLabel>
|
|
1863
|
+
{t('proposals.form.recurrence')}
|
|
1864
|
+
</FormLabel>
|
|
1865
|
+
<Select
|
|
1866
|
+
onValueChange={field.onChange}
|
|
1867
|
+
value={field.value}
|
|
1868
|
+
>
|
|
1869
|
+
<FormControl>
|
|
1870
|
+
<SelectTrigger className="w-full">
|
|
1871
|
+
<SelectValue />
|
|
1872
|
+
</SelectTrigger>
|
|
1873
|
+
</FormControl>
|
|
1874
|
+
<SelectContent>
|
|
1875
|
+
{RECURRENCE_OPTIONS.map((option) => (
|
|
1876
|
+
<SelectItem
|
|
1877
|
+
key={option}
|
|
1878
|
+
value={option}
|
|
1879
|
+
>
|
|
1880
|
+
{getProposalEnumLabel(
|
|
1881
|
+
'recurrence',
|
|
1882
|
+
option
|
|
1883
|
+
)}
|
|
1884
|
+
</SelectItem>
|
|
1885
|
+
))}
|
|
1886
|
+
</SelectContent>
|
|
1887
|
+
</Select>
|
|
1888
|
+
<FormMessage />
|
|
1889
|
+
</FormItem>
|
|
1890
|
+
)}
|
|
1891
|
+
/>
|
|
1892
|
+
|
|
1893
|
+
<FormField
|
|
1894
|
+
control={form.control}
|
|
1895
|
+
name={`items.${index}.quantity` as const}
|
|
1896
|
+
render={({ field }) => (
|
|
1897
|
+
<FormItem>
|
|
1898
|
+
<FormLabel>
|
|
1899
|
+
{t('proposals.form.quantity')}
|
|
1900
|
+
</FormLabel>
|
|
1901
|
+
<FormControl>
|
|
1902
|
+
<Input
|
|
1903
|
+
{...field}
|
|
1904
|
+
type="number"
|
|
1905
|
+
min="0"
|
|
1906
|
+
step="0.01"
|
|
1907
|
+
value={field.value ?? 0}
|
|
1908
|
+
onChange={(event) =>
|
|
1909
|
+
field.onChange(event.target.value)
|
|
1910
|
+
}
|
|
1911
|
+
/>
|
|
1912
|
+
</FormControl>
|
|
1913
|
+
<FormMessage />
|
|
1914
|
+
</FormItem>
|
|
1915
|
+
)}
|
|
1916
|
+
/>
|
|
1917
|
+
|
|
1918
|
+
<FormField
|
|
1919
|
+
control={form.control}
|
|
1920
|
+
name={`items.${index}.unitAmount` as const}
|
|
1921
|
+
render={({ field }) => (
|
|
1922
|
+
<FormItem>
|
|
1923
|
+
<FormLabel>
|
|
1924
|
+
{t('proposals.form.unitAmount')}
|
|
1925
|
+
</FormLabel>
|
|
1926
|
+
<FormControl>
|
|
1927
|
+
<InputMoney
|
|
1928
|
+
ref={field.ref}
|
|
1929
|
+
name={field.name}
|
|
1930
|
+
value={field.value ?? 0}
|
|
1931
|
+
onBlur={field.onBlur}
|
|
1932
|
+
onValueChange={(value) =>
|
|
1933
|
+
field.onChange(value ?? 0)
|
|
1934
|
+
}
|
|
1935
|
+
placeholder="0,00"
|
|
1936
|
+
/>
|
|
1937
|
+
</FormControl>
|
|
1938
|
+
<FormMessage />
|
|
1939
|
+
</FormItem>
|
|
1940
|
+
)}
|
|
1941
|
+
/>
|
|
1942
|
+
|
|
1943
|
+
<FormField
|
|
1944
|
+
control={form.control}
|
|
1945
|
+
name={`items.${index}.description` as const}
|
|
1946
|
+
render={({ field }) => (
|
|
1947
|
+
<FormItem className="xl:col-span-4">
|
|
1948
|
+
<FormLabel>
|
|
1949
|
+
{t('proposals.form.itemDescription')}
|
|
1950
|
+
</FormLabel>
|
|
1951
|
+
<FormControl>
|
|
1952
|
+
<Textarea
|
|
1953
|
+
{...field}
|
|
1954
|
+
value={field.value ?? ''}
|
|
1955
|
+
rows={3}
|
|
1956
|
+
placeholder={t(
|
|
1957
|
+
'proposals.form.itemDescriptionPlaceholder'
|
|
1958
|
+
)}
|
|
1959
|
+
/>
|
|
1960
|
+
</FormControl>
|
|
1961
|
+
<FormMessage />
|
|
1962
|
+
</FormItem>
|
|
1963
|
+
)}
|
|
1964
|
+
/>
|
|
1965
|
+
|
|
1966
|
+
<FormField
|
|
1967
|
+
control={form.control}
|
|
1968
|
+
name={`items.${index}.startDate` as const}
|
|
1969
|
+
render={({ field }) => (
|
|
1970
|
+
<FormItem>
|
|
1971
|
+
<FormLabel>
|
|
1972
|
+
{t('proposals.form.startDate')}
|
|
1973
|
+
</FormLabel>
|
|
1974
|
+
<FormControl>
|
|
1975
|
+
<Input
|
|
1976
|
+
type="date"
|
|
1977
|
+
{...field}
|
|
1978
|
+
value={field.value ?? ''}
|
|
1979
|
+
/>
|
|
1980
|
+
</FormControl>
|
|
1981
|
+
<FormMessage />
|
|
1982
|
+
</FormItem>
|
|
1983
|
+
)}
|
|
1984
|
+
/>
|
|
1985
|
+
|
|
1986
|
+
<FormField
|
|
1987
|
+
control={form.control}
|
|
1988
|
+
name={`items.${index}.endDate` as const}
|
|
1989
|
+
render={({ field }) => (
|
|
1990
|
+
<FormItem>
|
|
1991
|
+
<FormLabel>
|
|
1992
|
+
{t('proposals.form.endDate')}
|
|
1993
|
+
</FormLabel>
|
|
1994
|
+
<FormControl>
|
|
1995
|
+
<Input
|
|
1996
|
+
type="date"
|
|
1997
|
+
{...field}
|
|
1998
|
+
value={field.value ?? ''}
|
|
1999
|
+
/>
|
|
2000
|
+
</FormControl>
|
|
2001
|
+
<FormMessage />
|
|
2002
|
+
</FormItem>
|
|
2003
|
+
)}
|
|
2004
|
+
/>
|
|
2005
|
+
</div>
|
|
2006
|
+
|
|
2007
|
+
<div className="mt-3 flex justify-end">
|
|
2008
|
+
<div className="rounded-lg bg-muted/40 px-3 py-2 text-sm font-medium text-foreground">
|
|
2009
|
+
{t('proposals.form.lineTotal')}:{' '}
|
|
2010
|
+
{isDiscountItem ? '- ' : ''}
|
|
2011
|
+
{formatCurrency(lineTotalCents, 'BRL')}
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
</div>
|
|
2015
|
+
);
|
|
2016
|
+
})}
|
|
2017
|
+
</div>
|
|
2018
|
+
|
|
2019
|
+
<div className="grid gap-2 rounded-xl border border-border/60 bg-muted/15 p-4 sm:grid-cols-3">
|
|
2020
|
+
<div>
|
|
2021
|
+
<p className="text-xs text-muted-foreground">
|
|
2022
|
+
{t('proposals.form.subtotal')}
|
|
2023
|
+
</p>
|
|
2024
|
+
<p className="text-base font-semibold text-foreground">
|
|
2025
|
+
{formatCurrency(pricingSummary.subtotalCents, 'BRL')}
|
|
2026
|
+
</p>
|
|
2027
|
+
</div>
|
|
2028
|
+
<div>
|
|
2029
|
+
<p className="text-xs text-muted-foreground">
|
|
2030
|
+
{t('proposals.form.discount')}
|
|
2031
|
+
</p>
|
|
2032
|
+
<p className="text-base font-semibold text-foreground">
|
|
2033
|
+
-{' '}
|
|
2034
|
+
{formatCurrency(pricingSummary.discountCents, 'BRL')}
|
|
2035
|
+
</p>
|
|
2036
|
+
</div>
|
|
2037
|
+
<div>
|
|
2038
|
+
<p className="text-xs text-muted-foreground">
|
|
2039
|
+
{t('proposals.form.calculatedTotal')}
|
|
2040
|
+
</p>
|
|
2041
|
+
<p className="text-base font-semibold text-foreground">
|
|
2042
|
+
{formatCurrency(pricingSummary.totalCents, 'BRL')}
|
|
2043
|
+
</p>
|
|
2044
|
+
</div>
|
|
2045
|
+
</div>
|
|
2046
|
+
</div>
|
|
1438
2047
|
|
|
1439
2048
|
<FormField
|
|
1440
2049
|
control={form.control}
|
|
@@ -1588,6 +2197,11 @@ export function LeadProposalsTab({
|
|
|
1588
2197
|
</div>
|
|
1589
2198
|
|
|
1590
2199
|
<div className="shrink-0 border-t px-5 py-3">
|
|
2200
|
+
{draftStatusContent ? (
|
|
2201
|
+
<p className="mb-2 text-xs text-muted-foreground">
|
|
2202
|
+
{draftStatusContent}
|
|
2203
|
+
</p>
|
|
2204
|
+
) : null}
|
|
1591
2205
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
1592
2206
|
<Button
|
|
1593
2207
|
type="button"
|