@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.
@@ -2,216 +2,59 @@
2
2
 
3
3
  import { EmptyState } from '@/components/entity-list';
4
4
  import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
13
  } from '@/components/ui/alert-dialog';
14
14
  import { Badge } from '@/components/ui/badge';
15
15
  import { Button } from '@/components/ui/button';
16
16
  import {
17
- Card,
18
- CardContent,
19
- CardDescription,
20
- CardHeader,
21
- CardTitle,
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
22
  } from '@/components/ui/card';
23
23
  import {
24
- DropdownMenu,
25
- DropdownMenuContent,
26
- DropdownMenuItem,
27
- DropdownMenuSeparator,
28
- DropdownMenuTrigger,
24
+ DropdownMenu,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuSeparator,
28
+ DropdownMenuTrigger,
29
29
  } from '@/components/ui/dropdown-menu';
30
- import {
31
- Form,
32
- FormControl,
33
- FormField,
34
- FormItem,
35
- FormLabel,
36
- FormMessage,
37
- } from '@/components/ui/form';
38
- import { Input } from '@/components/ui/input';
39
- import {
40
- Select,
41
- SelectContent,
42
- SelectItem,
43
- SelectTrigger,
44
- SelectValue,
45
- } from '@/components/ui/select';
46
- import {
47
- Sheet,
48
- SheetContent,
49
- SheetDescription,
50
- SheetHeader,
51
- SheetTitle,
52
- } from '@/components/ui/sheet';
53
- import { Textarea } from '@/components/ui/textarea';
54
- import { useFormDraft } from '@/hooks/use-form-draft';
55
- import { formatDateTime } from '@/lib/format-date';
56
30
  import { cn } from '@/lib/utils';
57
31
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
58
- import { zodResolver } from '@hookform/resolvers/zod';
59
- import { formatDistanceToNow } from 'date-fns';
60
- import { enUS, ptBR } from 'date-fns/locale';
61
32
  import {
62
- CheckCircle2,
63
- Download,
64
- FileText,
65
- Loader2,
66
- MoreHorizontal,
67
- Pencil,
68
- Plus,
69
- Send,
70
- Trash2,
71
- XCircle,
33
+ CheckCircle2,
34
+ FileText,
35
+ Loader2,
36
+ MoreHorizontal,
37
+ Pencil,
38
+ Plus,
39
+ Send,
40
+ Trash2,
41
+ XCircle,
72
42
  } from 'lucide-react';
73
43
  import { useTranslations } from 'next-intl';
74
44
  import { useEffect, useMemo, useState } from 'react';
75
- import { useFieldArray, useForm, useWatch } from 'react-hook-form';
76
45
  import { toast } from 'sonner';
77
- import { z } from 'zod';
78
46
 
79
- import { InputMoney } from '@/components/ui/input-money';
80
47
  import type { CrmLead } from '../../_lib/crm-mocks';
81
48
  import type { PaginatedResult } from '../../person/_components/person-types';
