@hed-hog/contact 0.0.328 → 0.0.330
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/proposal/proposal.controller.d.ts +2 -2
- package/dist/proposal/proposal.controller.d.ts.map +1 -1
- package/dist/proposal/proposal.controller.js +8 -6
- package/dist/proposal/proposal.controller.js.map +1 -1
- package/dist/proposal/proposal.service.d.ts +8 -2
- package/dist/proposal/proposal.service.d.ts.map +1 -1
- package/dist/proposal/proposal.service.js +595 -162
- package/dist/proposal/proposal.service.js.map +1 -1
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +4 -1
- package/hedhog/data/setting_group.yaml +16 -5
- package/hedhog/frontend/app/_components/person-picker.tsx.ejs +81 -5
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +7 -2
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +103 -1302
- package/hedhog/frontend/app/proposals/_components/proposal-form-sheet.tsx.ejs +1306 -0
- package/hedhog/frontend/app/proposals/_components/proposal-types.ts.ejs +172 -0
- package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +300 -136
- package/hedhog/frontend/messages/en.json +20 -2
- package/hedhog/frontend/messages/pt.json +20 -2
- package/package.json +7 -6
- package/src/proposal/proposal.controller.ts +7 -5
- package/src/proposal/proposal.service.ts +662 -192
- package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +0 -110
|
@@ -2,216 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
import { EmptyState } from '@/components/entity-list';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Card,
|
|
18
|
+
CardContent,
|
|
19
|
+
CardDescription,
|
|
20
|
+
CardHeader,
|
|
21
|
+
CardTitle,
|
|
22
22
|
} from '@/components/ui/card';
|
|
23
23
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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-
|
|
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
|
|
1626
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
-
<
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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}
|