82
-
83
- type ProposalStatus =
84
- | 'draft'
85
- | 'pending_approval'
86
- | 'approved'
87
- | 'rejected'
88
- | 'cancelled'
89
- | 'expired'
90
- | 'contract_generated';
91
-
92
- type ProposalContractCategory =
93
- | 'employee'
94
- | 'contractor'
95
- | 'client'
96
- | 'supplier'
97
- | 'vendor'
98
- | 'partner'
99
- | 'internal'
100
- | 'other';
101
-
102
- type ProposalContractType =
103
- | 'clt'
104
- | 'pj'
105
- | 'freelancer_agreement'
106
- | 'service_agreement'
107
- | 'fixed_term'
108
- | 'recurring_service'
109
- | 'nda'
110
- | 'amendment'
111
- | 'addendum'
112
- | 'other';
113
-
114
- type ProposalBillingModel =
115
- | 'time_and_material'
116
- | 'monthly_retainer'
117
- | 'fixed_price';
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
-
134
- type ProposalItem = {
135
- id?: number;
136
- name: string;
137
- description?: string | null;
138
- quantity?: number | null;
139
- unit_amount_cents?: number | null;
140
- total_amount_cents?: number | null;
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;
147
- };
148
-
149
- type ProposalRevision = {
150
- id: number;
151
- revision_number: number;
152
- is_current?: boolean;
153
- title: string;
154
- summary?: string | null;
155
- valid_until?: string | null;
156
- status?: ProposalStatus | string;
157
- proposal_item?: ProposalItem[];
158
- };
159
-
160
- type ProposalApproval = {
161
- id: number;
162
- status?: string | null;
163
- decision_note?: string | null;
164
- submitted_at?: string | null;
165
- decided_at?: string | null;
166
- };
167
-
168
- type ProposalDocument = {
169
- id: number;
170
- file_id?: number | null;
171
- file_name?: string | null;
172
- mime_type?: string | null;
173
- document_type?: string | null;
174
- is_current?: boolean | null;
175
- created_at?: string | null;
176
- };
177
-
178
- type GenerateProposalDocumentResponse = {
179
- fileId?: number | null;
180
- fileName?: string | null;
181
- downloadUrl?: string | null;
182
- };
183
-
184
- type ProposalIntegrationLink = {
185
- targetModule?: string;
186
- targetEntityType?: string;
187
- targetEntityId?: string;
188
- };
189
-
190
- type ProposalRecord = {
191
- id: number;
192
- person_id: number;
193
- code?: string | null;
194
- title: string;
195
- status: ProposalStatus;
196
- contract_category?: ProposalContractCategory | null;
197
- contract_type?: ProposalContractType | null;
198
- billing_model?: ProposalBillingModel | null;
199
- currency_code?: string | null;
200
- subtotal_amount_cents?: number | null;
201
- discount_amount_cents?: number | null;
202
- tax_amount_cents?: number | null;
203
- total_amount_cents?: number | null;
204
- valid_until?: string | null;
205
- current_revision_number?: number | null;
206
- notes?: string | null;
207
- created_at: string;
208
- updated_at?: string | null;
209
- approved_at?: string | null;
210
- proposal_revision?: ProposalRevision[];
211
- proposal_approval?: ProposalApproval[];
212
- proposal_document?: ProposalDocument[];
213
- integration_links?: ProposalIntegrationLink[];
214
- };
49
+ import { ProposalFormSheet } from '../../proposals/_components/proposal-form-sheet';
50
+ import {
51
+ formatEnumLabel,
52
+ getCurrentRevision,
53
+ openStoredFile,
54
+ type GenerateProposalDocumentResponse,
55
+ type ProposalRecord,
56
+ type ProposalStatus,
57
+ } from '../../proposals/_components/proposal-types';
215
58
 
216
59
  type LeadProposalsTabProps = {
217
60
  lead: CrmLead;
@@ -219,218 +62,13 @@ type LeadProposalsTabProps = {
219
62
  onLeadUpdated: (lead: CrmLead) => Promise<void> | void;
220
63
  };
221
64
 
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
-
233
- const proposalFormSchema = z.object({
234
- code: z.string().max(40).optional(),
235
- title: z.string().trim().min(1),
236
- validUntil: z.string().optional(),
237
- contractCategory: z.enum([
238
- 'employee',
239
- 'contractor',
240
- 'client',
241
- 'supplier',
242
- 'vendor',
243
- 'partner',
244
- 'internal',
245
- 'other',
246
- ]),
247
- contractType: z.enum([
248
- 'clt',
249
- 'pj',
250
- 'freelancer_agreement',
251
- 'service_agreement',
252
- 'fixed_term',
253
- 'recurring_service',
254
- 'nda',
255
- 'amendment',
256
- 'addendum',
257
- 'other',
258
- ]),
259
- billingModel: z.enum([
260
- 'time_and_material',
261
- 'monthly_retainer',
262
- 'fixed_price',
263
- ]),
264
- summary: z.string().optional(),
265
- notes: z.string().optional(),
266
- items: z.array(proposalItemFormSchema).min(1),
267
- });
268
-
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';
281
-
282
- const CONTRACT_CATEGORY_OPTIONS: ProposalContractCategory[] = [
283
- 'client',
284
- 'supplier',
285
- 'vendor',
286
- 'partner',
287
- 'employee',
288
- 'contractor',
289
- 'internal',
290
- 'other',
291
- ];
292
-
293
- const CONTRACT_TYPE_OPTIONS: ProposalContractType[] = [
294
- 'service_agreement',
295
- 'recurring_service',
296
- 'fixed_term',
297
- 'freelancer_agreement',
298
- 'pj',
299
- 'clt',
300
- 'nda',
301
- 'amendment',
302
- 'addendum',
303
- 'other',
304
- ];
305
-
306
- const BILLING_MODEL_OPTIONS: ProposalBillingModel[] = [
307
- 'fixed_price',
308
- 'monthly_retainer',
309
- 'time_and_material',
310
- ];
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
-
359
- function toDateInputValue(value?: string | null) {
360
- if (!value) return '';
361
- const parsed = new Date(value);
362
- if (Number.isNaN(parsed.getTime())) return '';
363
- return parsed.toISOString().slice(0, 10);
364
- }
365
-
366
- function formatEnumLabel(value?: string | null) {
367
- if (!value) return '—';
368
-
369
- return value
370
- .split('_')
371
- .filter(Boolean)
372
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
373
- .join(' ');
374
- }
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
-
408
- function openStoredFile(fileId?: number | null) {
409
- if (!fileId) return;
410
- const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
411
- window.open(
412
- `${baseUrl}/file/open/${fileId}`,
413
- '_blank',
414
- 'noopener,noreferrer'
415
- );
416
- }
417
-
418
- function getCurrentRevision(proposal?: ProposalRecord | null) {
419
- if (!proposal?.proposal_revision?.length) return null;
420
-
421
- return (
422
- proposal.proposal_revision.find((revision) => revision.is_current) ??
423
- proposal.proposal_revision[0]
424
- );
425
- }
426
-
427
65
  export function LeadProposalsTab({
428
66
  lead,
429
67
  active,
430
68
  onLeadUpdated,
431
69
  }: LeadProposalsTabProps) {
432
70
  const t = useTranslations('contact.CrmPipeline');
433
- const { request, currentLocaleCode, getSettingValue } = useApp();
71
+ const { request, currentLocaleCode } = useApp();
434
72
  const locale = currentLocaleCode?.startsWith('pt') ? 'pt-BR' : 'en-US';
435
73
 
436
74
  const [selectedProposalId, setSelectedProposalId] = useState<number | null>(
@@ -441,134 +79,8 @@ export function LeadProposalsTab({
441
79
  null
442
80
  );
443
81
  const [deleteTarget, setDeleteTarget] = useState<ProposalRecord | null>(null);
444
- const [isSaving, setIsSaving] = useState(false);
445
82
  const [actionKey, setActionKey] = useState<string | null>(null);
446
83
 
447
- const form = useForm<ProposalFormValues>({
448
- resolver: zodResolver(proposalFormSchema),
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
- },
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,
516
- });
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
-
572
84
  const {
573
85
  data: proposalPage = { data: [], total: 0, page: 1, pageSize: 20 },
574
86
  isLoading: isLoadingProposals,
@@ -632,84 +144,6 @@ export function LeadProposalsTab({
632
144
  });
633
145
  }, [proposals]);
634
146
 
635
- useEffect(() => {
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
- ) {
649
- form.reset({
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()],
658
- });
659
- return;
660
- }
661
-
662
- if (
663
- editingProposal &&
664
- storedDraft?.payload.mode === 'edit' &&
665
- storedDraft.payload.leadId === lead.id &&
666
- storedDraft.payload.proposalId === editingProposal.id
667
- ) {
668
- form.reset({
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()],
677
- });
678
- return;
679
- }
680
-
681
- if (!editingProposal) {
682
- form.reset(createDefaultProposalFormValues());
683
- return;
684
- }
685
-
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
- ];
699
-
700
- form.reset({
701
- code: editingProposal.code ?? '',
702
- title: editingProposal.title,
703
- validUntil: toDateInputValue(editingProposal.valid_until),
704
- contractCategory: editingProposal.contract_category ?? 'client',
705
- contractType: editingProposal.contract_type ?? 'service_agreement',
706
- billingModel: editingProposal.billing_model ?? 'fixed_price',
707
- summary: currentRevision?.summary ?? '',
708
- notes: editingProposal.notes ?? '',
709
- items: revisionItems,
710
- });
711
- }, [editingProposal, form, formOpen, lead.id, loadDraft]);
712
-
713
147
  const selectedProposal = useMemo(() => {
714
148
  const matchingDetail =
715
149
  proposalDetail?.id === selectedProposalId ? proposalDetail : null;
@@ -853,106 +287,6 @@ export function LeadProposalsTab({
853
287
  ]);
854
288
  };
855
289
 
856
- const handleSave = async (values: ProposalFormValues) => {
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
-
894
- const payload = {
895
- person_id: lead.id,
896
- code: values.code?.trim() || undefined,
897
- title: values.title.trim(),
898
- contract_category: values.contractCategory,
899
- contract_type: values.contractType,
900
- billing_model: values.billingModel,
901
- currency_code: 'BRL',
902
- valid_until: toIsoDateFromInputValue(values.validUntil),
903
- subtotal_amount_cents: subtotalAmountCents,
904
- discount_amount_cents: discountAmountCents,
905
- total_amount_cents: totalAmountCents,
906
- summary: values.summary?.trim() || null,
907
- notes: values.notes?.trim() || null,
908
- items: normalizedItems,
909
- };
910
-
911
- try {
912
- setIsSaving(true);
913
-
914
- if (editingProposal) {
915
- await request({
916
- url: `/proposal/${editingProposal.id}`,
917
- method: 'PATCH',
918
- data: {
919
- ...payload,
920
- create_new_revision: true,
921
- },
922
- });
923
-
924
- toast.success(t('proposals.toasts.updateSuccess'));
925
- setSelectedProposalId(editingProposal.id);
926
- await refreshAll(editingProposal.id);
927
- } else {
928
- const response = await request<ProposalRecord>({
929
- url: '/proposal',
930
- method: 'POST',
931
- data: payload,
932
- });
933
-
934
- const createdProposal = response.data;
935
- setSelectedProposalId(createdProposal.id);
936
- toast.success(t('proposals.toasts.createSuccess'));
937
- await refreshAll(createdProposal.id);
938
- }
939
-
940
- clearDraft();
941
- setFormOpen(false);
942
- } catch (error) {
943
- const message =
944
- error instanceof Error
945
- ? error.message
946
- : editingProposal
947
- ? t('proposals.toasts.updateError')
948
- : t('proposals.toasts.createError');
949
-
950
- toast.error(message);
951
- } finally {
952
- setIsSaving(false);
953
- }
954
- };
955
-
956
290
  const handleStatusAction = async (
957
291
  proposal: ProposalRecord,
958
292
  action: 'submit' | 'approve' | 'reject'
@@ -1096,7 +430,7 @@ export function LeadProposalsTab({
1096
430
  </div>
1097
431
  </div>
1098
432
 
1099
- <div className="mt-4 flex-1 overflow-y-auto pr-1">
433
+ <div className="mt-4 min-h-0 flex-1 overflow-hidden pr-1">
1100
434
  {isLoadingProposals ? (
1101
435
  <div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
1102
436
  {t('proposals.loading')}
@@ -1112,15 +446,15 @@ export function LeadProposalsTab({
1112
446
  onAction={openCreateSheet}
1113
447
  />
1114
448
  ) : (
1115
- <div className="grid gap-4 xl:grid-cols-[minmax(0,340px)_minmax(0,1fr)]">
1116
- <Card className="min-w-0 border-border/60 shadow-sm">
1117
- <CardHeader className="pb-3">
449
+ <div className="grid h-full min-h-[320px] gap-4 xl:grid-cols-[minmax(0,340px)_minmax(0,1fr)]">
450
+ <Card className="flex min-w-0 flex-col overflow-hidden border-border/60 shadow-sm">
451
+ <CardHeader className="flex-none pb-3">
1118
452
  <CardTitle className="text-base">
1119
453
  {t('proposals.count', { count: proposals.length })}
1120
454
  </CardTitle>
1121
455
  <CardDescription>{t('proposals.selectHint')}</CardDescription>
1122
456
  </CardHeader>
1123
- <CardContent className="space-y-2 pt-0">
457
+ <CardContent className="min-h-0 flex-1 space-y-2 overflow-y-auto pt-0">
1124
458
  {proposals.map((proposal) => {
1125
459
  const currentRevision = getCurrentRevision(proposal);
1126
460
  const isSelected = proposal.id === selectedProposalId;
@@ -1157,6 +491,15 @@ export function LeadProposalsTab({
1157
491
  >
1158
492
  {getStatusLabel(proposal.status)}
1159
493
  </Badge>
494
+ {proposal.status === 'pending_approval' &&
495
+ (proposal.required_approvals ?? 1) > 1 ? (
496
+ <span className="text-xs text-muted-foreground">
497
+ {t('proposals.approval.progress', {
498
+ count: proposal.approval_count ?? 0,
499
+ required: proposal.required_approvals ?? 1,
500
+ })}
501
+ </span>
502
+ ) : null}
1160
503
  </div>
1161
504
  <p className="text-xs text-muted-foreground">
1162
505
  {proposal.code || `#${proposal.id}`}
@@ -1195,7 +538,8 @@ export function LeadProposalsTab({
1195
538
  </DropdownMenuItem>
1196
539
  ) : null}
1197
540
 
1198
- {proposal.status === 'pending_approval' ? (
541
+ {proposal.status === 'pending_approval' &&
542
+ !proposal.current_user_has_approved ? (
1199
543
  <>
1200
544
  <DropdownMenuItem
1201
545
  onClick={() =>
@@ -1266,8 +610,8 @@ export function LeadProposalsTab({
1266
610
  </CardContent>
1267
611
  </Card>
1268
612
 
1269
- <Card className="min-w-0 border-border/60 shadow-sm">
1270
- <CardHeader className="gap-3">
613
+ <Card className="flex min-w-0 flex-col overflow-hidden border-border/60 shadow-sm">
614
+ <CardHeader className="flex-none gap-3">
1271
615
  <div className="flex flex-wrap items-start justify-between gap-3">
1272
616
  <div className="space-y-1">
1273
617
  <CardTitle className="text-base">
@@ -1312,7 +656,8 @@ export function LeadProposalsTab({
1312
656
  </Button>
1313
657
  ) : null}
1314
658
 
1315
- {selectedProposal.status === 'pending_approval' ? (
659
+ {selectedProposal.status === 'pending_approval' &&
660
+ !selectedProposal.current_user_has_approved ? (
1316
661
  <>
1317
662
  <Button
1318
663
  size="sm"
@@ -1369,26 +714,12 @@ export function LeadProposalsTab({
1369
714
  )}
1370
715
  {t('proposals.actions.generatePdf')}
1371
716
  </Button>
1372
-
1373
- {currentGeneratedPdf?.file_id ? (
1374
- <Button
1375
- variant="outline"
1376
- size="sm"
1377
- className="gap-2"
1378
- onClick={() =>
1379
- openStoredFile(currentGeneratedPdf.file_id)
1380
- }
1381
- >
1382
- <Download className="size-4" />
1383
- {t('proposals.actions.openPdf')}
1384
- </Button>
1385
- ) : null}
1386
717
  </div>
1387
718
  ) : null}
1388
719
  </div>
1389
720
  </CardHeader>
1390
721
 
1391
- <CardContent className="space-y-4">
722
+ <CardContent className="min-h-0 flex-1 space-y-4 overflow-y-auto">
1392
723
  {!selectedProposal ? (
1393
724
  <div className="rounded-xl border border-dashed p-6 text-sm text-muted-foreground">
1394
725
  <div className="flex flex-col items-center justify-center gap-2 text-center">
@@ -1419,7 +750,7 @@ export function LeadProposalsTab({
1419
750
  <p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
1420
751
  {t('proposals.fields.status')}
1421
752
  </p>
1422
- <div className="mt-2">
753
+ <div className="mt-2 space-y-1">
1423
754
  <Badge
1424
755
  variant="outline"
1425
756
  className={getStatusBadgeClassName(
@@ -1428,6 +759,15 @@ export function LeadProposalsTab({
1428
759
  >
1429
760
  {getStatusLabel(selectedProposal.status)}
1430
761
  </Badge>
762
+ {selectedProposal.status === 'pending_approval' ? (
763
+ <p className="text-xs text-muted-foreground">
764
+ {t('proposals.approval.progress', {
765
+ count: selectedProposal.approval_count ?? 0,
766
+ required:
767
+ selectedProposal.required_approvals ?? 1,
768
+ })}
769
+ </p>
770
+ ) : null}
1431
771
  </div>
1432
772
  </div>
1433
773
  <div className="rounded-xl border border-border/60 bg-muted/20 p-3">
@@ -1622,8 +962,22 @@ export function LeadProposalsTab({
1622
962
  </p>
1623
963
 
1624
964
  {currentGeneratedPdf ? (
1625
- <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/50 bg-muted/15 p-3">
1626
- <div className="min-w-0">
965
+ <div
966
+ role="button"
967
+ tabIndex={0}
968
+ onClick={() => handleGeneratePdf(selectedProposal)}
969
+ onKeyDown={(e) => {
970
+ if (e.key === 'Enter' || e.key === ' ') {
971
+ e.preventDefault();
972
+ handleGeneratePdf(selectedProposal);
973
+ }
974
+ }}
975
+ className={cn(
976
+ 'flex cursor-pointer flex-wrap items-center justify-between gap-3 rounded-xl border border-border/50 bg-muted/15 p-3 transition-all hover:border-primary/40 hover:bg-muted/30',
977
+ actionKey === `generate-pdf-${selectedProposal.id}` && 'pointer-events-none opacity-60'
978
+ )}
979
+ >
980
+ <div className="min-w-0 flex-1">
1627
981
  <p className="text-sm font-medium text-foreground">
1628
982
  {currentGeneratedPdf.file_name ||
1629
983
  t('proposals.info.generatedDocument')}
@@ -1636,20 +990,14 @@ export function LeadProposalsTab({
1636
990
  })}
1637
991
  </p>
1638
992
  </div>
1639
-
1640
- {currentGeneratedPdf.file_id ? (
1641
- <Button
1642
- variant="outline"
1643
- size="sm"
1644
- className="gap-2"
1645
- onClick={() =>
1646
- openStoredFile(currentGeneratedPdf.file_id)
1647
- }
1648
- >
1649
- <Download className="size-4" />
1650
- {t('proposals.actions.openPdf')}
1651
- </Button>
1652
- ) : null}
993
+ <div className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
994
+ {actionKey === `generate-pdf-${selectedProposal.id}` ? (
995
+ <Loader2 className="size-3.5 animate-spin" />
996
+ ) : (
997
+ <FileText className="size-3.5" />
998
+ )}
999
+ {t('proposals.actions.generatePdf')}
1000
+ </div>
1653
1001
  </div>
1654
1002
  ) : (
1655
1003
  <div className="rounded-xl border border-dashed p-3 text-sm text-muted-foreground">
@@ -1665,564 +1013,17 @@ export function LeadProposalsTab({
1665
1013
  )}
1666
1014
  </div>
1667
1015
 
1668
- <Sheet open={formOpen} onOpenChange={setFormOpen}>
1669
- <SheetContent className="flex h-full w-full max-w-[95vw] flex-col overflow-hidden p-0 sm:max-w-2xl xl:max-w-4xl">
1670
- <SheetHeader className="shrink-0 border-b px-5 py-4 text-left">
1671
- <div className="flex items-start gap-3 pr-6">
1672
- <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted text-muted-foreground">
1673
- <FileText className="size-5" />
1674
- </div>
1675
- <div className="space-y-1">
1676
- <SheetTitle>
1677
- {editingProposal
1678
- ? t('proposals.form.editTitle')
1679
- : t('proposals.form.createTitle')}
1680
- </SheetTitle>
1681
- <SheetDescription>
1682
- {editingProposal
1683
- ? t('proposals.form.editDescription')
1684
- : t('proposals.form.createDescription')}
1685
- </SheetDescription>
1686
- </div>
1687
- </div>
1688
- </SheetHeader>
1689
-
1690
- <Form {...form}>
1691
- <form
1692
- onSubmit={form.handleSubmit(handleSave)}
1693
- className="flex flex-1 flex-col overflow-hidden"
1694
- >
1695
- <div className="flex-1 overflow-y-auto px-5 py-4">
1696
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
1697
- <FormField
1698
- control={form.control}
1699
- name="title"
1700
- render={({ field }) => (
1701
- <FormItem className="lg:col-span-2">
1702
- <FormLabel>{t('proposals.form.title')}</FormLabel>
1703
- <FormControl>
1704
- <Input
1705
- {...field}
1706
- placeholder={t('proposals.form.titlePlaceholder')}
1707
- />
1708
- </FormControl>
1709
- <FormMessage />
1710
- </FormItem>
1711
- )}
1712
- />
1713
-
1714
- <FormField
1715
- control={form.control}
1716
- name="code"
1717
- render={({ field }) => (
1718
- <FormItem>
1719
- <FormLabel>{t('proposals.form.code')}</FormLabel>
1720
- <FormControl>
1721
- <Input
1722
- {...field}
1723
- value={field.value ?? ''}
1724
- placeholder={t('proposals.form.codePlaceholder')}
1725
- />
1726
- </FormControl>
1727
- <FormMessage />
1728
- </FormItem>
1729
- )}
1730
- />
1731
-
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>
2047
-
2048
- <FormField
2049
- control={form.control}
2050
- name="validUntil"
2051
- render={({ field }) => (
2052
- <FormItem>
2053
- <FormLabel>{t('proposals.form.validUntil')}</FormLabel>
2054
- <FormControl>
2055
- <Input
2056
- type="date"
2057
- {...field}
2058
- value={field.value ?? ''}
2059
- />
2060
- </FormControl>
2061
- <FormMessage />
2062
- </FormItem>
2063
- )}
2064
- />
2065
-
2066
- <FormField
2067
- control={form.control}
2068
- name="contractCategory"
2069
- render={({ field }) => (
2070
- <FormItem>
2071
- <FormLabel>
2072
- {t('proposals.form.contractCategory')}
2073
- </FormLabel>
2074
- <Select
2075
- onValueChange={field.onChange}
2076
- value={field.value}
2077
- >
2078
- <FormControl>
2079
- <SelectTrigger className="w-full">
2080
- <SelectValue />
2081
- </SelectTrigger>
2082
- </FormControl>
2083
- <SelectContent>
2084
- {CONTRACT_CATEGORY_OPTIONS.map((option) => (
2085
- <SelectItem key={option} value={option}>
2086
- {getProposalEnumLabel(
2087
- 'contractCategory',
2088
- option
2089
- )}
2090
- </SelectItem>
2091
- ))}
2092
- </SelectContent>
2093
- </Select>
2094
- <FormMessage />
2095
- </FormItem>
2096
- )}
2097
- />
2098
-
2099
- <FormField
2100
- control={form.control}
2101
- name="contractType"
2102
- render={({ field }) => (
2103
- <FormItem>
2104
- <FormLabel>
2105
- {t('proposals.form.contractType')}
2106
- </FormLabel>
2107
- <Select
2108
- onValueChange={field.onChange}
2109
- value={field.value}
2110
- >
2111
- <FormControl>
2112
- <SelectTrigger className="w-full">
2113
- <SelectValue />
2114
- </SelectTrigger>
2115
- </FormControl>
2116
- <SelectContent>
2117
- {CONTRACT_TYPE_OPTIONS.map((option) => (
2118
- <SelectItem key={option} value={option}>
2119
- {getProposalEnumLabel('contractType', option)}
2120
- </SelectItem>
2121
- ))}
2122
- </SelectContent>
2123
- </Select>
2124
- <FormMessage />
2125
- </FormItem>
2126
- )}
2127
- />
2128
-
2129
- <FormField
2130
- control={form.control}
2131
- name="billingModel"
2132
- render={({ field }) => (
2133
- <FormItem className="lg:col-span-2">
2134
- <FormLabel>
2135
- {t('proposals.form.billingModel')}
2136
- </FormLabel>
2137
- <Select
2138
- onValueChange={field.onChange}
2139
- value={field.value}
2140
- >
2141
- <FormControl>
2142
- <SelectTrigger className="w-full">
2143
- <SelectValue />
2144
- </SelectTrigger>
2145
- </FormControl>
2146
- <SelectContent>
2147
- {BILLING_MODEL_OPTIONS.map((option) => (
2148
- <SelectItem key={option} value={option}>
2149
- {getProposalEnumLabel('billingModel', option)}
2150
- </SelectItem>
2151
- ))}
2152
- </SelectContent>
2153
- </Select>
2154
- <FormMessage />
2155
- </FormItem>
2156
- )}
2157
- />
2158
-
2159
- <FormField
2160
- control={form.control}
2161
- name="summary"
2162
- render={({ field }) => (
2163
- <FormItem className="lg:col-span-2">
2164
- <FormLabel>{t('proposals.form.summary')}</FormLabel>
2165
- <FormControl>
2166
- <Textarea
2167
- {...field}
2168
- value={field.value ?? ''}
2169
- rows={4}
2170
- placeholder={t('proposals.form.summaryPlaceholder')}
2171
- />
2172
- </FormControl>
2173
- <FormMessage />
2174
- </FormItem>
2175
- )}
2176
- />
2177
-
2178
- <FormField
2179
- control={form.control}
2180
- name="notes"
2181
- render={({ field }) => (
2182
- <FormItem className="lg:col-span-2">
2183
- <FormLabel>{t('proposals.form.notes')}</FormLabel>
2184
- <FormControl>
2185
- <Textarea
2186
- {...field}
2187
- value={field.value ?? ''}
2188
- rows={6}
2189
- placeholder={t('proposals.form.notesPlaceholder')}
2190
- />
2191
- </FormControl>
2192
- <FormMessage />
2193
- </FormItem>
2194
- )}
2195
- />
2196
- </div>
2197
- </div>
2198
-
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}
2205
- <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2206
- <Button
2207
- type="button"
2208
- variant="outline"
2209
- onClick={() => setFormOpen(false)}
2210
- disabled={isSaving}
2211
- >
2212
- {t('proposals.actions.cancel')}
2213
- </Button>
2214
- <Button type="submit" disabled={isSaving} className="gap-2">
2215
- {isSaving ? (
2216
- <Loader2 className="size-4 animate-spin" />
2217
- ) : null}
2218
- {t('proposals.actions.save')}
2219
- </Button>
2220
- </div>
2221
- </div>
2222
- </form>
2223
- </Form>
2224
- </SheetContent>
2225
- </Sheet>
1016
+ <ProposalFormSheet
1017
+ proposalId={editingProposal?.id ?? null}
1018
+ personId={lead.id}
1019
+ open={formOpen}
1020
+ onClose={() => setFormOpen(false)}
1021
+ onSaved={async (savedId) => {
1022
+ setFormOpen(false);
1023
+ setSelectedProposalId(savedId);
1024
+ await refreshAll(savedId);
1025
+ }}
1026
+ />
2226
1027
 
2227
1028
  <AlertDialog
2228
1029
  open={!!deleteTarget}