@hed-hog/operations 0.0.305 → 0.0.309
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/controllers/operations-approvals.controller.d.ts +114 -1
- package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
- package/dist/controllers/operations-approvals.controller.js +16 -3
- package/dist/controllers/operations-approvals.controller.js.map +1 -1
- package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +16 -3
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +14 -453
- package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
- package/dist/controllers/operations-contracts.controller.js +11 -112
- package/dist/controllers/operations-contracts.controller.js.map +1 -1
- package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
- package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
- package/dist/controllers/operations-org-structure.controller.js +18 -5
- package/dist/controllers/operations-org-structure.controller.js.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +28 -4
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.js +17 -5
- package/dist/controllers/operations-projects.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +52 -4
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +28 -11
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/list-approvals.dto.d.ts +6 -0
- package/dist/dto/list-approvals.dto.d.ts.map +1 -0
- package/dist/dto/list-approvals.dto.js +28 -0
- package/dist/dto/list-approvals.dto.js.map +1 -0
- package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
- package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborator-types.dto.js +7 -1
- package/dist/dto/list-collaborator-types.dto.js.map +1 -1
- package/dist/dto/list-collaborators.dto.d.ts +1 -0
- package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
- package/dist/dto/list-collaborators.dto.js +5 -0
- package/dist/dto/list-collaborators.dto.js.map +1 -1
- package/dist/dto/list-contracts.dto.d.ts +8 -0
- package/dist/dto/list-contracts.dto.d.ts.map +1 -0
- package/dist/dto/list-contracts.dto.js +38 -0
- package/dist/dto/list-contracts.dto.js.map +1 -0
- package/dist/dto/list-departments.dto.d.ts +5 -0
- package/dist/dto/list-departments.dto.d.ts.map +1 -0
- package/dist/dto/list-departments.dto.js +23 -0
- package/dist/dto/list-departments.dto.js.map +1 -0
- package/dist/dto/list-projects.dto.d.ts +5 -0
- package/dist/dto/list-projects.dto.d.ts.map +1 -0
- package/dist/dto/list-projects.dto.js +23 -0
- package/dist/dto/list-projects.dto.js.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
- package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
- package/dist/dto/list-schedule-adjustments.dto.js +23 -0
- package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
- package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
- package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
- package/dist/dto/list-time-off-requests.dto.js +23 -0
- package/dist/dto/list-time-off-requests.dto.js.map +1 -0
- package/dist/dto/list-timesheets.dto.d.ts +5 -0
- package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
- package/dist/dto/list-timesheets.dto.js +23 -0
- package/dist/dto/list-timesheets.dto.js.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
- package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
- package/dist/dto/reorder-collaborator-types.dto.js +25 -0
- package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
- package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
- package/dist/dto/update-collaborator-type.dto.js +2 -1
- package/dist/dto/update-collaborator-type.dto.js.map +1 -1
- package/dist/operations.service.d.ts +362 -271
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +1195 -1098
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +73 -22
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +19 -55
- package/hedhog/data/operations_collaborator_type.yaml +76 -76
- package/hedhog/data/route.yaml +52 -70
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
- package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
- package/hedhog/frontend/app/approvals/page.tsx.ejs +843 -151
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +457 -154
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
- package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
- package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +546 -118
- package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +647 -342
- package/hedhog/frontend/messages/en.json +148 -14
- package/hedhog/frontend/messages/pt.json +199 -56
- package/hedhog/table/operations_collaborator.yaml +18 -18
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
- package/hedhog/table/operations_collaborator_type.yaml +33 -33
- package/hedhog/table/operations_contract.yaml +0 -9
- package/hedhog/table/operations_contract_document.yaml +33 -33
- package/package.json +4 -4
- package/src/controllers/operations-approvals.controller.ts +9 -3
- package/src/controllers/operations-collaborators.controller.ts +15 -2
- package/src/controllers/operations-contracts.controller.ts +8 -92
- package/src/controllers/operations-org-structure.controller.ts +17 -4
- package/src/controllers/operations-projects.controller.ts +10 -4
- package/src/controllers/operations-timesheets.controller.ts +30 -8
- package/src/dto/create-collaborator-type.dto.ts +43 -43
- package/src/dto/create-collaborator.dto.ts +223 -223
- package/src/dto/list-approvals.dto.ts +12 -0
- package/src/dto/list-collaborator-types.dto.ts +20 -15
- package/src/dto/list-collaborators.dto.ts +34 -30
- package/src/dto/list-contracts.dto.ts +20 -0
- package/src/dto/list-departments.dto.ts +8 -0
- package/src/dto/list-projects.dto.ts +8 -0
- package/src/dto/list-schedule-adjustments.dto.ts +8 -0
- package/src/dto/list-time-off-requests.dto.ts +8 -0
- package/src/dto/list-timesheets.dto.ts +8 -0
- package/src/dto/reorder-collaborator-types.dto.ts +10 -0
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/operations.service.spec.ts +96 -30
- package/src/operations.service.ts +1738 -1777
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
- package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
- package/hedhog/table/operations_contract_financial_term.yaml +0 -40
- package/hedhog/table/operations_contract_revision.yaml +0 -38
- package/hedhog/table/operations_contract_signature.yaml +0 -38
- package/hedhog/table/operations_contract_template.yaml +0 -58
|
@@ -1,3520 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Button } from '@/components/ui/button';
|
|
4
|
-
import { Input } from '@/components/ui/input';
|
|
5
|
-
import { InputMoney } from '@/components/ui/input-money';
|
|
6
|
-
import { Label } from '@/components/ui/label';
|
|
7
|
-
import {
|
|
8
|
-
Select,
|
|
9
|
-
SelectContent,
|
|
10
|
-
SelectItem,
|
|
11
|
-
SelectTrigger,
|
|
12
|
-
SelectValue,
|
|
13
|
-
} from '@/components/ui/select';
|
|
14
|
-
import { Switch } from '@/components/ui/switch';
|
|
15
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
16
|
-
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
17
|
-
import {
|
|
18
|
-
BadgeDollarSign,
|
|
19
|
-
BriefcaseBusiness,
|
|
20
|
-
CheckCircle2,
|
|
21
|
-
ChevronLeft,
|
|
22
|
-
ChevronRight,
|
|
23
|
-
CopyPlus,
|
|
24
|
-
Download,
|
|
25
|
-
FileSpreadsheet,
|
|
26
|
-
FileText,
|
|
27
|
-
LayoutTemplate,
|
|
28
|
-
LoaderCircle,
|
|
29
|
-
NotebookText,
|
|
30
|
-
Save,
|
|
31
|
-
Sparkles,
|
|
32
|
-
Trash2,
|
|
33
|
-
Upload,
|
|
34
|
-
UserRound,
|
|
35
|
-
Users,
|
|
36
|
-
} from 'lucide-react';
|
|
37
|
-
import { useTranslations } from 'next-intl';
|
|
38
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
39
|
-
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
40
|
-
import type {
|
|
41
|
-
OperationsCollaborator,
|
|
42
|
-
OperationsContract,
|
|
43
|
-
OperationsContractDetails,
|
|
44
|
-
OperationsContractTemplate,
|
|
45
|
-
} from '../_lib/types';
|
|
46
|
-
import {
|
|
47
|
-
formatCurrency,
|
|
48
|
-
formatDate,
|
|
49
|
-
formatEnumLabel,
|
|
50
|
-
} from '../_lib/utils/format';
|
|
51
|
-
import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
|
|
52
|
-
import { CollaboratorSelectWithCreate } from './collaborator-select-with-create';
|
|
53
|
-
import { ContractContentEditor } from './contract-content-editor';
|
|
54
|
-
import { ContractTemplateSelectWithCreate } from './contract-template-select-with-create';
|
|
55
|
-
import { PersonSelectWithCreate } from './person-select-with-create';
|
|
56
|
-
import { SectionCard } from './section-card';
|
|
57
|
-
|
|
58
|
-
type CreationMode = 'blank' | 'template' | 'upload' | 'duplicate' | 'ai_prompt';
|
|
59
|
-
|
|
60
|
-
type PartyFieldMode = 'existing' | 'new';
|
|
61
|
-
|
|
62
|
-
type PersonValueOption = {
|
|
63
|
-
id?: number;
|
|
64
|
-
value: string;
|
|
65
|
-
label: string;
|
|
66
|
-
normalizedValue: string;
|
|
67
|
-
isPrimary: boolean;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type PersonContactRecord = {
|
|
71
|
-
id?: number;
|
|
72
|
-
value?: string | null;
|
|
73
|
-
is_primary?: boolean;
|
|
74
|
-
contact_type_id?: number;
|
|
75
|
-
contact_type?: {
|
|
76
|
-
id?: number;
|
|
77
|
-
code?: string;
|
|
78
|
-
} | null;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
type PersonDocumentRecord = {
|
|
82
|
-
id?: number;
|
|
83
|
-
value?: string | null;
|
|
84
|
-
document_type_id?: number;
|
|
85
|
-
document_type?: {
|
|
86
|
-
id?: number;
|
|
87
|
-
code?: string;
|
|
88
|
-
} | null;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
type PersonAddressRecord = {
|
|
92
|
-
id?: number;
|
|
93
|
-
line1?: string | null;
|
|
94
|
-
line2?: string | null;
|
|
95
|
-
city?: string | null;
|
|
96
|
-
state?: string | null;
|
|
97
|
-
is_primary?: boolean;
|
|
98
|
-
address_type?: string | null;
|
|
99
|
-
postal_code?: string | null;
|
|
100
|
-
country_code?: string | null;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
type PersonDetailsRecord = {
|
|
104
|
-
id: number;
|
|
105
|
-
name: string;
|
|
106
|
-
type?: string | null;
|
|
107
|
-
status?: string | null;
|
|
108
|
-
contact?: PersonContactRecord[];
|
|
109
|
-
document?: PersonDocumentRecord[];
|
|
110
|
-
address?: PersonAddressRecord[];
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
type ContactTypeLookup = {
|
|
114
|
-
code: string;
|
|
115
|
-
contact_type_id: number;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type DocumentTypeLookup = {
|
|
119
|
-
code: string;
|
|
120
|
-
document_type_id: number;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
type PartyState = {
|
|
124
|
-
displayName: string;
|
|
125
|
-
partyRole: string;
|
|
126
|
-
partyType: string;
|
|
127
|
-
documentNumber: string;
|
|
128
|
-
email: string;
|
|
129
|
-
phone: string;
|
|
130
|
-
isPrimary: boolean;
|
|
131
|
-
personId: number | null;
|
|
132
|
-
availableDocuments: PersonValueOption[];
|
|
133
|
-
availableEmails: PersonValueOption[];
|
|
134
|
-
availablePhones: PersonValueOption[];
|
|
135
|
-
documentMode: PartyFieldMode;
|
|
136
|
-
emailMode: PartyFieldMode;
|
|
137
|
-
phoneMode: PartyFieldMode;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
type SignatureState = {
|
|
141
|
-
signerName: string;
|
|
142
|
-
signerRole: string;
|
|
143
|
-
signerEmail: string;
|
|
144
|
-
status: string;
|
|
145
|
-
signedAt: string;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
type FinancialTermState = {
|
|
149
|
-
label: string;
|
|
150
|
-
termType: string;
|
|
151
|
-
amount: string;
|
|
152
|
-
recurrence: string;
|
|
153
|
-
dueDay: string;
|
|
154
|
-
notes: string;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
type RevisionState = {
|
|
158
|
-
title: string;
|
|
159
|
-
revisionType: string;
|
|
160
|
-
effectiveDate: string;
|
|
161
|
-
status: string;
|
|
162
|
-
summary: string;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
type DocumentState = {
|
|
166
|
-
id?: number;
|
|
167
|
-
fileId?: number | null;
|
|
168
|
-
fileName: string;
|
|
169
|
-
mimeType: string;
|
|
170
|
-
extractionStatus?: string | null;
|
|
171
|
-
extractionSummary?: string | null;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
type ContractWizardState = {
|
|
175
|
-
code: string;
|
|
176
|
-
name: string;
|
|
177
|
-
clientName: string;
|
|
178
|
-
contractTemplateId: string;
|
|
179
|
-
contractCategory: string;
|
|
180
|
-
contractType: string;
|
|
181
|
-
signatureStatus: string;
|
|
182
|
-
isActive: boolean;
|
|
183
|
-
billingModel: string;
|
|
184
|
-
accountManagerCollaboratorId: string;
|
|
185
|
-
relatedCollaboratorId: string;
|
|
186
|
-
originType: string;
|
|
187
|
-
originId: string;
|
|
188
|
-
startDate: string;
|
|
189
|
-
endDate: string;
|
|
190
|
-
signedAt: string;
|
|
191
|
-
effectiveDate: string;
|
|
192
|
-
budgetAmount: string;
|
|
193
|
-
monthlyHourCap: string;
|
|
194
|
-
status: string;
|
|
195
|
-
description: string;
|
|
196
|
-
contentHtml: string;
|
|
197
|
-
creationMode: CreationMode;
|
|
198
|
-
wizardStep: number;
|
|
199
|
-
parties: PartyState[];
|
|
200
|
-
signatures: SignatureState[];
|
|
201
|
-
financialTerms: FinancialTermState[];
|
|
202
|
-
revisions: RevisionState[];
|
|
203
|
-
sourceDocument: DocumentState | null;
|
|
204
|
-
generatedPdfDocument: DocumentState | null;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
type ExtractedDraft = Partial<ContractWizardState> & {
|
|
208
|
-
summary?: string;
|
|
209
|
-
missingFields?: string[];
|
|
210
|
-
warnings?: string[];
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
type ContractAiReviewState = {
|
|
214
|
-
summary?: string;
|
|
215
|
-
missingFields: string[];
|
|
216
|
-
warnings: string[];
|
|
217
|
-
checklist: string[];
|
|
218
|
-
status?: string;
|
|
219
|
-
reviewedAt?: string;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
type ContractWizardSheetProps = {
|
|
223
|
-
contractId?: number;
|
|
224
|
-
duplicateFromId?: number;
|
|
225
|
-
initialTemplateId?: number;
|
|
226
|
-
isCreateFlow?: boolean;
|
|
227
|
-
onCancel?: () => void;
|
|
228
|
-
onSaved?: (contract: OperationsContractDetails) => void | Promise<void>;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const allSteps = [
|
|
232
|
-
{ key: 'origin', label: 'Origem', icon: Sparkles },
|
|
233
|
-
{ key: 'identification', label: 'Identificacao', icon: BriefcaseBusiness },
|
|
234
|
-
{ key: 'parties', label: 'Partes', icon: Users },
|
|
235
|
-
{ key: 'terms', label: 'Condicoes', icon: BadgeDollarSign },
|
|
236
|
-
{ key: 'content', label: 'Conteudo', icon: NotebookText },
|
|
237
|
-
{ key: 'review', label: 'Revisao', icon: CheckCircle2 },
|
|
238
|
-
] as const;
|
|
239
|
-
|
|
240
|
-
const createFlowSteps = allSteps.filter((step) => step.key !== 'content');
|
|
241
|
-
|
|
242
|
-
const contractDateFields: Array<{
|
|
243
|
-
key: 'startDate' | 'endDate' | 'signedAt' | 'effectiveDate';
|
|
244
|
-
label: string;
|
|
245
|
-
}> = [
|
|
246
|
-
{ key: 'startDate', label: 'Inicio' },
|
|
247
|
-
{ key: 'endDate', label: 'Fim' },
|
|
248
|
-
{ key: 'signedAt', label: 'Assinado em' },
|
|
249
|
-
{ key: 'effectiveDate', label: 'Vigencia' },
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
const PERSON_FIELD_NEW_VALUE = '__new__';
|
|
253
|
-
const EMAIL_CONTACT_TYPE_ID = 2;
|
|
254
|
-
const PHONE_CONTACT_TYPE_IDS = new Set([1, 3, 4, 5]);
|
|
255
|
-
const PHONE_CONTACT_TYPE_CODES = new Set([
|
|
256
|
-
'PHONE',
|
|
257
|
-
'FAX',
|
|
258
|
-
'WHATSAPP',
|
|
259
|
-
'TELEGRAM',
|
|
260
|
-
'MOBILE',
|
|
261
|
-
]);
|
|
262
|
-
|
|
263
|
-
function emptyParty(): PartyState {
|
|
264
|
-
return {
|
|
265
|
-
displayName: '',
|
|
266
|
-
partyRole: 'client',
|
|
267
|
-
partyType: 'company',
|
|
268
|
-
documentNumber: '',
|
|
269
|
-
email: '',
|
|
270
|
-
phone: '',
|
|
271
|
-
isPrimary: false,
|
|
272
|
-
personId: null,
|
|
273
|
-
availableDocuments: [],
|
|
274
|
-
availableEmails: [],
|
|
275
|
-
availablePhones: [],
|
|
276
|
-
documentMode: 'new',
|
|
277
|
-
emailMode: 'new',
|
|
278
|
-
phoneMode: 'new',
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function emptySignature(): SignatureState {
|
|
283
|
-
return {
|
|
284
|
-
signerName: '',
|
|
285
|
-
signerRole: '',
|
|
286
|
-
signerEmail: '',
|
|
287
|
-
status: 'pending',
|
|
288
|
-
signedAt: '',
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function emptyFinancialTerm(): FinancialTermState {
|
|
293
|
-
return {
|
|
294
|
-
label: '',
|
|
295
|
-
termType: 'value',
|
|
296
|
-
amount: '',
|
|
297
|
-
recurrence: 'one_time',
|
|
298
|
-
dueDay: '',
|
|
299
|
-
notes: '',
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function buildEmptyForm(mode: CreationMode = 'blank'): ContractWizardState {
|
|
304
|
-
return {
|
|
305
|
-
code: '',
|
|
306
|
-
name: '',
|
|
307
|
-
clientName: '',
|
|
308
|
-
contractTemplateId: 'none',
|
|
309
|
-
contractCategory: 'client',
|
|
310
|
-
contractType: 'service_agreement',
|
|
311
|
-
signatureStatus: 'not_started',
|
|
312
|
-
isActive: true,
|
|
313
|
-
billingModel: 'time_and_material',
|
|
314
|
-
accountManagerCollaboratorId: 'none',
|
|
315
|
-
relatedCollaboratorId: 'none',
|
|
316
|
-
originType: 'manual',
|
|
317
|
-
originId: '',
|
|
318
|
-
startDate: '',
|
|
319
|
-
endDate: '',
|
|
320
|
-
signedAt: '',
|
|
321
|
-
effectiveDate: '',
|
|
322
|
-
budgetAmount: '',
|
|
323
|
-
monthlyHourCap: '',
|
|
324
|
-
status: 'draft',
|
|
325
|
-
description: '',
|
|
326
|
-
contentHtml: '',
|
|
327
|
-
creationMode: mode,
|
|
328
|
-
wizardStep: 0,
|
|
329
|
-
parties: [emptyParty()],
|
|
330
|
-
signatures: [emptySignature()],
|
|
331
|
-
financialTerms: [emptyFinancialTerm()],
|
|
332
|
-
revisions: [],
|
|
333
|
-
sourceDocument: null,
|
|
334
|
-
generatedPdfDocument: null,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function toTextValue(value: unknown) {
|
|
339
|
-
return typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
343
|
-
return value && typeof value === 'object'
|
|
344
|
-
? (value as Record<string, unknown>)
|
|
345
|
-
: {};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function normalizePartyFieldValue(
|
|
349
|
-
kind: 'document' | 'email' | 'phone',
|
|
350
|
-
value?: string | null
|
|
351
|
-
) {
|
|
352
|
-
const normalized = String(value ?? '').trim();
|
|
353
|
-
|
|
354
|
-
if (!normalized) {
|
|
355
|
-
return '';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (kind === 'email') {
|
|
359
|
-
return normalized.toLowerCase();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return normalized.replace(/\D/g, '');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function dedupePersonValueOptions(options: PersonValueOption[]) {
|
|
366
|
-
const seen = new Set<string>();
|
|
367
|
-
|
|
368
|
-
return options.filter((option) => {
|
|
369
|
-
if (!option.normalizedValue || seen.has(option.normalizedValue)) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
seen.add(option.normalizedValue);
|
|
374
|
-
return true;
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function getDefaultPersonOptionValue(options: PersonValueOption[]) {
|
|
379
|
-
return (
|
|
380
|
-
options.find((option) => option.isPrimary)?.value ?? options[0]?.value ?? ''
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function resolvePartyFieldMode(
|
|
385
|
-
kind: 'document' | 'email' | 'phone',
|
|
386
|
-
value: string,
|
|
387
|
-
options: PersonValueOption[]
|
|
388
|
-
): PartyFieldMode {
|
|
389
|
-
if (!options.length) {
|
|
390
|
-
return 'new';
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const normalizedValue = normalizePartyFieldValue(kind, value);
|
|
394
|
-
return normalizedValue &&
|
|
395
|
-
options.some((option) => option.normalizedValue === normalizedValue)
|
|
396
|
-
? 'existing'
|
|
397
|
-
: 'new';
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function buildPersonValueOptions(person: PersonDetailsRecord) {
|
|
401
|
-
const documents = dedupePersonValueOptions(
|
|
402
|
-
(person.document ?? []).flatMap((document) => {
|
|
403
|
-
const value = String(document.value ?? '').trim();
|
|
404
|
-
if (!value) {
|
|
405
|
-
return [];
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const code = String(document.document_type?.code ?? 'documento').trim();
|
|
409
|
-
return [
|
|
410
|
-
{
|
|
411
|
-
id: document.id,
|
|
412
|
-
value,
|
|
413
|
-
label: `${formatEnumLabel(code)} • ${value}`,
|
|
414
|
-
normalizedValue: normalizePartyFieldValue('document', value),
|
|
415
|
-
isPrimary: false,
|
|
416
|
-
},
|
|
417
|
-
];
|
|
418
|
-
})
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
const emails = dedupePersonValueOptions(
|
|
422
|
-
(person.contact ?? [])
|
|
423
|
-
.filter((contact) => {
|
|
424
|
-
const code = String(contact.contact_type?.code ?? '').toUpperCase();
|
|
425
|
-
const typeId = Number(
|
|
426
|
-
contact.contact_type?.id ?? contact.contact_type_id ?? 0
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
return code === 'EMAIL' || typeId === EMAIL_CONTACT_TYPE_ID;
|
|
430
|
-
})
|
|
431
|
-
.flatMap((contact) => {
|
|
432
|
-
const value = String(contact.value ?? '').trim();
|
|
433
|
-
if (!value) {
|
|
434
|
-
return [];
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const code = String(contact.contact_type?.code ?? 'email').trim();
|
|
438
|
-
return [
|
|
439
|
-
{
|
|
440
|
-
id: contact.id,
|
|
441
|
-
value,
|
|
442
|
-
label: `${contact.is_primary ? 'Principal' : formatEnumLabel(code)} • ${value}`,
|
|
443
|
-
normalizedValue: normalizePartyFieldValue('email', value),
|
|
444
|
-
isPrimary: Boolean(contact.is_primary),
|
|
445
|
-
},
|
|
446
|
-
];
|
|
447
|
-
})
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
const phones = dedupePersonValueOptions(
|
|
451
|
-
(person.contact ?? [])
|
|
452
|
-
.filter((contact) => {
|
|
453
|
-
const code = String(contact.contact_type?.code ?? '').toUpperCase();
|
|
454
|
-
const typeId = Number(
|
|
455
|
-
contact.contact_type?.id ?? contact.contact_type_id ?? 0
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
return (
|
|
459
|
-
PHONE_CONTACT_TYPE_CODES.has(code) ||
|
|
460
|
-
PHONE_CONTACT_TYPE_IDS.has(typeId)
|
|
461
|
-
);
|
|
462
|
-
})
|
|
463
|
-
.flatMap((contact) => {
|
|
464
|
-
const value = String(contact.value ?? '').trim();
|
|
465
|
-
if (!value) {
|
|
466
|
-
return [];
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const code = String(contact.contact_type?.code ?? 'telefone').trim();
|
|
470
|
-
return [
|
|
471
|
-
{
|
|
472
|
-
id: contact.id,
|
|
473
|
-
value,
|
|
474
|
-
label: `${contact.is_primary ? 'Principal' : formatEnumLabel(code)} • ${value}`,
|
|
475
|
-
normalizedValue: normalizePartyFieldValue('phone', value),
|
|
476
|
-
isPrimary: Boolean(contact.is_primary),
|
|
477
|
-
},
|
|
478
|
-
];
|
|
479
|
-
})
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
documents,
|
|
484
|
-
emails,
|
|
485
|
-
phones,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function applyPersonDetailsToParty(
|
|
490
|
-
party: PartyState,
|
|
491
|
-
personId: number,
|
|
492
|
-
personName: string,
|
|
493
|
-
person: PersonDetailsRecord
|
|
494
|
-
): PartyState {
|
|
495
|
-
const { documents, emails, phones } = buildPersonValueOptions(person);
|
|
496
|
-
const nextDocumentValue = getDefaultPersonOptionValue(documents);
|
|
497
|
-
const nextEmailValue = getDefaultPersonOptionValue(emails);
|
|
498
|
-
const nextPhoneValue = getDefaultPersonOptionValue(phones);
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
...party,
|
|
502
|
-
displayName: personName || person.name || party.displayName,
|
|
503
|
-
partyType:
|
|
504
|
-
person.type === 'individual' || person.type === 'company'
|
|
505
|
-
? person.type
|
|
506
|
-
: party.partyType,
|
|
507
|
-
personId,
|
|
508
|
-
availableDocuments: documents,
|
|
509
|
-
availableEmails: emails,
|
|
510
|
-
availablePhones: phones,
|
|
511
|
-
documentNumber: nextDocumentValue || party.documentNumber,
|
|
512
|
-
email: nextEmailValue || party.email,
|
|
513
|
-
phone: nextPhoneValue || party.phone,
|
|
514
|
-
documentMode: documents.length ? 'existing' : 'new',
|
|
515
|
-
emailMode: emails.length ? 'existing' : 'new',
|
|
516
|
-
phoneMode: phones.length ? 'existing' : 'new',
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function clearPersonDetailsFromParty(
|
|
521
|
-
party: PartyState,
|
|
522
|
-
personName = ''
|
|
523
|
-
): PartyState {
|
|
524
|
-
return {
|
|
525
|
-
...party,
|
|
526
|
-
displayName: personName,
|
|
527
|
-
personId: null,
|
|
528
|
-
availableDocuments: [],
|
|
529
|
-
availableEmails: [],
|
|
530
|
-
availablePhones: [],
|
|
531
|
-
documentMode: 'new',
|
|
532
|
-
emailMode: 'new',
|
|
533
|
-
phoneMode: 'new',
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function getPrimaryPartyIndex(parties: PartyState[]) {
|
|
538
|
-
const primaryIndex = parties.findIndex((party) => party.isPrimary);
|
|
539
|
-
return primaryIndex >= 0 ? primaryIndex : 0;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function preservePartySelectionMetadata(
|
|
543
|
-
nextForm: ContractWizardState,
|
|
544
|
-
currentForm: ContractWizardState
|
|
545
|
-
): ContractWizardState {
|
|
546
|
-
return {
|
|
547
|
-
...nextForm,
|
|
548
|
-
parties: nextForm.parties.map((party, index) => {
|
|
549
|
-
const currentParty = currentForm.parties[index];
|
|
550
|
-
if (!currentParty?.personId) {
|
|
551
|
-
return party;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const availableDocuments = currentParty.availableDocuments ?? [];
|
|
555
|
-
const availableEmails = currentParty.availableEmails ?? [];
|
|
556
|
-
const availablePhones = currentParty.availablePhones ?? [];
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
...party,
|
|
560
|
-
personId: currentParty.personId,
|
|
561
|
-
availableDocuments,
|
|
562
|
-
availableEmails,
|
|
563
|
-
availablePhones,
|
|
564
|
-
documentMode: resolvePartyFieldMode(
|
|
565
|
-
'document',
|
|
566
|
-
party.documentNumber,
|
|
567
|
-
availableDocuments
|
|
568
|
-
),
|
|
569
|
-
emailMode: resolvePartyFieldMode('email', party.email, availableEmails),
|
|
570
|
-
phoneMode: resolvePartyFieldMode('phone', party.phone, availablePhones),
|
|
571
|
-
};
|
|
572
|
-
}),
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function PersonDataChoiceField({
|
|
577
|
-
label,
|
|
578
|
-
value,
|
|
579
|
-
options,
|
|
580
|
-
mode,
|
|
581
|
-
kind,
|
|
582
|
-
inputType = 'text',
|
|
583
|
-
placeholder,
|
|
584
|
-
onModeChange,
|
|
585
|
-
onValueChange,
|
|
586
|
-
}: {
|
|
587
|
-
label: string;
|
|
588
|
-
value: string;
|
|
589
|
-
options: PersonValueOption[];
|
|
590
|
-
mode: PartyFieldMode;
|
|
591
|
-
kind: 'document' | 'email' | 'phone';
|
|
592
|
-
inputType?: 'text' | 'email' | 'tel';
|
|
593
|
-
placeholder: string;
|
|
594
|
-
onModeChange: (mode: PartyFieldMode) => void;
|
|
595
|
-
onValueChange: (value: string) => void;
|
|
596
|
-
}) {
|
|
597
|
-
const hasOptions = options.length > 0;
|
|
598
|
-
const normalizedValue = normalizePartyFieldValue(kind, value);
|
|
599
|
-
const matchesExisting = options.some(
|
|
600
|
-
(option) => option.normalizedValue === normalizedValue
|
|
601
|
-
);
|
|
602
|
-
const selectValue =
|
|
603
|
-
hasOptions && mode === 'existing' && matchesExisting
|
|
604
|
-
? (options.find((option) => option.normalizedValue === normalizedValue)
|
|
605
|
-
?.value ?? PERSON_FIELD_NEW_VALUE)
|
|
606
|
-
: PERSON_FIELD_NEW_VALUE;
|
|
607
|
-
|
|
608
|
-
return (
|
|
609
|
-
<div className="space-y-1.5">
|
|
610
|
-
<Label className="text-xs">{label}</Label>
|
|
611
|
-
|
|
612
|
-
{hasOptions ? (
|
|
613
|
-
<Select
|
|
614
|
-
value={selectValue}
|
|
615
|
-
onValueChange={(selectedValue) => {
|
|
616
|
-
if (selectedValue === PERSON_FIELD_NEW_VALUE) {
|
|
617
|
-
onModeChange('new');
|
|
618
|
-
if (mode !== 'new' && matchesExisting) {
|
|
619
|
-
onValueChange('');
|
|
620
|
-
}
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
onModeChange('existing');
|
|
625
|
-
onValueChange(selectedValue);
|
|
626
|
-
}}
|
|
627
|
-
>
|
|
628
|
-
<SelectTrigger className="w-full">
|
|
629
|
-
<SelectValue placeholder={placeholder} />
|
|
630
|
-
</SelectTrigger>
|
|
631
|
-
<SelectContent>
|
|
632
|
-
{options.map((option) => (
|
|
633
|
-
<SelectItem
|
|
634
|
-
key={`${option.label}-${option.normalizedValue}`}
|
|
635
|
-
value={option.value}
|
|
636
|
-
>
|
|
637
|
-
{option.label}
|
|
638
|
-
</SelectItem>
|
|
639
|
-
))}
|
|
640
|
-
<SelectItem value={PERSON_FIELD_NEW_VALUE}>Digitar novo</SelectItem>
|
|
641
|
-
</SelectContent>
|
|
642
|
-
</Select>
|
|
643
|
-
) : null}
|
|
644
|
-
|
|
645
|
-
{!hasOptions || mode === 'new' ? (
|
|
646
|
-
<Input
|
|
647
|
-
type={inputType}
|
|
648
|
-
value={value}
|
|
649
|
-
placeholder={placeholder}
|
|
650
|
-
onChange={(event) => onValueChange(event.target.value)}
|
|
651
|
-
/>
|
|
652
|
-
) : null}
|
|
653
|
-
|
|
654
|
-
<p className="text-[11px] text-muted-foreground">
|
|
655
|
-
{hasOptions
|
|
656
|
-
? mode === 'existing'
|
|
657
|
-
? 'Usando um dado já cadastrado na pessoa selecionada.'
|
|
658
|
-
: 'O novo valor será salvo também no cadastro da pessoa ao salvar o contrato.'
|
|
659
|
-
: 'Sem dados cadastrados para essa pessoa. O que você digitar será salvo junto ao contrato.'}
|
|
660
|
-
</p>
|
|
661
|
-
</div>
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function openStoredFile(fileId?: number | null) {
|
|
666
|
-
if (!fileId) return;
|
|
667
|
-
const baseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
|
|
668
|
-
window.open(
|
|
669
|
-
`${baseUrl}/file/open/${fileId}`,
|
|
670
|
-
'_blank',
|
|
671
|
-
'noopener,noreferrer'
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function toFormState(contract: OperationsContractDetails): ContractWizardState {
|
|
676
|
-
const sourceDocument =
|
|
677
|
-
contract.documents.find(
|
|
678
|
-
(document) =>
|
|
679
|
-
document.isCurrent && document.documentType === 'source_upload'
|
|
680
|
-
) ?? null;
|
|
681
|
-
const generatedPdfDocument =
|
|
682
|
-
contract.documents.find(
|
|
683
|
-
(document) =>
|
|
684
|
-
document.isCurrent && document.documentType === 'generated_pdf'
|
|
685
|
-
) ?? null;
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
code: contract.code ?? '',
|
|
689
|
-
name: contract.name ?? '',
|
|
690
|
-
clientName: contract.clientName ?? '',
|
|
691
|
-
contractTemplateId: contract.contractTemplateId
|
|
692
|
-
? String(contract.contractTemplateId)
|
|
693
|
-
: 'none',
|
|
694
|
-
contractCategory: contract.contractCategory ?? 'client',
|
|
695
|
-
contractType: contract.contractType ?? 'service_agreement',
|
|
696
|
-
signatureStatus: contract.signatureStatus ?? 'not_started',
|
|
697
|
-
isActive: contract.isActive ?? true,
|
|
698
|
-
billingModel: contract.billingModel ?? 'time_and_material',
|
|
699
|
-
accountManagerCollaboratorId: contract.accountManagerCollaboratorId
|
|
700
|
-
? String(contract.accountManagerCollaboratorId)
|
|
701
|
-
: 'none',
|
|
702
|
-
relatedCollaboratorId: contract.relatedCollaboratorId
|
|
703
|
-
? String(contract.relatedCollaboratorId)
|
|
704
|
-
: 'none',
|
|
705
|
-
originType: contract.originType ?? 'manual',
|
|
706
|
-
originId:
|
|
707
|
-
contract.originId !== null && contract.originId !== undefined
|
|
708
|
-
? String(contract.originId)
|
|
709
|
-
: '',
|
|
710
|
-
startDate: contract.startDate ?? '',
|
|
711
|
-
endDate: contract.endDate ?? '',
|
|
712
|
-
signedAt: contract.signedAt ?? '',
|
|
713
|
-
effectiveDate: contract.effectiveDate ?? '',
|
|
714
|
-
budgetAmount:
|
|
715
|
-
contract.budgetAmount !== null && contract.budgetAmount !== undefined
|
|
716
|
-
? String(contract.budgetAmount)
|
|
717
|
-
: '',
|
|
718
|
-
monthlyHourCap:
|
|
719
|
-
contract.monthlyHourCap !== null && contract.monthlyHourCap !== undefined
|
|
720
|
-
? String(contract.monthlyHourCap)
|
|
721
|
-
: '',
|
|
722
|
-
status: contract.status ?? 'draft',
|
|
723
|
-
description: contract.description ?? '',
|
|
724
|
-
contentHtml: contract.contentHtml ?? '',
|
|
725
|
-
creationMode: (contract.creationMode as CreationMode) ?? 'blank',
|
|
726
|
-
wizardStep: contract.wizardStep ?? 0,
|
|
727
|
-
parties: contract.parties.length
|
|
728
|
-
? contract.parties.map((party) => ({
|
|
729
|
-
displayName: party.displayName ?? '',
|
|
730
|
-
partyRole: party.partyRole ?? 'client',
|
|
731
|
-
partyType: party.partyType ?? 'company',
|
|
732
|
-
documentNumber: party.documentNumber ?? '',
|
|
733
|
-
email: party.email ?? '',
|
|
734
|
-
phone: party.phone ?? '',
|
|
735
|
-
isPrimary: party.isPrimary ?? false,
|
|
736
|
-
personId: null,
|
|
737
|
-
availableDocuments: [],
|
|
738
|
-
availableEmails: [],
|
|
739
|
-
availablePhones: [],
|
|
740
|
-
documentMode: 'new',
|
|
741
|
-
emailMode: 'new',
|
|
742
|
-
phoneMode: 'new',
|
|
743
|
-
}))
|
|
744
|
-
: [emptyParty()],
|
|
745
|
-
signatures: contract.signatures.length
|
|
746
|
-
? contract.signatures.map((signature) => ({
|
|
747
|
-
signerName: signature.signerName ?? '',
|
|
748
|
-
signerRole: signature.signerRole ?? '',
|
|
749
|
-
signerEmail: signature.signerEmail ?? '',
|
|
750
|
-
status: signature.status ?? 'pending',
|
|
751
|
-
signedAt: signature.signedAt ?? '',
|
|
752
|
-
}))
|
|
753
|
-
: [emptySignature()],
|
|
754
|
-
financialTerms: contract.financialTerms.length
|
|
755
|
-
? contract.financialTerms.map((term) => ({
|
|
756
|
-
label: term.label ?? '',
|
|
757
|
-
termType: term.termType ?? 'value',
|
|
758
|
-
amount:
|
|
759
|
-
term.amount !== null && term.amount !== undefined
|
|
760
|
-
? String(term.amount)
|
|
761
|
-
: '',
|
|
762
|
-
recurrence: term.recurrence ?? 'one_time',
|
|
763
|
-
dueDay:
|
|
764
|
-
term.dueDay !== null && term.dueDay !== undefined
|
|
765
|
-
? String(term.dueDay)
|
|
766
|
-
: '',
|
|
767
|
-
notes: term.notes ?? '',
|
|
768
|
-
}))
|
|
769
|
-
: [emptyFinancialTerm()],
|
|
770
|
-
revisions: contract.revisions.length
|
|
771
|
-
? contract.revisions.map((revision) => ({
|
|
772
|
-
title: revision.title ?? '',
|
|
773
|
-
revisionType: revision.revisionType ?? 'revision',
|
|
774
|
-
effectiveDate: revision.effectiveDate ?? '',
|
|
775
|
-
status: revision.status ?? 'draft',
|
|
776
|
-
summary: revision.summary ?? '',
|
|
777
|
-
}))
|
|
778
|
-
: [],
|
|
779
|
-
sourceDocument: sourceDocument
|
|
780
|
-
? {
|
|
781
|
-
id: sourceDocument.id,
|
|
782
|
-
fileId: sourceDocument.fileId ?? null,
|
|
783
|
-
fileName: sourceDocument.fileName,
|
|
784
|
-
mimeType: sourceDocument.mimeType,
|
|
785
|
-
extractionStatus: sourceDocument.extractionStatus ?? null,
|
|
786
|
-
extractionSummary: sourceDocument.extractionSummary ?? null,
|
|
787
|
-
}
|
|
788
|
-
: null,
|
|
789
|
-
generatedPdfDocument: generatedPdfDocument
|
|
790
|
-
? {
|
|
791
|
-
id: generatedPdfDocument.id,
|
|
792
|
-
fileId: generatedPdfDocument.fileId ?? null,
|
|
793
|
-
fileName: generatedPdfDocument.fileName,
|
|
794
|
-
mimeType: generatedPdfDocument.mimeType,
|
|
795
|
-
extractionStatus: generatedPdfDocument.extractionStatus ?? null,
|
|
796
|
-
extractionSummary: generatedPdfDocument.extractionSummary ?? null,
|
|
797
|
-
}
|
|
798
|
-
: null,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function normalizeAiReview(
|
|
803
|
-
review?: Partial<ContractAiReviewState> | null
|
|
804
|
-
): ContractAiReviewState | null {
|
|
805
|
-
if (!review) {
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
summary: typeof review.summary === 'string' ? review.summary : undefined,
|
|
811
|
-
missingFields: Array.isArray(review.missingFields)
|
|
812
|
-
? review.missingFields.filter(Boolean).map(String)
|
|
813
|
-
: [],
|
|
814
|
-
warnings: Array.isArray(review.warnings)
|
|
815
|
-
? review.warnings.filter(Boolean).map(String)
|
|
816
|
-
: [],
|
|
817
|
-
checklist: Array.isArray(review.checklist)
|
|
818
|
-
? review.checklist.filter(Boolean).map(String)
|
|
819
|
-
: [],
|
|
820
|
-
status: typeof review.status === 'string' ? review.status : undefined,
|
|
821
|
-
reviewedAt:
|
|
822
|
-
typeof review.reviewedAt === 'string' ? review.reviewedAt : undefined,
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function parseStoredAiReview(
|
|
827
|
-
history?: OperationsContractDetails['history']
|
|
828
|
-
): ContractAiReviewState | null {
|
|
829
|
-
if (!Array.isArray(history)) {
|
|
830
|
-
return null;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
for (const item of history) {
|
|
834
|
-
if (item.action !== 'legal_reviewed' || !item.metadataJson) {
|
|
835
|
-
continue;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
try {
|
|
839
|
-
const parsed = JSON.parse(
|
|
840
|
-
item.metadataJson
|
|
841
|
-
) as Partial<ContractAiReviewState>;
|
|
842
|
-
return normalizeAiReview({
|
|
843
|
-
...parsed,
|
|
844
|
-
reviewedAt:
|
|
845
|
-
typeof parsed.reviewedAt === 'string'
|
|
846
|
-
? parsed.reviewedAt
|
|
847
|
-
: item.createdAt,
|
|
848
|
-
});
|
|
849
|
-
} catch {
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
return null;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function mergeExtractedDraft(
|
|
858
|
-
current: ContractWizardState,
|
|
859
|
-
draft: ExtractedDraft
|
|
860
|
-
): ContractWizardState {
|
|
861
|
-
const next = { ...current };
|
|
862
|
-
const assignIfEmpty = (
|
|
863
|
-
key: keyof ContractWizardState,
|
|
864
|
-
value: unknown,
|
|
865
|
-
fallback?: string
|
|
866
|
-
) => {
|
|
867
|
-
const normalized = toTextValue(value || fallback).trim();
|
|
868
|
-
if (!normalized) return;
|
|
869
|
-
if (!toTextValue(next[key]).trim()) {
|
|
870
|
-
(next[key] as string) = normalized;
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
assignIfEmpty('code', draft.code);
|
|
875
|
-
assignIfEmpty('name', draft.name);
|
|
876
|
-
assignIfEmpty('clientName', draft.clientName);
|
|
877
|
-
assignIfEmpty('startDate', draft.startDate);
|
|
878
|
-
assignIfEmpty('endDate', draft.endDate);
|
|
879
|
-
assignIfEmpty('signedAt', draft.signedAt);
|
|
880
|
-
assignIfEmpty('effectiveDate', draft.effectiveDate);
|
|
881
|
-
assignIfEmpty('budgetAmount', draft.budgetAmount);
|
|
882
|
-
assignIfEmpty('monthlyHourCap', draft.monthlyHourCap);
|
|
883
|
-
assignIfEmpty('description', draft.description, draft.summary);
|
|
884
|
-
|
|
885
|
-
if (!next.contentHtml.trim() && toTextValue(draft.contentHtml).trim()) {
|
|
886
|
-
next.contentHtml = toTextValue(draft.contentHtml).trim();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
next.contractCategory = draft.contractCategory || next.contractCategory;
|
|
890
|
-
next.contractType = draft.contractType || next.contractType;
|
|
891
|
-
next.signatureStatus = draft.signatureStatus || next.signatureStatus;
|
|
892
|
-
next.billingModel = draft.billingModel || next.billingModel;
|
|
893
|
-
next.status = draft.status || next.status;
|
|
894
|
-
next.originType = draft.originType || next.originType;
|
|
895
|
-
next.isActive = draft.isActive ?? next.isActive;
|
|
896
|
-
|
|
897
|
-
const extractedParties = Array.isArray(draft.parties)
|
|
898
|
-
? draft.parties
|
|
899
|
-
.map((party) => {
|
|
900
|
-
const value = asRecord(party);
|
|
901
|
-
return {
|
|
902
|
-
displayName: toTextValue(value.displayName),
|
|
903
|
-
partyRole: toTextValue(value.partyRole || 'client'),
|
|
904
|
-
partyType: toTextValue(value.partyType || 'company'),
|
|
905
|
-
documentNumber: toTextValue(value.documentNumber),
|
|
906
|
-
email: toTextValue(value.email),
|
|
907
|
-
phone: toTextValue(value.phone),
|
|
908
|
-
isPrimary: Boolean(value.isPrimary),
|
|
909
|
-
personId: null,
|
|
910
|
-
availableDocuments: [],
|
|
911
|
-
availableEmails: [],
|
|
912
|
-
availablePhones: [],
|
|
913
|
-
documentMode: 'new' as const,
|
|
914
|
-
emailMode: 'new' as const,
|
|
915
|
-
phoneMode: 'new' as const,
|
|
916
|
-
};
|
|
917
|
-
})
|
|
918
|
-
.filter((party) => party.displayName)
|
|
919
|
-
: [];
|
|
920
|
-
|
|
921
|
-
if (
|
|
922
|
-
extractedParties.length &&
|
|
923
|
-
current.parties.every((party) => !party.displayName.trim())
|
|
924
|
-
) {
|
|
925
|
-
next.parties = extractedParties;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const extractedSignatures = Array.isArray(draft.signatures)
|
|
929
|
-
? draft.signatures
|
|
930
|
-
.map((signature) => {
|
|
931
|
-
const value = asRecord(signature);
|
|
932
|
-
return {
|
|
933
|
-
signerName: toTextValue(value.signerName),
|
|
934
|
-
signerRole: toTextValue(value.signerRole),
|
|
935
|
-
signerEmail: toTextValue(value.signerEmail),
|
|
936
|
-
status: toTextValue(value.status || 'pending'),
|
|
937
|
-
signedAt: toTextValue(value.signedAt),
|
|
938
|
-
};
|
|
939
|
-
})
|
|
940
|
-
.filter((signature) => signature.signerName)
|
|
941
|
-
: [];
|
|
942
|
-
|
|
943
|
-
if (
|
|
944
|
-
extractedSignatures.length &&
|
|
945
|
-
current.signatures.every((signature) => !signature.signerName.trim())
|
|
946
|
-
) {
|
|
947
|
-
next.signatures = extractedSignatures;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const extractedTerms = Array.isArray(draft.financialTerms)
|
|
951
|
-
? draft.financialTerms
|
|
952
|
-
.map((term) => {
|
|
953
|
-
const value = asRecord(term);
|
|
954
|
-
return {
|
|
955
|
-
label: toTextValue(value.label),
|
|
956
|
-
termType: toTextValue(value.termType || 'value'),
|
|
957
|
-
amount: toTextValue(value.amount),
|
|
958
|
-
recurrence: toTextValue(value.recurrence || 'one_time'),
|
|
959
|
-
dueDay: toTextValue(value.dueDay),
|
|
960
|
-
notes: toTextValue(value.notes),
|
|
961
|
-
};
|
|
962
|
-
})
|
|
963
|
-
.filter((term) => term.label)
|
|
964
|
-
: [];
|
|
965
|
-
|
|
966
|
-
if (
|
|
967
|
-
extractedTerms.length &&
|
|
968
|
-
current.financialTerms.every((term) => !term.label.trim())
|
|
969
|
-
) {
|
|
970
|
-
next.financialTerms = extractedTerms;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
return next;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
export function ContractWizardSheet({
|
|
977
|
-
contractId,
|
|
978
|
-
duplicateFromId,
|
|
979
|
-
initialTemplateId,
|
|
980
|
-
isCreateFlow = false,
|
|
981
|
-
onCancel,
|
|
982
|
-
onSaved,
|
|
983
|
-
}: ContractWizardSheetProps) {
|
|
984
|
-
const t = useTranslations('operations.ContractFormPage');
|
|
985
|
-
const { request, showToastHandler, currentLocaleCode, getSettingValue } =
|
|
986
|
-
useApp();
|
|
987
|
-
const wizardSteps = isCreateFlow ? createFlowSteps : allSteps;
|
|
988
|
-
const contentStepIndex = wizardSteps.findIndex(
|
|
989
|
-
(step) => step.key === 'content'
|
|
990
|
-
);
|
|
991
|
-
const reviewStepIndex = wizardSteps.findIndex(
|
|
992
|
-
(step) => step.key === 'review'
|
|
993
|
-
);
|
|
994
|
-
const defaultEditStep =
|
|
995
|
-
contentStepIndex >= 0 ? contentStepIndex : reviewStepIndex;
|
|
996
|
-
const [form, setForm] = useState<ContractWizardState>(buildEmptyForm());
|
|
997
|
-
const [draftId, setDraftId] = useState<number | null>(contractId ?? null);
|
|
998
|
-
const [activeStep, setActiveStep] = useState(
|
|
999
|
-
contractId ? defaultEditStep : 0
|
|
1000
|
-
);
|
|
1001
|
-
const [creationMode, setCreationMode] = useState<CreationMode>(
|
|
1002
|
-
duplicateFromId ? 'duplicate' : initialTemplateId ? 'template' : 'blank'
|
|
1003
|
-
);
|
|
1004
|
-
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
|
1005
|
-
initialTemplateId ?? null
|
|
1006
|
-
);
|
|
1007
|
-
const [selectedDuplicateId, setSelectedDuplicateId] = useState<number | null>(
|
|
1008
|
-
duplicateFromId ?? null
|
|
1009
|
-
);
|
|
1010
|
-
const [sourceFile, setSourceFile] = useState<File | null>(null);
|
|
1011
|
-
const [aiStartPrompt, setAiStartPrompt] = useState('');
|
|
1012
|
-
const [isBusy, setIsBusy] = useState(false);
|
|
1013
|
-
const [isExtracting, setIsExtracting] = useState(false);
|
|
1014
|
-
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
|
1015
|
-
const [aiReview, setAiReview] = useState<ContractAiReviewState | null>(null);
|
|
1016
|
-
const autoStartedRef = useRef(false);
|
|
1017
|
-
|
|
1018
|
-
const { data: contractTemplates = [] } = useQuery<
|
|
1019
|
-
OperationsContractTemplate[]
|
|
1020
|
-
>({
|
|
1021
|
-
queryKey: ['operations-contract-wizard-templates', currentLocaleCode],
|
|
1022
|
-
queryFn: () =>
|
|
1023
|
-
fetchOperations<OperationsContractTemplate[]>(
|
|
1024
|
-
request,
|
|
1025
|
-
'/operations/contract-templates'
|
|
1026
|
-
),
|
|
1027
|
-
placeholderData: (old) => old ?? [],
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
|
|
1031
|
-
queryKey: ['operations-contract-wizard-collaborators', currentLocaleCode],
|
|
1032
|
-
queryFn: () =>
|
|
1033
|
-
fetchOperations<OperationsCollaborator[]>(
|
|
1034
|
-
request,
|
|
1035
|
-
'/operations/collaborators'
|
|
1036
|
-
),
|
|
1037
|
-
placeholderData: (old) => old ?? [],
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
const { data: contracts = [] } = useQuery<OperationsContract[]>({
|
|
1041
|
-
queryKey: ['operations-contract-wizard-contracts', currentLocaleCode],
|
|
1042
|
-
queryFn: () =>
|
|
1043
|
-
fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
|
|
1044
|
-
placeholderData: (old) => old ?? [],
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
const { data: initialContract } = useQuery<OperationsContractDetails>({
|
|
1048
|
-
queryKey: [
|
|
1049
|
-
'operations-contract-wizard-detail',
|
|
1050
|
-
currentLocaleCode,
|
|
1051
|
-
contractId,
|
|
1052
|
-
],
|
|
1053
|
-
enabled: Boolean(contractId),
|
|
1054
|
-
queryFn: () =>
|
|
1055
|
-
fetchOperations<OperationsContractDetails>(
|
|
1056
|
-
request,
|
|
1057
|
-
`/operations/contracts/${contractId}`
|
|
1058
|
-
),
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
const { data: personContactTypes = [] } = useQuery<ContactTypeLookup[]>({
|
|
1062
|
-
queryKey: ['operations-contract-person-contact-types', currentLocaleCode],
|
|
1063
|
-
queryFn: async () => {
|
|
1064
|
-
const response = await request<{ data?: ContactTypeLookup[] }>({
|
|
1065
|
-
url: '/person-contact-type?pageSize=100',
|
|
1066
|
-
method: 'GET',
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
return Array.isArray(response?.data?.data) ? response.data.data : [];
|
|
1070
|
-
},
|
|
1071
|
-
placeholderData: (old) => old ?? [],
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
const { data: personDocumentTypes = [] } = useQuery<DocumentTypeLookup[]>({
|
|
1075
|
-
queryKey: ['operations-contract-person-document-types', currentLocaleCode],
|
|
1076
|
-
queryFn: async () => {
|
|
1077
|
-
const response = await request<{ data?: DocumentTypeLookup[] }>({
|
|
1078
|
-
url: '/person-document-type?pageSize=100',
|
|
1079
|
-
method: 'GET',
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
return Array.isArray(response?.data?.data) ? response.data.data : [];
|
|
1083
|
-
},
|
|
1084
|
-
placeholderData: (old) => old ?? [],
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
useEffect(() => {
|
|
1088
|
-
if (!initialContract) return;
|
|
1089
|
-
setDraftId(initialContract.id);
|
|
1090
|
-
setCreationMode((initialContract.creationMode as CreationMode) ?? 'blank');
|
|
1091
|
-
setForm((current) =>
|
|
1092
|
-
preservePartySelectionMetadata(toFormState(initialContract), current)
|
|
1093
|
-
);
|
|
1094
|
-
setAiReview(parseStoredAiReview(initialContract.history));
|
|
1095
|
-
setActiveStep(defaultEditStep);
|
|
1096
|
-
}, [defaultEditStep, initialContract]);
|
|
1097
|
-
|
|
1098
|
-
const hasOpenAi = Boolean(getSettingValue('ai-openai-api-key-enabled'));
|
|
1099
|
-
const hasGemini = Boolean(getSettingValue('ai-gemini-api-key-enabled'));
|
|
1100
|
-
const aiProvider = hasOpenAi ? 'openai' : hasGemini ? 'gemini' : null;
|
|
1101
|
-
const aiProviderLabel = aiProvider === 'openai' ? 'OpenAI' : 'Gemini';
|
|
1102
|
-
|
|
1103
|
-
useEffect(() => {
|
|
1104
|
-
if (!aiProvider && creationMode === 'ai_prompt') {
|
|
1105
|
-
setCreationMode('blank');
|
|
1106
|
-
}
|
|
1107
|
-
}, [aiProvider, creationMode]);
|
|
1108
|
-
|
|
1109
|
-
const getOptionLabel = (group: string, value?: string | null) => {
|
|
1110
|
-
if (!value) {
|
|
1111
|
-
return '-';
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
const key = `options.${group}.${value}`;
|
|
1115
|
-
return t.has(key) ? t(key) : formatEnumLabel(value);
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
|
-
const selectedTemplate =
|
|
1119
|
-
contractTemplates.find((item) => item.id === selectedTemplateId) ?? null;
|
|
1120
|
-
const primaryPartyIndex = getPrimaryPartyIndex(form.parties);
|
|
1121
|
-
const primaryParty = form.parties[primaryPartyIndex] ?? null;
|
|
1122
|
-
|
|
1123
|
-
const updatePartyAt = (
|
|
1124
|
-
index: number,
|
|
1125
|
-
updater: (party: PartyState) => PartyState,
|
|
1126
|
-
options?: { syncClientName?: boolean }
|
|
1127
|
-
) => {
|
|
1128
|
-
setForm((current) => {
|
|
1129
|
-
const nextParties = current.parties.map((item, itemIndex) =>
|
|
1130
|
-
itemIndex === index ? updater(item) : item
|
|
1131
|
-
);
|
|
1132
|
-
const nextPrimaryParty = nextParties[getPrimaryPartyIndex(nextParties)];
|
|
1133
|
-
|
|
1134
|
-
return {
|
|
1135
|
-
...current,
|
|
1136
|
-
clientName:
|
|
1137
|
-
options?.syncClientName && nextPrimaryParty?.displayName
|
|
1138
|
-
? nextPrimaryParty.displayName
|
|
1139
|
-
: current.clientName,
|
|
1140
|
-
parties: nextParties,
|
|
1141
|
-
};
|
|
1142
|
-
});
|
|
1143
|
-
};
|
|
1144
|
-
|
|
1145
|
-
const handlePartyPersonChange = async (
|
|
1146
|
-
index: number,
|
|
1147
|
-
personId: number | null,
|
|
1148
|
-
personName: string,
|
|
1149
|
-
options?: { syncClientName?: boolean; markAsPrimary?: boolean }
|
|
1150
|
-
) => {
|
|
1151
|
-
const syncClientName = Boolean(options?.syncClientName);
|
|
1152
|
-
const markAsPrimary = Boolean(options?.markAsPrimary);
|
|
1153
|
-
|
|
1154
|
-
if (!personId) {
|
|
1155
|
-
setForm((current) => ({
|
|
1156
|
-
...current,
|
|
1157
|
-
clientName: syncClientName ? personName : current.clientName,
|
|
1158
|
-
parties: current.parties.map((item, itemIndex) => {
|
|
1159
|
-
if (itemIndex !== index) {
|
|
1160
|
-
return markAsPrimary ? { ...item, isPrimary: false } : item;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return {
|
|
1164
|
-
...clearPersonDetailsFromParty(item, personName),
|
|
1165
|
-
isPrimary: markAsPrimary ? true : item.isPrimary,
|
|
1166
|
-
};
|
|
1167
|
-
}),
|
|
1168
|
-
}));
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
try {
|
|
1173
|
-
const person = await fetchOperations<PersonDetailsRecord>(
|
|
1174
|
-
request,
|
|
1175
|
-
`/person/${personId}`
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
setForm((current) => ({
|
|
1179
|
-
...current,
|
|
1180
|
-
clientName: syncClientName
|
|
1181
|
-
? personName || person.name || current.clientName
|
|
1182
|
-
: current.clientName,
|
|
1183
|
-
parties: current.parties.map((item, itemIndex) => {
|
|
1184
|
-
if (itemIndex !== index) {
|
|
1185
|
-
return markAsPrimary ? { ...item, isPrimary: false } : item;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return {
|
|
1189
|
-
...applyPersonDetailsToParty(item, personId, personName, person),
|
|
1190
|
-
isPrimary: markAsPrimary ? true : item.isPrimary,
|
|
1191
|
-
};
|
|
1192
|
-
}),
|
|
1193
|
-
}));
|
|
1194
|
-
} catch {
|
|
1195
|
-
setForm((current) => ({
|
|
1196
|
-
...current,
|
|
1197
|
-
clientName: syncClientName ? personName : current.clientName,
|
|
1198
|
-
parties: current.parties.map((item, itemIndex) => {
|
|
1199
|
-
if (itemIndex !== index) {
|
|
1200
|
-
return markAsPrimary ? { ...item, isPrimary: false } : item;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
return {
|
|
1204
|
-
...item,
|
|
1205
|
-
displayName: personName,
|
|
1206
|
-
personId,
|
|
1207
|
-
availableDocuments: [],
|
|
1208
|
-
availableEmails: [],
|
|
1209
|
-
availablePhones: [],
|
|
1210
|
-
documentMode: 'new',
|
|
1211
|
-
emailMode: 'new',
|
|
1212
|
-
phoneMode: 'new',
|
|
1213
|
-
isPrimary: markAsPrimary ? true : item.isPrimary,
|
|
1214
|
-
};
|
|
1215
|
-
}),
|
|
1216
|
-
}));
|
|
1217
|
-
showToastHandler?.(
|
|
1218
|
-
'error',
|
|
1219
|
-
'Nao foi possivel carregar os documentos, e-mails e telefones da pessoa selecionada.'
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
const syncSelectedPeopleDetails = async () => {
|
|
1225
|
-
const pendingByPerson = new Map<
|
|
1226
|
-
number,
|
|
1227
|
-
{
|
|
1228
|
-
documents: Set<string>;
|
|
1229
|
-
emails: Set<string>;
|
|
1230
|
-
phones: Set<string>;
|
|
1231
|
-
}
|
|
1232
|
-
>();
|
|
1233
|
-
|
|
1234
|
-
for (const party of form.parties) {
|
|
1235
|
-
if (!party.personId) {
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
const nextDocument = trimToNull(party.documentNumber);
|
|
1240
|
-
const nextEmail = trimToNull(party.email);
|
|
1241
|
-
const nextPhone = trimToNull(party.phone);
|
|
1242
|
-
const hasPendingValues =
|
|
1243
|
-
(party.documentMode === 'new' && nextDocument) ||
|
|
1244
|
-
(party.emailMode === 'new' && nextEmail) ||
|
|
1245
|
-
(party.phoneMode === 'new' && nextPhone);
|
|
1246
|
-
|
|
1247
|
-
if (!hasPendingValues) {
|
|
1248
|
-
continue;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
const bucket = pendingByPerson.get(party.personId) ?? {
|
|
1252
|
-
documents: new Set<string>(),
|
|
1253
|
-
emails: new Set<string>(),
|
|
1254
|
-
phones: new Set<string>(),
|
|
1255
|
-
};
|
|
1256
|
-
|
|
1257
|
-
if (party.documentMode === 'new' && nextDocument) {
|
|
1258
|
-
bucket.documents.add(nextDocument);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
if (party.emailMode === 'new' && nextEmail) {
|
|
1262
|
-
bucket.emails.add(nextEmail);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
if (party.phoneMode === 'new' && nextPhone) {
|
|
1266
|
-
bucket.phones.add(nextPhone);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
pendingByPerson.set(party.personId, bucket);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (!pendingByPerson.size) {
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const resolveContactTypeId = (
|
|
1277
|
-
preferredCodes: string[],
|
|
1278
|
-
fallbackId: number
|
|
1279
|
-
) => {
|
|
1280
|
-
for (const preferredCode of preferredCodes) {
|
|
1281
|
-
const found = personContactTypes.find(
|
|
1282
|
-
(item) => String(item.code).toUpperCase() === preferredCode
|
|
1283
|
-
);
|
|
1284
|
-
|
|
1285
|
-
if (found?.contact_type_id) {
|
|
1286
|
-
return found.contact_type_id;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
return fallbackId;
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
const emailTypeId = resolveContactTypeId(['EMAIL'], EMAIL_CONTACT_TYPE_ID);
|
|
1294
|
-
const phoneTypeId = resolveContactTypeId(
|
|
1295
|
-
['PHONE', 'WHATSAPP', 'TELEGRAM', 'FAX', 'MOBILE'],
|
|
1296
|
-
1
|
|
1297
|
-
);
|
|
1298
|
-
|
|
1299
|
-
for (const [personId, pending] of pendingByPerson.entries()) {
|
|
1300
|
-
const person = await fetchOperations<PersonDetailsRecord>(
|
|
1301
|
-
request,
|
|
1302
|
-
`/person/${personId}`
|
|
1303
|
-
);
|
|
1304
|
-
|
|
1305
|
-
const contacts: Array<{
|
|
1306
|
-
id?: number;
|
|
1307
|
-
value: string;
|
|
1308
|
-
is_primary: boolean;
|
|
1309
|
-
contact_type_id: number;
|
|
1310
|
-
}> = (person.contact ?? [])
|
|
1311
|
-
.map((contact) => ({
|
|
1312
|
-
id: contact.id,
|
|
1313
|
-
value: String(contact.value ?? '').trim(),
|
|
1314
|
-
is_primary: Boolean(contact.is_primary),
|
|
1315
|
-
contact_type_id: Number(
|
|
1316
|
-
contact.contact_type_id ?? contact.contact_type?.id ?? 0
|
|
1317
|
-
),
|
|
1318
|
-
}))
|
|
1319
|
-
.filter((contact) => contact.value && contact.contact_type_id > 0);
|
|
1320
|
-
|
|
1321
|
-
const documents: Array<{
|
|
1322
|
-
id?: number;
|
|
1323
|
-
value: string;
|
|
1324
|
-
document_type_id: number;
|
|
1325
|
-
}> = (person.document ?? [])
|
|
1326
|
-
.map((document) => ({
|
|
1327
|
-
id: document.id,
|
|
1328
|
-
value: String(document.value ?? '').trim(),
|
|
1329
|
-
document_type_id: Number(
|
|
1330
|
-
document.document_type_id ?? document.document_type?.id ?? 0
|
|
1331
|
-
),
|
|
1332
|
-
}))
|
|
1333
|
-
.filter((document) => document.value && document.document_type_id > 0);
|
|
1334
|
-
|
|
1335
|
-
const addresses = (person.address ?? []).map((address) => ({
|
|
1336
|
-
id: address.id,
|
|
1337
|
-
line1: String(address.line1 ?? '').trim(),
|
|
1338
|
-
line2: trimToNull(address.line2 ?? null),
|
|
1339
|
-
city: String(address.city ?? '').trim(),
|
|
1340
|
-
state: String(address.state ?? '').trim(),
|
|
1341
|
-
country_code: trimToNull(address.country_code ?? null),
|
|
1342
|
-
postal_code: trimToNull(address.postal_code ?? null),
|
|
1343
|
-
is_primary: Boolean(address.is_primary),
|
|
1344
|
-
address_type: String(address.address_type ?? 'residential'),
|
|
1345
|
-
}));
|
|
1346
|
-
|
|
1347
|
-
const existingEmails = new Set(
|
|
1348
|
-
contacts
|
|
1349
|
-
.filter((contact) => contact.contact_type_id === emailTypeId)
|
|
1350
|
-
.map((contact) => normalizePartyFieldValue('email', contact.value))
|
|
1351
|
-
);
|
|
1352
|
-
const existingPhones = new Set(
|
|
1353
|
-
contacts
|
|
1354
|
-
.filter((contact) => contact.contact_type_id !== emailTypeId)
|
|
1355
|
-
.map((contact) => normalizePartyFieldValue('phone', contact.value))
|
|
1356
|
-
);
|
|
1357
|
-
const existingDocuments = new Set(
|
|
1358
|
-
documents.map((document) =>
|
|
1359
|
-
normalizePartyFieldValue('document', document.value)
|
|
1360
|
-
)
|
|
1361
|
-
);
|
|
1362
|
-
|
|
1363
|
-
let hasChanges = false;
|
|
1364
|
-
|
|
1365
|
-
for (const email of pending.emails) {
|
|
1366
|
-
const normalizedEmail = normalizePartyFieldValue('email', email);
|
|
1367
|
-
if (!normalizedEmail || existingEmails.has(normalizedEmail)) {
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
contacts.push({
|
|
1372
|
-
value: email.trim(),
|
|
1373
|
-
is_primary: !contacts.some(
|
|
1374
|
-
(contact) =>
|
|
1375
|
-
contact.contact_type_id === emailTypeId && contact.is_primary
|
|
1376
|
-
),
|
|
1377
|
-
contact_type_id: emailTypeId,
|
|
1378
|
-
});
|
|
1379
|
-
existingEmails.add(normalizedEmail);
|
|
1380
|
-
hasChanges = true;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
for (const phone of pending.phones) {
|
|
1384
|
-
const normalizedPhone = normalizePartyFieldValue('phone', phone);
|
|
1385
|
-
if (!normalizedPhone || existingPhones.has(normalizedPhone)) {
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
contacts.push({
|
|
1390
|
-
value: phone.trim(),
|
|
1391
|
-
is_primary: !contacts.some(
|
|
1392
|
-
(contact) =>
|
|
1393
|
-
contact.contact_type_id === phoneTypeId && contact.is_primary
|
|
1394
|
-
),
|
|
1395
|
-
contact_type_id: phoneTypeId,
|
|
1396
|
-
});
|
|
1397
|
-
existingPhones.add(normalizedPhone);
|
|
1398
|
-
hasChanges = true;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
if (pending.documents.size) {
|
|
1402
|
-
const defaultDocumentTypeId =
|
|
1403
|
-
documents[0]?.document_type_id ||
|
|
1404
|
-
personDocumentTypes.find(
|
|
1405
|
-
(item) =>
|
|
1406
|
-
String(item.code).toUpperCase() ===
|
|
1407
|
-
(person.type === 'company' ? 'CNPJ' : 'CPF')
|
|
1408
|
-
)?.document_type_id ||
|
|
1409
|
-
personDocumentTypes[0]?.document_type_id;
|
|
1410
|
-
|
|
1411
|
-
if (!defaultDocumentTypeId) {
|
|
1412
|
-
throw new Error('Missing person document type lookup');
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
for (const documentValue of pending.documents) {
|
|
1416
|
-
const normalizedDocument = normalizePartyFieldValue(
|
|
1417
|
-
'document',
|
|
1418
|
-
documentValue
|
|
1419
|
-
);
|
|
1420
|
-
if (
|
|
1421
|
-
!normalizedDocument ||
|
|
1422
|
-
existingDocuments.has(normalizedDocument)
|
|
1423
|
-
) {
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
documents.push({
|
|
1428
|
-
value: documentValue.trim(),
|
|
1429
|
-
document_type_id: defaultDocumentTypeId,
|
|
1430
|
-
});
|
|
1431
|
-
existingDocuments.add(normalizedDocument);
|
|
1432
|
-
hasChanges = true;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (!hasChanges) {
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
await request({
|
|
1441
|
-
url: `/person/${personId}`,
|
|
1442
|
-
method: 'PATCH',
|
|
1443
|
-
data: {
|
|
1444
|
-
name: person.name,
|
|
1445
|
-
type: person.type ?? 'individual',
|
|
1446
|
-
status: person.status ?? 'active',
|
|
1447
|
-
contacts,
|
|
1448
|
-
documents,
|
|
1449
|
-
addresses,
|
|
1450
|
-
},
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
const reloadContract = useCallback(
|
|
1456
|
-
async (nextId: number) => {
|
|
1457
|
-
const detail = await fetchOperations<OperationsContractDetails>(
|
|
1458
|
-
request,
|
|
1459
|
-
`/operations/contracts/${nextId}`
|
|
1460
|
-
);
|
|
1461
|
-
setDraftId(detail.id);
|
|
1462
|
-
setCreationMode(
|
|
1463
|
-
creationMode === 'ai_prompt'
|
|
1464
|
-
? 'ai_prompt'
|
|
1465
|
-
: ((detail.creationMode as CreationMode) ?? creationMode)
|
|
1466
|
-
);
|
|
1467
|
-
setForm((current) =>
|
|
1468
|
-
preservePartySelectionMetadata(toFormState(detail), current)
|
|
1469
|
-
);
|
|
1470
|
-
setAiReview(parseStoredAiReview(detail.history));
|
|
1471
|
-
return detail;
|
|
1472
|
-
},
|
|
1473
|
-
[creationMode, request]
|
|
1474
|
-
);
|
|
1475
|
-
|
|
1476
|
-
const uploadSourceDocument = useCallback(
|
|
1477
|
-
async (file: File) => {
|
|
1478
|
-
const formData = new FormData();
|
|
1479
|
-
formData.append('file', file);
|
|
1480
|
-
formData.append('destination', 'operations/contracts/source');
|
|
1481
|
-
|
|
1482
|
-
const response = await request<{ id?: number }>({
|
|
1483
|
-
url: '/file',
|
|
1484
|
-
method: 'POST',
|
|
1485
|
-
data: formData,
|
|
1486
|
-
headers: {
|
|
1487
|
-
'Content-Type': 'multipart/form-data',
|
|
1488
|
-
},
|
|
1489
|
-
} as never);
|
|
1490
|
-
|
|
1491
|
-
const nextFileId = Number(response?.data?.id);
|
|
1492
|
-
if (!nextFileId) {
|
|
1493
|
-
throw new Error('Nao foi possivel enviar o arquivo.');
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
return {
|
|
1497
|
-
fileId: nextFileId,
|
|
1498
|
-
fileName: file.name,
|
|
1499
|
-
mimeType: file.type || 'application/pdf',
|
|
1500
|
-
};
|
|
1501
|
-
},
|
|
1502
|
-
[request]
|
|
1503
|
-
);
|
|
1504
|
-
|
|
1505
|
-
const startDraft = useCallback(async () => {
|
|
1506
|
-
if (draftId) return draftId;
|
|
1507
|
-
|
|
1508
|
-
setIsBusy(true);
|
|
1509
|
-
try {
|
|
1510
|
-
let sourcePayload:
|
|
1511
|
-
| { fileId: number; fileName: string; mimeType: string }
|
|
1512
|
-
| undefined;
|
|
1513
|
-
|
|
1514
|
-
if (creationMode === 'upload') {
|
|
1515
|
-
if (!sourceFile) {
|
|
1516
|
-
showToastHandler?.(
|
|
1517
|
-
'error',
|
|
1518
|
-
'Selecione um PDF ou Word para continuar.'
|
|
1519
|
-
);
|
|
1520
|
-
return null;
|
|
1521
|
-
}
|
|
1522
|
-
sourcePayload = await uploadSourceDocument(sourceFile);
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
if (creationMode === 'ai_prompt' && !aiStartPrompt.trim()) {
|
|
1526
|
-
showToastHandler?.('error', 'Descreva o contrato para continuar.');
|
|
1527
|
-
return null;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
if (creationMode === 'template' && !selectedTemplateId) {
|
|
1531
|
-
showToastHandler?.('error', 'Selecione um template para continuar.');
|
|
1532
|
-
return null;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
if (creationMode === 'duplicate' && !selectedDuplicateId) {
|
|
1536
|
-
showToastHandler?.('error', 'Selecione um contrato para duplicar.');
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
const response = await mutateOperations<OperationsContractDetails>(
|
|
1541
|
-
request,
|
|
1542
|
-
'/operations/contracts/drafts',
|
|
1543
|
-
'POST',
|
|
1544
|
-
{
|
|
1545
|
-
creationMode: creationMode === 'ai_prompt' ? 'blank' : creationMode,
|
|
1546
|
-
templateId: selectedTemplateId ?? null,
|
|
1547
|
-
duplicateFromId: selectedDuplicateId ?? null,
|
|
1548
|
-
sourceFileId: sourcePayload?.fileId ?? null,
|
|
1549
|
-
sourceFileName: sourcePayload?.fileName ?? null,
|
|
1550
|
-
sourceMimeType: sourcePayload?.mimeType ?? null,
|
|
1551
|
-
}
|
|
1552
|
-
);
|
|
1553
|
-
|
|
1554
|
-
setDraftId(response.id);
|
|
1555
|
-
setCreationMode(
|
|
1556
|
-
creationMode === 'ai_prompt'
|
|
1557
|
-
? 'ai_prompt'
|
|
1558
|
-
: ((response.creationMode as CreationMode) ?? creationMode)
|
|
1559
|
-
);
|
|
1560
|
-
setForm((current) =>
|
|
1561
|
-
preservePartySelectionMetadata(toFormState(response), current)
|
|
1562
|
-
);
|
|
1563
|
-
showToastHandler?.('success', 'Rascunho criado com sucesso.');
|
|
1564
|
-
return response.id;
|
|
1565
|
-
} catch {
|
|
1566
|
-
showToastHandler?.('error', 'Nao foi possivel criar o rascunho.');
|
|
1567
|
-
return null;
|
|
1568
|
-
} finally {
|
|
1569
|
-
setIsBusy(false);
|
|
1570
|
-
}
|
|
1571
|
-
}, [
|
|
1572
|
-
aiStartPrompt,
|
|
1573
|
-
creationMode,
|
|
1574
|
-
draftId,
|
|
1575
|
-
request,
|
|
1576
|
-
selectedDuplicateId,
|
|
1577
|
-
selectedTemplateId,
|
|
1578
|
-
showToastHandler,
|
|
1579
|
-
sourceFile,
|
|
1580
|
-
uploadSourceDocument,
|
|
1581
|
-
]);
|
|
1582
|
-
|
|
1583
|
-
useEffect(() => {
|
|
1584
|
-
if (autoStartedRef.current || contractId) return;
|
|
1585
|
-
|
|
1586
|
-
if (duplicateFromId || initialTemplateId) {
|
|
1587
|
-
autoStartedRef.current = true;
|
|
1588
|
-
void startDraft();
|
|
1589
|
-
}
|
|
1590
|
-
}, [contractId, duplicateFromId, initialTemplateId, startDraft]);
|
|
1591
|
-
|
|
1592
|
-
const buildPayload = (step = activeStep) => ({
|
|
1593
|
-
code: trimToNull(form.code)?.toUpperCase() ?? null,
|
|
1594
|
-
name: trimToNull(form.name),
|
|
1595
|
-
clientName: trimToNull(form.clientName),
|
|
1596
|
-
contractCategory: form.contractCategory,
|
|
1597
|
-
contractType: form.contractType,
|
|
1598
|
-
signatureStatus: form.signatureStatus,
|
|
1599
|
-
isActive: form.isActive,
|
|
1600
|
-
billingModel: form.billingModel,
|
|
1601
|
-
accountManagerCollaboratorId:
|
|
1602
|
-
form.accountManagerCollaboratorId === 'none'
|
|
1603
|
-
? null
|
|
1604
|
-
: parseNumberInput(form.accountManagerCollaboratorId),
|
|
1605
|
-
relatedCollaboratorId:
|
|
1606
|
-
form.relatedCollaboratorId === 'none'
|
|
1607
|
-
? null
|
|
1608
|
-
: parseNumberInput(form.relatedCollaboratorId),
|
|
1609
|
-
contractTemplateId:
|
|
1610
|
-
form.contractTemplateId === 'none'
|
|
1611
|
-
? null
|
|
1612
|
-
: parseNumberInput(form.contractTemplateId),
|
|
1613
|
-
originType: form.originType,
|
|
1614
|
-
originId: parseNumberInput(form.originId),
|
|
1615
|
-
startDate: trimToNull(form.startDate),
|
|
1616
|
-
endDate: trimToNull(form.endDate),
|
|
1617
|
-
signedAt: trimToNull(form.signedAt),
|
|
1618
|
-
effectiveDate: trimToNull(form.effectiveDate),
|
|
1619
|
-
budgetAmount: parseNumberInput(form.budgetAmount),
|
|
1620
|
-
monthlyHourCap: parseNumberInput(form.monthlyHourCap),
|
|
1621
|
-
status: form.status,
|
|
1622
|
-
creationMode: creationMode === 'ai_prompt' ? 'blank' : creationMode,
|
|
1623
|
-
wizardStep: step,
|
|
1624
|
-
description: trimToNull(form.description),
|
|
1625
|
-
contentHtml: trimToNull(form.contentHtml),
|
|
1626
|
-
parties: form.parties
|
|
1627
|
-
.filter((party) => party.displayName.trim())
|
|
1628
|
-
.map((party) => ({
|
|
1629
|
-
displayName: party.displayName.trim(),
|
|
1630
|
-
partyRole: party.partyRole,
|
|
1631
|
-
partyType: party.partyType,
|
|
1632
|
-
documentNumber: trimToNull(party.documentNumber),
|
|
1633
|
-
email: trimToNull(party.email),
|
|
1634
|
-
phone: trimToNull(party.phone),
|
|
1635
|
-
isPrimary: party.isPrimary,
|
|
1636
|
-
})),
|
|
1637
|
-
signatures: form.signatures
|
|
1638
|
-
.filter((signature) => signature.signerName.trim())
|
|
1639
|
-
.map((signature) => ({
|
|
1640
|
-
signerName: signature.signerName.trim(),
|
|
1641
|
-
signerRole: trimToNull(signature.signerRole),
|
|
1642
|
-
signerEmail: trimToNull(signature.signerEmail),
|
|
1643
|
-
status: signature.status,
|
|
1644
|
-
signedAt: trimToNull(signature.signedAt),
|
|
1645
|
-
})),
|
|
1646
|
-
financialTerms: form.financialTerms
|
|
1647
|
-
.filter((term) => term.label.trim())
|
|
1648
|
-
.map((term) => ({
|
|
1649
|
-
label: term.label.trim(),
|
|
1650
|
-
termType: term.termType,
|
|
1651
|
-
amount: parseNumberInput(term.amount) ?? 0,
|
|
1652
|
-
recurrence: term.recurrence,
|
|
1653
|
-
dueDay: parseNumberInput(term.dueDay),
|
|
1654
|
-
notes: trimToNull(term.notes),
|
|
1655
|
-
})),
|
|
1656
|
-
revisions: form.revisions
|
|
1657
|
-
.filter((revision) => revision.title.trim())
|
|
1658
|
-
.map((revision) => ({
|
|
1659
|
-
title: revision.title.trim(),
|
|
1660
|
-
revisionType: revision.revisionType,
|
|
1661
|
-
effectiveDate: trimToNull(revision.effectiveDate),
|
|
1662
|
-
status: revision.status,
|
|
1663
|
-
summary: trimToNull(revision.summary),
|
|
1664
|
-
})),
|
|
1665
|
-
});
|
|
1666
|
-
|
|
1667
|
-
const saveDraft = async (step = activeStep) => {
|
|
1668
|
-
const nextDraftId = draftId ?? (await startDraft());
|
|
1669
|
-
if (!nextDraftId) return null;
|
|
1670
|
-
|
|
1671
|
-
setIsBusy(true);
|
|
1672
|
-
try {
|
|
1673
|
-
await syncSelectedPeopleDetails();
|
|
1674
|
-
|
|
1675
|
-
const response = await mutateOperations<OperationsContractDetails>(
|
|
1676
|
-
request,
|
|
1677
|
-
`/operations/contracts/${nextDraftId}`,
|
|
1678
|
-
'PATCH',
|
|
1679
|
-
buildPayload(step)
|
|
1680
|
-
);
|
|
1681
|
-
setDraftId(response.id);
|
|
1682
|
-
setCreationMode(
|
|
1683
|
-
creationMode === 'ai_prompt'
|
|
1684
|
-
? 'ai_prompt'
|
|
1685
|
-
: ((response.creationMode as CreationMode) ?? creationMode)
|
|
1686
|
-
);
|
|
1687
|
-
setForm((current) =>
|
|
1688
|
-
preservePartySelectionMetadata(toFormState(response), current)
|
|
1689
|
-
);
|
|
1690
|
-
showToastHandler?.('success', 'Rascunho salvo.');
|
|
1691
|
-
return response;
|
|
1692
|
-
} catch {
|
|
1693
|
-
showToastHandler?.(
|
|
1694
|
-
'error',
|
|
1695
|
-
'Nao foi possivel salvar o rascunho ou sincronizar os dados da pessoa selecionada.'
|
|
1696
|
-
);
|
|
1697
|
-
return null;
|
|
1698
|
-
} finally {
|
|
1699
|
-
setIsBusy(false);
|
|
1700
|
-
}
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
const handleExtractSource = async () => {
|
|
1704
|
-
const nextDraftId = draftId ?? (await startDraft());
|
|
1705
|
-
if (!nextDraftId) return;
|
|
1706
|
-
if (!aiProvider) {
|
|
1707
|
-
showToastHandler?.(
|
|
1708
|
-
'error',
|
|
1709
|
-
'Configure OpenAI ou Gemini para extrair o contrato.'
|
|
1710
|
-
);
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
setIsExtracting(true);
|
|
1715
|
-
try {
|
|
1716
|
-
const extracted = await mutateOperations<ExtractedDraft>(
|
|
1717
|
-
request,
|
|
1718
|
-
`/operations/contracts/${nextDraftId}/extract-source`,
|
|
1719
|
-
'POST',
|
|
1720
|
-
{
|
|
1721
|
-
provider: aiProvider,
|
|
1722
|
-
promptMessage:
|
|
1723
|
-
'Analyze the attached contract and extract a structured draft for the contract form.',
|
|
1724
|
-
}
|
|
1725
|
-
);
|
|
1726
|
-
setAiReview(
|
|
1727
|
-
normalizeAiReview({
|
|
1728
|
-
summary: extracted.summary,
|
|
1729
|
-
missingFields: Array.isArray(extracted.missingFields)
|
|
1730
|
-
? extracted.missingFields.filter(Boolean)
|
|
1731
|
-
: [],
|
|
1732
|
-
warnings: Array.isArray(extracted.warnings)
|
|
1733
|
-
? extracted.warnings.filter(Boolean)
|
|
1734
|
-
: [],
|
|
1735
|
-
checklist: [],
|
|
1736
|
-
})
|
|
1737
|
-
);
|
|
1738
|
-
setForm((current) => {
|
|
1739
|
-
const merged = mergeExtractedDraft(current, extracted);
|
|
1740
|
-
return {
|
|
1741
|
-
...merged,
|
|
1742
|
-
sourceDocument: merged.sourceDocument
|
|
1743
|
-
? {
|
|
1744
|
-
...merged.sourceDocument,
|
|
1745
|
-
extractionStatus: 'completed',
|
|
1746
|
-
extractionSummary:
|
|
1747
|
-
extracted.summary ||
|
|
1748
|
-
extracted.description ||
|
|
1749
|
-
extracted.name ||
|
|
1750
|
-
null,
|
|
1751
|
-
}
|
|
1752
|
-
: merged.sourceDocument,
|
|
1753
|
-
};
|
|
1754
|
-
});
|
|
1755
|
-
showToastHandler?.(
|
|
1756
|
-
'success',
|
|
1757
|
-
`Dados extraidos com ${aiProviderLabel}. Revise antes de concluir.`
|
|
1758
|
-
);
|
|
1759
|
-
} catch {
|
|
1760
|
-
showToastHandler?.(
|
|
1761
|
-
'error',
|
|
1762
|
-
'Nao foi possivel extrair os dados do documento.'
|
|
1763
|
-
);
|
|
1764
|
-
} finally {
|
|
1765
|
-
setIsExtracting(false);
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
|
|
1769
|
-
const handleNext = async () => {
|
|
1770
|
-
if (activeStep === 0 && !draftId) {
|
|
1771
|
-
if (creationMode === 'ai_prompt' || creationMode === 'duplicate') {
|
|
1772
|
-
setActiveStep(1);
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
const nextDraftId = await startDraft();
|
|
1777
|
-
if (nextDraftId && creationMode !== 'upload') {
|
|
1778
|
-
setActiveStep(1);
|
|
1779
|
-
}
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
if (activeStep === 1 && !draftId) {
|
|
1784
|
-
if (creationMode === 'ai_prompt' && aiStartPrompt.trim()) {
|
|
1785
|
-
setForm((current) => ({
|
|
1786
|
-
...current,
|
|
1787
|
-
description: current.description || aiStartPrompt.trim(),
|
|
1788
|
-
}));
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
if (creationMode === 'duplicate' || creationMode === 'ai_prompt') {
|
|
1792
|
-
const nextDraftId = await startDraft();
|
|
1793
|
-
if (!nextDraftId) return;
|
|
1794
|
-
|
|
1795
|
-
setDraftId(nextDraftId);
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
const response = await saveDraft(
|
|
1801
|
-
Math.min(activeStep + 1, wizardSteps.length - 1)
|
|
1802
|
-
);
|
|
1803
|
-
if (response) {
|
|
1804
|
-
setActiveStep(Math.min(activeStep + 1, wizardSteps.length - 1));
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
|
|
1808
|
-
const handleGeneratePdf = async () => {
|
|
1809
|
-
const nextDraftId = draftId ?? (await startDraft());
|
|
1810
|
-
if (!nextDraftId) return;
|
|
1811
|
-
|
|
1812
|
-
const saved = await saveDraft(activeStep);
|
|
1813
|
-
if (!saved) return;
|
|
1814
|
-
|
|
1815
|
-
setIsGeneratingPdf(true);
|
|
1816
|
-
try {
|
|
1817
|
-
const response = await mutateOperations<{ fileId?: number }>(
|
|
1818
|
-
request,
|
|
1819
|
-
`/operations/contracts/${nextDraftId}/generate-pdf`,
|
|
1820
|
-
'POST'
|
|
1821
|
-
);
|
|
1822
|
-
await reloadContract(nextDraftId);
|
|
1823
|
-
showToastHandler?.('success', 'PDF gerado com sucesso.');
|
|
1824
|
-
if (response.fileId) {
|
|
1825
|
-
openStoredFile(response.fileId);
|
|
1826
|
-
}
|
|
1827
|
-
} catch {
|
|
1828
|
-
showToastHandler?.(
|
|
1829
|
-
'error',
|
|
1830
|
-
'Nao foi possivel gerar o PDF. Verifique a configuracao do servidor.'
|
|
1831
|
-
);
|
|
1832
|
-
} finally {
|
|
1833
|
-
setIsGeneratingPdf(false);
|
|
1834
|
-
}
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
const handleRunLegalReview = useCallback(
|
|
1838
|
-
async (targetContractId?: number | null) => {
|
|
1839
|
-
if (!targetContractId) {
|
|
1840
|
-
return null;
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
setIsBusy(true);
|
|
1844
|
-
try {
|
|
1845
|
-
const review = await mutateOperations<ContractAiReviewState>(
|
|
1846
|
-
request,
|
|
1847
|
-
`/operations/contracts/${targetContractId}/legal-review`,
|
|
1848
|
-
'POST',
|
|
1849
|
-
{
|
|
1850
|
-
provider: aiProvider ?? undefined,
|
|
1851
|
-
promptMessage:
|
|
1852
|
-
'Review this contract draft and produce an advisory legal checklist for revision.',
|
|
1853
|
-
}
|
|
1854
|
-
);
|
|
1855
|
-
|
|
1856
|
-
const normalized = normalizeAiReview(review);
|
|
1857
|
-
setAiReview(normalized);
|
|
1858
|
-
showToastHandler?.(
|
|
1859
|
-
'success',
|
|
1860
|
-
'Checklist juridico orientativo atualizado.'
|
|
1861
|
-
);
|
|
1862
|
-
return normalized;
|
|
1863
|
-
} catch {
|
|
1864
|
-
showToastHandler?.(
|
|
1865
|
-
'error',
|
|
1866
|
-
'Nao foi possivel atualizar a revisao juridica orientativa.'
|
|
1867
|
-
);
|
|
1868
|
-
return null;
|
|
1869
|
-
} finally {
|
|
1870
|
-
setIsBusy(false);
|
|
1871
|
-
}
|
|
1872
|
-
},
|
|
1873
|
-
[aiProvider, request, showToastHandler]
|
|
1874
|
-
);
|
|
1875
|
-
|
|
1876
|
-
const runPostCreateAutomation = useCallback(
|
|
1877
|
-
async (contract: OperationsContractDetails) => {
|
|
1878
|
-
let latestContract = contract;
|
|
1879
|
-
let generationSucceeded = false;
|
|
1880
|
-
let reviewSucceeded = false;
|
|
1881
|
-
|
|
1882
|
-
setIsBusy(true);
|
|
1883
|
-
try {
|
|
1884
|
-
latestContract = await mutateOperations<OperationsContractDetails>(
|
|
1885
|
-
request,
|
|
1886
|
-
`/operations/contracts/${contract.id}/generate-content`,
|
|
1887
|
-
'POST',
|
|
1888
|
-
{
|
|
1889
|
-
provider: aiProvider ?? undefined,
|
|
1890
|
-
promptMessage:
|
|
1891
|
-
trimToNull(aiStartPrompt) ?? trimToNull(form.description),
|
|
1892
|
-
overwrite: !trimToNull(contract.contentHtml),
|
|
1893
|
-
}
|
|
1894
|
-
);
|
|
1895
|
-
|
|
1896
|
-
generationSucceeded = true;
|
|
1897
|
-
setDraftId(latestContract.id);
|
|
1898
|
-
setForm((current) =>
|
|
1899
|
-
preservePartySelectionMetadata(toFormState(latestContract), current)
|
|
1900
|
-
);
|
|
1901
|
-
} catch {
|
|
1902
|
-
showToastHandler?.(
|
|
1903
|
-
'error',
|
|
1904
|
-
'Nao foi possivel gerar automaticamente o conteudo inicial do contrato.'
|
|
1905
|
-
);
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
try {
|
|
1909
|
-
const review = await mutateOperations<ContractAiReviewState>(
|
|
1910
|
-
request,
|
|
1911
|
-
`/operations/contracts/${contract.id}/legal-review`,
|
|
1912
|
-
'POST',
|
|
1913
|
-
{
|
|
1914
|
-
provider: aiProvider ?? undefined,
|
|
1915
|
-
promptMessage:
|
|
1916
|
-
'Review this generated contract draft and return an advisory legal checklist for revision.',
|
|
1917
|
-
}
|
|
1918
|
-
);
|
|
1919
|
-
|
|
1920
|
-
reviewSucceeded = true;
|
|
1921
|
-
setAiReview(normalizeAiReview(review));
|
|
1922
|
-
} catch {
|
|
1923
|
-
showToastHandler?.(
|
|
1924
|
-
'error',
|
|
1925
|
-
'Nao foi possivel montar o checklist juridico orientativo automaticamente.'
|
|
1926
|
-
);
|
|
1927
|
-
} finally {
|
|
1928
|
-
setIsBusy(false);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
if (generationSucceeded || reviewSucceeded) {
|
|
1932
|
-
showToastHandler?.(
|
|
1933
|
-
'success',
|
|
1934
|
-
'Criacao concluida. O conteudo inicial e a revisao orientativa ja estao prontos para edicao.'
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
if (generationSucceeded || reviewSucceeded) {
|
|
1939
|
-
try {
|
|
1940
|
-
latestContract = await reloadContract(contract.id);
|
|
1941
|
-
} catch {
|
|
1942
|
-
// noop: the editor can still open with the latest saved response
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
return latestContract;
|
|
1947
|
-
},
|
|
1948
|
-
[
|
|
1949
|
-
aiProvider,
|
|
1950
|
-
aiStartPrompt,
|
|
1951
|
-
form.description,
|
|
1952
|
-
reloadContract,
|
|
1953
|
-
request,
|
|
1954
|
-
showToastHandler,
|
|
1955
|
-
]
|
|
1956
|
-
);
|
|
1957
|
-
|
|
1958
|
-
const handleComplete = async () => {
|
|
1959
|
-
const response = await saveDraft(reviewStepIndex);
|
|
1960
|
-
if (!response) return;
|
|
1961
|
-
|
|
1962
|
-
const finalResponse = isCreateFlow
|
|
1963
|
-
? await runPostCreateAutomation(response)
|
|
1964
|
-
: response;
|
|
1965
|
-
|
|
1966
|
-
await onSaved?.(finalResponse);
|
|
1967
|
-
};
|
|
1968
|
-
|
|
1969
|
-
const modeCards = useMemo(
|
|
1970
|
-
() => [
|
|
1971
|
-
{
|
|
1972
|
-
value: 'upload' as CreationMode,
|
|
1973
|
-
title: 'Enviar contrato',
|
|
1974
|
-
description:
|
|
1975
|
-
'Anexe PDF ou Word, relacione o arquivo ao contrato e deixe a IA preencher o rascunho.',
|
|
1976
|
-
icon: Upload,
|
|
1977
|
-
color: 'text-sky-600 bg-sky-500/10',
|
|
1978
|
-
},
|
|
1979
|
-
{
|
|
1980
|
-
value: 'template' as CreationMode,
|
|
1981
|
-
title: 'Usar template',
|
|
1982
|
-
description:
|
|
1983
|
-
'Comece com um modelo reutilizavel e ajuste apenas os campos necessarios.',
|
|
1984
|
-
icon: LayoutTemplate,
|
|
1985
|
-
color: 'text-amber-600 bg-amber-500/10',
|
|
1986
|
-
},
|
|
1987
|
-
{
|
|
1988
|
-
value: 'ai_prompt' as CreationMode,
|
|
1989
|
-
title: 'Prompt com IA',
|
|
1990
|
-
description: aiProvider
|
|
1991
|
-
? 'Descreva o contrato em linguagem natural e deixe a IA montar um rascunho inicial.'
|
|
1992
|
-
: 'Configure OpenAI ou Gemini para habilitar este modo.',
|
|
1993
|
-
icon: Sparkles,
|
|
1994
|
-
color: 'text-fuchsia-600 bg-fuchsia-500/10',
|
|
1995
|
-
disabled: !aiProvider,
|
|
1996
|
-
},
|
|
1997
|
-
{
|
|
1998
|
-
value: 'blank' as CreationMode,
|
|
1999
|
-
title: 'Do zero',
|
|
2000
|
-
description: 'Crie um rascunho enxuto e preencha os dados aos poucos.',
|
|
2001
|
-
icon: FileText,
|
|
2002
|
-
color: 'text-emerald-600 bg-emerald-500/10',
|
|
2003
|
-
},
|
|
2004
|
-
{
|
|
2005
|
-
value: 'duplicate' as CreationMode,
|
|
2006
|
-
title: 'Duplicar',
|
|
2007
|
-
description:
|
|
2008
|
-
'Reaproveite um contrato existente como base para um novo registro.',
|
|
2009
|
-
icon: CopyPlus,
|
|
2010
|
-
color: 'text-violet-600 bg-violet-500/10',
|
|
2011
|
-
},
|
|
2012
|
-
],
|
|
2013
|
-
[aiProvider]
|
|
2014
|
-
);
|
|
2015
|
-
|
|
2016
|
-
const currentStep = wizardSteps[activeStep];
|
|
2017
|
-
|
|
2018
|
-
const stepContent = (() => {
|
|
2019
|
-
if (activeStep === 0) {
|
|
2020
|
-
return (
|
|
2021
|
-
<div className="space-y-4">
|
|
2022
|
-
<SectionCard
|
|
2023
|
-
title="Escolha como deseja iniciar"
|
|
2024
|
-
description="O contrato nasce como rascunho e voce pode completar tudo depois."
|
|
2025
|
-
compact
|
|
2026
|
-
descriptionMode="tooltip"
|
|
2027
|
-
>
|
|
2028
|
-
<div className="grid gap-3">
|
|
2029
|
-
{modeCards.map((card) => {
|
|
2030
|
-
const Icon = card.icon;
|
|
2031
|
-
const isSelected = creationMode === card.value;
|
|
2032
|
-
const isDisabled = Boolean(card.disabled);
|
|
2033
|
-
return (
|
|
2034
|
-
<button
|
|
2035
|
-
key={card.value}
|
|
2036
|
-
type="button"
|
|
2037
|
-
disabled={isDisabled}
|
|
2038
|
-
className={`rounded-xl border p-4 text-left transition-all ${
|
|
2039
|
-
isSelected
|
|
2040
|
-
? 'border-primary bg-accent/30 ring-1 ring-primary/20'
|
|
2041
|
-
: isDisabled
|
|
2042
|
-
? 'cursor-not-allowed opacity-50'
|
|
2043
|
-
: 'cursor-pointer hover:border-primary/30 hover:bg-accent/20'
|
|
2044
|
-
}`}
|
|
2045
|
-
onClick={() => {
|
|
2046
|
-
if (isDisabled) return;
|
|
2047
|
-
setCreationMode(card.value);
|
|
2048
|
-
setForm((current) => ({
|
|
2049
|
-
...current,
|
|
2050
|
-
creationMode: card.value,
|
|
2051
|
-
}));
|
|
2052
|
-
}}
|
|
2053
|
-
>
|
|
2054
|
-
<div className="flex items-start gap-3">
|
|
2055
|
-
<div className={`rounded-xl p-2.5 ${card.color}`}>
|
|
2056
|
-
<Icon className="size-5" />
|
|
2057
|
-
</div>
|
|
2058
|
-
<div className="space-y-1">
|
|
2059
|
-
<div className="font-medium">{card.title}</div>
|
|
2060
|
-
<p className="text-sm text-muted-foreground">
|
|
2061
|
-
{card.description}
|
|
2062
|
-
</p>
|
|
2063
|
-
</div>
|
|
2064
|
-
</div>
|
|
2065
|
-
</button>
|
|
2066
|
-
);
|
|
2067
|
-
})}
|
|
2068
|
-
</div>
|
|
2069
|
-
</SectionCard>
|
|
2070
|
-
|
|
2071
|
-
{creationMode === 'upload' ? (
|
|
2072
|
-
<SectionCard
|
|
2073
|
-
title="Documento fonte"
|
|
2074
|
-
description="O arquivo fica vinculado ao contrato e pode ser usado pela IA para preencher o cadastro."
|
|
2075
|
-
compact
|
|
2076
|
-
descriptionMode="tooltip"
|
|
2077
|
-
>
|
|
2078
|
-
<div className="space-y-4">
|
|
2079
|
-
<label className="flex cursor-pointer flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed px-4 py-8 text-center transition-colors hover:border-primary/40 hover:bg-accent/20">
|
|
2080
|
-
<FileSpreadsheet className="size-7 text-primary" />
|
|
2081
|
-
<div className="space-y-1">
|
|
2082
|
-
<div className="font-medium">
|
|
2083
|
-
Selecionar PDF, DOC ou DOCX
|
|
2084
|
-
</div>
|
|
2085
|
-
<p className="text-sm text-muted-foreground">
|
|
2086
|
-
O arquivo sera guardado como documento fonte do contrato.
|
|
2087
|
-
</p>
|
|
2088
|
-
</div>
|
|
2089
|
-
<input
|
|
2090
|
-
type="file"
|
|
2091
|
-
className="hidden"
|
|
2092
|
-
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
2093
|
-
onChange={(event) => {
|
|
2094
|
-
setSourceFile(event.target.files?.[0] ?? null);
|
|
2095
|
-
}}
|
|
2096
|
-
/>
|
|
2097
|
-
</label>
|
|
2098
|
-
|
|
2099
|
-
{sourceFile ? (
|
|
2100
|
-
<div className="rounded-xl border bg-muted/20 px-4 py-3 text-sm">
|
|
2101
|
-
<div className="font-medium">{sourceFile.name}</div>
|
|
2102
|
-
<div className="text-muted-foreground">
|
|
2103
|
-
{Math.max(sourceFile.size / 1024, 1).toFixed(0)} KB
|
|
2104
|
-
</div>
|
|
2105
|
-
</div>
|
|
2106
|
-
) : null}
|
|
2107
|
-
|
|
2108
|
-
{form.sourceDocument ? (
|
|
2109
|
-
<div className="rounded-xl border px-4 py-3 text-sm">
|
|
2110
|
-
<div className="font-medium">
|
|
2111
|
-
{form.sourceDocument.fileName}
|
|
2112
|
-
</div>
|
|
2113
|
-
<div className="text-muted-foreground">
|
|
2114
|
-
Status da extracao:{' '}
|
|
2115
|
-
{formatEnumLabel(form.sourceDocument.extractionStatus)}
|
|
2116
|
-
</div>
|
|
2117
|
-
</div>
|
|
2118
|
-
) : null}
|
|
2119
|
-
|
|
2120
|
-
<div className="flex flex-wrap gap-2">
|
|
2121
|
-
<Button
|
|
2122
|
-
type="button"
|
|
2123
|
-
variant="outline"
|
|
2124
|
-
className="cursor-pointer"
|
|
2125
|
-
disabled={(!draftId && !sourceFile) || isExtracting}
|
|
2126
|
-
onClick={() => void handleExtractSource()}
|
|
2127
|
-
>
|
|
2128
|
-
{isExtracting ? (
|
|
2129
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
2130
|
-
) : (
|
|
2131
|
-
<Sparkles className="size-4" />
|
|
2132
|
-
)}
|
|
2133
|
-
Extrair com {aiProviderLabel || 'IA'}
|
|
2134
|
-
</Button>
|
|
2135
|
-
{form.sourceDocument?.fileId ? (
|
|
2136
|
-
<Button
|
|
2137
|
-
type="button"
|
|
2138
|
-
variant="outline"
|
|
2139
|
-
className="cursor-pointer"
|
|
2140
|
-
onClick={() =>
|
|
2141
|
-
openStoredFile(form.sourceDocument?.fileId)
|
|
2142
|
-
}
|
|
2143
|
-
>
|
|
2144
|
-
<Download className="size-4" />
|
|
2145
|
-
Abrir documento
|
|
2146
|
-
</Button>
|
|
2147
|
-
) : null}
|
|
2148
|
-
</div>
|
|
2149
|
-
|
|
2150
|
-
{aiReview ? (
|
|
2151
|
-
<div className="space-y-3 rounded-2xl border bg-muted/20 p-4">
|
|
2152
|
-
{aiReview.summary ? (
|
|
2153
|
-
<p className="text-sm">{aiReview.summary}</p>
|
|
2154
|
-
) : null}
|
|
2155
|
-
{aiReview.missingFields.length ? (
|
|
2156
|
-
<div className="space-y-1 text-sm">
|
|
2157
|
-
<div className="font-medium">Campos faltantes</div>
|
|
2158
|
-
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
2159
|
-
{aiReview.missingFields.map((item) => (
|
|
2160
|
-
<li key={item}>{item}</li>
|
|
2161
|
-
))}
|
|
2162
|
-
</ul>
|
|
2163
|
-
</div>
|
|
2164
|
-
) : null}
|
|
2165
|
-
{aiReview.warnings.length ? (
|
|
2166
|
-
<div className="space-y-1 text-sm">
|
|
2167
|
-
<div className="font-medium">Avisos</div>
|
|
2168
|
-
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
2169
|
-
{aiReview.warnings.map((item) => (
|
|
2170
|
-
<li key={item}>{item}</li>
|
|
2171
|
-
))}
|
|
2172
|
-
</ul>
|
|
2173
|
-
</div>
|
|
2174
|
-
) : null}
|
|
2175
|
-
</div>
|
|
2176
|
-
) : null}
|
|
2177
|
-
</div>
|
|
2178
|
-
</SectionCard>
|
|
2179
|
-
) : null}
|
|
2180
|
-
|
|
2181
|
-
{creationMode === 'template' ? (
|
|
2182
|
-
<ContractTemplateSelectWithCreate
|
|
2183
|
-
label="Template do contrato"
|
|
2184
|
-
value={selectedTemplateId}
|
|
2185
|
-
initialSelectedLabel={selectedTemplate?.name ?? ''}
|
|
2186
|
-
placeholder="Selecione um template"
|
|
2187
|
-
onChange={(template) => {
|
|
2188
|
-
setSelectedTemplateId(template?.id ?? null);
|
|
2189
|
-
setForm((current) => ({
|
|
2190
|
-
...current,
|
|
2191
|
-
contractTemplateId: template ? String(template.id) : 'none',
|
|
2192
|
-
}));
|
|
2193
|
-
}}
|
|
2194
|
-
/>
|
|
2195
|
-
) : null}
|
|
2196
|
-
</div>
|
|
2197
|
-
);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
if (activeStep === 1) {
|
|
2201
|
-
if (creationMode === 'ai_prompt' && !draftId) {
|
|
2202
|
-
return (
|
|
2203
|
-
<div className="space-y-4">
|
|
2204
|
-
<SectionCard
|
|
2205
|
-
title="Prompt inicial para a IA"
|
|
2206
|
-
description="Descreva o tipo de contrato e os principais pontos para gerar um primeiro rascunho."
|
|
2207
|
-
compact
|
|
2208
|
-
descriptionMode="tooltip"
|
|
2209
|
-
>
|
|
2210
|
-
<div className="space-y-3">
|
|
2211
|
-
<Textarea
|
|
2212
|
-
rows={6}
|
|
2213
|
-
value={aiStartPrompt}
|
|
2214
|
-
placeholder="Ex: Gere um contrato de prestacao de servicos mensais com vigencia de 12 meses, pagamento recorrente, confidencialidade e clausula de rescisao."
|
|
2215
|
-
onChange={(event) => setAiStartPrompt(event.target.value)}
|
|
2216
|
-
/>
|
|
2217
|
-
<p className="text-sm text-muted-foreground">
|
|
2218
|
-
Ao concluir o cadastro, a IA vai gerar um texto inicial para
|
|
2219
|
-
voce revisar e ajustar no modo de edicao.
|
|
2220
|
-
</p>
|
|
2221
|
-
</div>
|
|
2222
|
-
</SectionCard>
|
|
2223
|
-
</div>
|
|
2224
|
-
);
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
if (creationMode === 'duplicate' && !draftId) {
|
|
2228
|
-
return (
|
|
2229
|
-
<div className="space-y-4">
|
|
2230
|
-
<SectionCard
|
|
2231
|
-
title="Contrato base"
|
|
2232
|
-
description="Selecione o contrato que sera usado para duplicar este novo rascunho. Este campo e obrigatorio para continuar."
|
|
2233
|
-
compact
|
|
2234
|
-
descriptionMode="tooltip"
|
|
2235
|
-
>
|
|
2236
|
-
<div className="space-y-3">
|
|
2237
|
-
<Select
|
|
2238
|
-
value={
|
|
2239
|
-
selectedDuplicateId ? String(selectedDuplicateId) : 'none'
|
|
2240
|
-
}
|
|
2241
|
-
onValueChange={(value) =>
|
|
2242
|
-
setSelectedDuplicateId(
|
|
2243
|
-
value === 'none' ? null : Number(value)
|
|
2244
|
-
)
|
|
2245
|
-
}
|
|
2246
|
-
>
|
|
2247
|
-
<SelectTrigger className="w-full">
|
|
2248
|
-
<SelectValue placeholder="Selecione um contrato" />
|
|
2249
|
-
</SelectTrigger>
|
|
2250
|
-
<SelectContent>
|
|
2251
|
-
<SelectItem value="none">Selecione um contrato</SelectItem>
|
|
2252
|
-
{contracts.map((contract) => (
|
|
2253
|
-
<SelectItem key={contract.id} value={String(contract.id)}>
|
|
2254
|
-
{(
|
|
2255
|
-
contract.name ||
|
|
2256
|
-
contract.code ||
|
|
2257
|
-
`Contrato ${contract.id}`
|
|
2258
|
-
).trim()}
|
|
2259
|
-
</SelectItem>
|
|
2260
|
-
))}
|
|
2261
|
-
</SelectContent>
|
|
2262
|
-
</Select>
|
|
2263
|
-
|
|
2264
|
-
<p className="text-sm text-muted-foreground">
|
|
2265
|
-
Se preferir, volte e escolha outra opcao para iniciar.
|
|
2266
|
-
</p>
|
|
2267
|
-
</div>
|
|
2268
|
-
</SectionCard>
|
|
2269
|
-
</div>
|
|
2270
|
-
);
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
return (
|
|
2274
|
-
<div className="space-y-4">
|
|
2275
|
-
<SectionCard
|
|
2276
|
-
title="Informacoes principais"
|
|
2277
|
-
description="Preencha os dados basicos para identificar o contrato e seus relacionamentos."
|
|
2278
|
-
compact
|
|
2279
|
-
descriptionMode="tooltip"
|
|
2280
|
-
>
|
|
2281
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
2282
|
-
<div className="space-y-1.5">
|
|
2283
|
-
<Label className="text-xs">Codigo</Label>
|
|
2284
|
-
<Input
|
|
2285
|
-
value={form.code}
|
|
2286
|
-
onChange={(event) =>
|
|
2287
|
-
setForm((current) => ({
|
|
2288
|
-
...current,
|
|
2289
|
-
code: event.target.value,
|
|
2290
|
-
}))
|
|
2291
|
-
}
|
|
2292
|
-
/>
|
|
2293
|
-
</div>
|
|
2294
|
-
<div className="space-y-1.5">
|
|
2295
|
-
<Label className="text-xs">Titulo do contrato</Label>
|
|
2296
|
-
<Input
|
|
2297
|
-
value={form.name}
|
|
2298
|
-
onChange={(event) =>
|
|
2299
|
-
setForm((current) => ({
|
|
2300
|
-
...current,
|
|
2301
|
-
name: event.target.value,
|
|
2302
|
-
}))
|
|
2303
|
-
}
|
|
2304
|
-
/>
|
|
2305
|
-
</div>
|
|
2306
|
-
<div className="space-y-3 md:col-span-2">
|
|
2307
|
-
<PersonSelectWithCreate
|
|
2308
|
-
label="Cliente ou parte principal"
|
|
2309
|
-
entityLabel="cliente"
|
|
2310
|
-
value={primaryParty?.personId ?? null}
|
|
2311
|
-
initialSelectedLabel={form.clientName}
|
|
2312
|
-
selectPlaceholder="Selecionar pessoa ou empresa"
|
|
2313
|
-
personTypeFilter="all"
|
|
2314
|
-
onChange={(personId, personName) => {
|
|
2315
|
-
void handlePartyPersonChange(
|
|
2316
|
-
primaryPartyIndex,
|
|
2317
|
-
personId,
|
|
2318
|
-
personName,
|
|
2319
|
-
{
|
|
2320
|
-
syncClientName: true,
|
|
2321
|
-
markAsPrimary: true,
|
|
2322
|
-
}
|
|
2323
|
-
);
|
|
2324
|
-
}}
|
|
2325
|
-
/>
|
|
2326
|
-
|
|
2327
|
-
<div className="space-y-3 rounded-2xl border bg-muted/20 p-3">
|
|
2328
|
-
<div className="space-y-1">
|
|
2329
|
-
<div className="text-sm font-medium">
|
|
2330
|
-
Dados usados na parte principal
|
|
2331
|
-
</div>
|
|
2332
|
-
<p className="text-xs text-muted-foreground">
|
|
2333
|
-
Escolha quais dados do cadastro dessa pessoa devem entrar
|
|
2334
|
-
no contrato. Se faltar algo, digite um novo valor.
|
|
2335
|
-
</p>
|
|
2336
|
-
</div>
|
|
2337
|
-
|
|
2338
|
-
<div className="grid gap-3 md:grid-cols-3">
|
|
2339
|
-
<PersonDataChoiceField
|
|
2340
|
-
label="Documento"
|
|
2341
|
-
value={primaryParty?.documentNumber ?? ''}
|
|
2342
|
-
options={primaryParty?.availableDocuments ?? []}
|
|
2343
|
-
mode={primaryParty?.documentMode ?? 'new'}
|
|
2344
|
-
kind="document"
|
|
2345
|
-
placeholder="Selecionar ou digitar documento"
|
|
2346
|
-
onModeChange={(mode) =>
|
|
2347
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2348
|
-
...party,
|
|
2349
|
-
documentMode: mode,
|
|
2350
|
-
}))
|
|
2351
|
-
}
|
|
2352
|
-
onValueChange={(value) =>
|
|
2353
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2354
|
-
...party,
|
|
2355
|
-
documentNumber: value,
|
|
2356
|
-
}))
|
|
2357
|
-
}
|
|
2358
|
-
/>
|
|
2359
|
-
<PersonDataChoiceField
|
|
2360
|
-
label="E-mail"
|
|
2361
|
-
value={primaryParty?.email ?? ''}
|
|
2362
|
-
options={primaryParty?.availableEmails ?? []}
|
|
2363
|
-
mode={primaryParty?.emailMode ?? 'new'}
|
|
2364
|
-
kind="email"
|
|
2365
|
-
inputType="email"
|
|
2366
|
-
placeholder="Selecionar ou digitar e-mail"
|
|
2367
|
-
onModeChange={(mode) =>
|
|
2368
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2369
|
-
...party,
|
|
2370
|
-
emailMode: mode,
|
|
2371
|
-
}))
|
|
2372
|
-
}
|
|
2373
|
-
onValueChange={(value) =>
|
|
2374
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2375
|
-
...party,
|
|
2376
|
-
email: value,
|
|
2377
|
-
}))
|
|
2378
|
-
}
|
|
2379
|
-
/>
|
|
2380
|
-
<PersonDataChoiceField
|
|
2381
|
-
label="Telefone"
|
|
2382
|
-
value={primaryParty?.phone ?? ''}
|
|
2383
|
-
options={primaryParty?.availablePhones ?? []}
|
|
2384
|
-
mode={primaryParty?.phoneMode ?? 'new'}
|
|
2385
|
-
kind="phone"
|
|
2386
|
-
inputType="tel"
|
|
2387
|
-
placeholder="Selecionar ou digitar telefone"
|
|
2388
|
-
onModeChange={(mode) =>
|
|
2389
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2390
|
-
...party,
|
|
2391
|
-
phoneMode: mode,
|
|
2392
|
-
}))
|
|
2393
|
-
}
|
|
2394
|
-
onValueChange={(value) =>
|
|
2395
|
-
updatePartyAt(primaryPartyIndex, (party) => ({
|
|
2396
|
-
...party,
|
|
2397
|
-
phone: value,
|
|
2398
|
-
}))
|
|
2399
|
-
}
|
|
2400
|
-
/>
|
|
2401
|
-
</div>
|
|
2402
|
-
</div>
|
|
2403
|
-
</div>
|
|
2404
|
-
<ContractTemplateSelectWithCreate
|
|
2405
|
-
label="Template vinculado"
|
|
2406
|
-
value={
|
|
2407
|
-
form.contractTemplateId !== 'none'
|
|
2408
|
-
? Number(form.contractTemplateId)
|
|
2409
|
-
: null
|
|
2410
|
-
}
|
|
2411
|
-
initialSelectedLabel={
|
|
2412
|
-
contractTemplates.find(
|
|
2413
|
-
(template) =>
|
|
2414
|
-
String(template.id) === form.contractTemplateId
|
|
2415
|
-
)?.name ?? ''
|
|
2416
|
-
}
|
|
2417
|
-
placeholder="Selecionar template"
|
|
2418
|
-
onChange={(template) => {
|
|
2419
|
-
setSelectedTemplateId(template?.id ?? null);
|
|
2420
|
-
setForm((current) => ({
|
|
2421
|
-
...current,
|
|
2422
|
-
contractTemplateId: template ? String(template.id) : 'none',
|
|
2423
|
-
contractCategory:
|
|
2424
|
-
template?.contractCategory ?? current.contractCategory,
|
|
2425
|
-
contractType:
|
|
2426
|
-
template?.contractType ?? current.contractType,
|
|
2427
|
-
billingModel:
|
|
2428
|
-
template?.billingModel ?? current.billingModel,
|
|
2429
|
-
signatureStatus:
|
|
2430
|
-
template?.signatureStatus ?? current.signatureStatus,
|
|
2431
|
-
name: current.name || template?.name || '',
|
|
2432
|
-
description:
|
|
2433
|
-
current.description || template?.description || '',
|
|
2434
|
-
contentHtml:
|
|
2435
|
-
current.contentHtml || template?.contentHtml || '',
|
|
2436
|
-
}));
|
|
2437
|
-
}}
|
|
2438
|
-
/>
|
|
2439
|
-
<CollaboratorSelectWithCreate
|
|
2440
|
-
label="Gestor responsavel"
|
|
2441
|
-
value={
|
|
2442
|
-
form.accountManagerCollaboratorId !== 'none'
|
|
2443
|
-
? Number(form.accountManagerCollaboratorId)
|
|
2444
|
-
: null
|
|
2445
|
-
}
|
|
2446
|
-
initialSelectedLabel={
|
|
2447
|
-
collaborators.find(
|
|
2448
|
-
(item) =>
|
|
2449
|
-
String(item.id) === form.accountManagerCollaboratorId
|
|
2450
|
-
)?.displayName ?? ''
|
|
2451
|
-
}
|
|
2452
|
-
placeholder="Selecionar colaborador"
|
|
2453
|
-
onChange={(option) =>
|
|
2454
|
-
setForm((current) => ({
|
|
2455
|
-
...current,
|
|
2456
|
-
accountManagerCollaboratorId: option
|
|
2457
|
-
? String(option.id)
|
|
2458
|
-
: 'none',
|
|
2459
|
-
}))
|
|
2460
|
-
}
|
|
2461
|
-
/>
|
|
2462
|
-
<CollaboratorSelectWithCreate
|
|
2463
|
-
label="Colaborador relacionado"
|
|
2464
|
-
value={
|
|
2465
|
-
form.relatedCollaboratorId !== 'none'
|
|
2466
|
-
? Number(form.relatedCollaboratorId)
|
|
2467
|
-
: null
|
|
2468
|
-
}
|
|
2469
|
-
initialSelectedLabel={
|
|
2470
|
-
collaborators.find(
|
|
2471
|
-
(item) => String(item.id) === form.relatedCollaboratorId
|
|
2472
|
-
)?.displayName ?? ''
|
|
2473
|
-
}
|
|
2474
|
-
placeholder="Selecionar colaborador"
|
|
2475
|
-
onChange={(option) =>
|
|
2476
|
-
setForm((current) => ({
|
|
2477
|
-
...current,
|
|
2478
|
-
relatedCollaboratorId: option ? String(option.id) : 'none',
|
|
2479
|
-
}))
|
|
2480
|
-
}
|
|
2481
|
-
/>
|
|
2482
|
-
<div className="space-y-1.5">
|
|
2483
|
-
<Label className="text-xs">Categoria</Label>
|
|
2484
|
-
<Select
|
|
2485
|
-
value={form.contractCategory}
|
|
2486
|
-
onValueChange={(value) =>
|
|
2487
|
-
setForm((current) => ({
|
|
2488
|
-
...current,
|
|
2489
|
-
contractCategory: value,
|
|
2490
|
-
}))
|
|
2491
|
-
}
|
|
2492
|
-
>
|
|
2493
|
-
<SelectTrigger className="w-full">
|
|
2494
|
-
<SelectValue />
|
|
2495
|
-
</SelectTrigger>
|
|
2496
|
-
<SelectContent>
|
|
2497
|
-
{[
|
|
2498
|
-
'employee',
|
|
2499
|
-
'contractor',
|
|
2500
|
-
'client',
|
|
2501
|
-
'supplier',
|
|
2502
|
-
'vendor',
|
|
2503
|
-
'partner',
|
|
2504
|
-
'internal',
|
|
2505
|
-
'other',
|
|
2506
|
-
].map((value) => (
|
|
2507
|
-
<SelectItem key={value} value={value}>
|
|
2508
|
-
{getOptionLabel('contractCategories', value)}
|
|
2509
|
-
</SelectItem>
|
|
2510
|
-
))}
|
|
2511
|
-
</SelectContent>
|
|
2512
|
-
</Select>
|
|
2513
|
-
</div>
|
|
2514
|
-
<div className="space-y-1.5">
|
|
2515
|
-
<Label className="text-xs">Tipo</Label>
|
|
2516
|
-
<Select
|
|
2517
|
-
value={form.contractType}
|
|
2518
|
-
onValueChange={(value) =>
|
|
2519
|
-
setForm((current) => ({ ...current, contractType: value }))
|
|
2520
|
-
}
|
|
2521
|
-
>
|
|
2522
|
-
<SelectTrigger className="w-full">
|
|
2523
|
-
<SelectValue />
|
|
2524
|
-
</SelectTrigger>
|
|
2525
|
-
<SelectContent>
|
|
2526
|
-
{[
|
|
2527
|
-
'clt',
|
|
2528
|
-
'pj',
|
|
2529
|
-
'freelancer_agreement',
|
|
2530
|
-
'service_agreement',
|
|
2531
|
-
'fixed_term',
|
|
2532
|
-
'recurring_service',
|
|
2533
|
-
'nda',
|
|
2534
|
-
'amendment',
|
|
2535
|
-
'addendum',
|
|
2536
|
-
'other',
|
|
2537
|
-
].map((value) => (
|
|
2538
|
-
<SelectItem key={value} value={value}>
|
|
2539
|
-
{getOptionLabel('contractTypes', value)}
|
|
2540
|
-
</SelectItem>
|
|
2541
|
-
))}
|
|
2542
|
-
</SelectContent>
|
|
2543
|
-
</Select>
|
|
2544
|
-
</div>
|
|
2545
|
-
</div>
|
|
2546
|
-
</SectionCard>
|
|
2547
|
-
</div>
|
|
2548
|
-
);
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
if (activeStep === 2) {
|
|
2552
|
-
return (
|
|
2553
|
-
<div className="space-y-4">
|
|
2554
|
-
<SectionCard
|
|
2555
|
-
title="Partes relacionadas"
|
|
2556
|
-
description="Adicione apenas o essencial agora. O restante pode ser completado depois."
|
|
2557
|
-
compact
|
|
2558
|
-
descriptionMode="tooltip"
|
|
2559
|
-
>
|
|
2560
|
-
<div className="space-y-3">
|
|
2561
|
-
{form.parties.map((party, index) => (
|
|
2562
|
-
<div key={`party-${index}`} className="border-t pt-3">
|
|
2563
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
2564
|
-
<PersonSelectWithCreate
|
|
2565
|
-
label={`Parte ${index + 1}`}
|
|
2566
|
-
entityLabel="parte"
|
|
2567
|
-
value={party.personId}
|
|
2568
|
-
initialSelectedLabel={party.displayName}
|
|
2569
|
-
selectPlaceholder="Selecionar pessoa ou empresa"
|
|
2570
|
-
personTypeFilter="all"
|
|
2571
|
-
onChange={(personId, personName) =>
|
|
2572
|
-
void handlePartyPersonChange(
|
|
2573
|
-
index,
|
|
2574
|
-
personId,
|
|
2575
|
-
personName,
|
|
2576
|
-
{
|
|
2577
|
-
syncClientName: party.isPrimary,
|
|
2578
|
-
}
|
|
2579
|
-
)
|
|
2580
|
-
}
|
|
2581
|
-
/>
|
|
2582
|
-
<div className="space-y-1.5">
|
|
2583
|
-
<Label className="text-xs">Papel</Label>
|
|
2584
|
-
<Select
|
|
2585
|
-
value={party.partyRole}
|
|
2586
|
-
onValueChange={(value) =>
|
|
2587
|
-
setForm((current) => ({
|
|
2588
|
-
...current,
|
|
2589
|
-
parties: current.parties.map((item, itemIndex) =>
|
|
2590
|
-
itemIndex === index
|
|
2591
|
-
? { ...item, partyRole: value }
|
|
2592
|
-
: item
|
|
2593
|
-
),
|
|
2594
|
-
}))
|
|
2595
|
-
}
|
|
2596
|
-
>
|
|
2597
|
-
<SelectTrigger className="w-full">
|
|
2598
|
-
<SelectValue />
|
|
2599
|
-
</SelectTrigger>
|
|
2600
|
-
<SelectContent>
|
|
2601
|
-
{[
|
|
2602
|
-
'employee',
|
|
2603
|
-
'employer',
|
|
2604
|
-
'client',
|
|
2605
|
-
'supplier',
|
|
2606
|
-
'vendor',
|
|
2607
|
-
'partner',
|
|
2608
|
-
'witness',
|
|
2609
|
-
'internal_owner',
|
|
2610
|
-
'other',
|
|
2611
|
-
].map((value) => (
|
|
2612
|
-
<SelectItem key={value} value={value}>
|
|
2613
|
-
{getOptionLabel('partyRoles', value)}
|
|
2614
|
-
</SelectItem>
|
|
2615
|
-
))}
|
|
2616
|
-
</SelectContent>
|
|
2617
|
-
</Select>
|
|
2618
|
-
</div>
|
|
2619
|
-
<PersonDataChoiceField
|
|
2620
|
-
label="Documento"
|
|
2621
|
-
value={party.documentNumber}
|
|
2622
|
-
options={party.availableDocuments}
|
|
2623
|
-
mode={party.documentMode}
|
|
2624
|
-
kind="document"
|
|
2625
|
-
placeholder="Selecionar ou digitar documento"
|
|
2626
|
-
onModeChange={(mode) =>
|
|
2627
|
-
updatePartyAt(index, (item) => ({
|
|
2628
|
-
...item,
|
|
2629
|
-
documentMode: mode,
|
|
2630
|
-
}))
|
|
2631
|
-
}
|
|
2632
|
-
onValueChange={(value) =>
|
|
2633
|
-
updatePartyAt(index, (item) => ({
|
|
2634
|
-
...item,
|
|
2635
|
-
documentNumber: value,
|
|
2636
|
-
}))
|
|
2637
|
-
}
|
|
2638
|
-
/>
|
|
2639
|
-
<PersonDataChoiceField
|
|
2640
|
-
label="E-mail"
|
|
2641
|
-
value={party.email}
|
|
2642
|
-
options={party.availableEmails}
|
|
2643
|
-
mode={party.emailMode}
|
|
2644
|
-
kind="email"
|
|
2645
|
-
inputType="email"
|
|
2646
|
-
placeholder="Selecionar ou digitar e-mail"
|
|
2647
|
-
onModeChange={(mode) =>
|
|
2648
|
-
updatePartyAt(index, (item) => ({
|
|
2649
|
-
...item,
|
|
2650
|
-
emailMode: mode,
|
|
2651
|
-
}))
|
|
2652
|
-
}
|
|
2653
|
-
onValueChange={(value) =>
|
|
2654
|
-
updatePartyAt(index, (item) => ({
|
|
2655
|
-
...item,
|
|
2656
|
-
email: value,
|
|
2657
|
-
}))
|
|
2658
|
-
}
|
|
2659
|
-
/>
|
|
2660
|
-
<PersonDataChoiceField
|
|
2661
|
-
label="Telefone"
|
|
2662
|
-
value={party.phone}
|
|
2663
|
-
options={party.availablePhones}
|
|
2664
|
-
mode={party.phoneMode}
|
|
2665
|
-
kind="phone"
|
|
2666
|
-
inputType="tel"
|
|
2667
|
-
placeholder="Selecionar ou digitar telefone"
|
|
2668
|
-
onModeChange={(mode) =>
|
|
2669
|
-
updatePartyAt(index, (item) => ({
|
|
2670
|
-
...item,
|
|
2671
|
-
phoneMode: mode,
|
|
2672
|
-
}))
|
|
2673
|
-
}
|
|
2674
|
-
onValueChange={(value) =>
|
|
2675
|
-
updatePartyAt(index, (item) => ({
|
|
2676
|
-
...item,
|
|
2677
|
-
phone: value,
|
|
2678
|
-
}))
|
|
2679
|
-
}
|
|
2680
|
-
/>
|
|
2681
|
-
<div className="flex items-center gap-2 pt-6">
|
|
2682
|
-
<Switch
|
|
2683
|
-
checked={party.isPrimary}
|
|
2684
|
-
onCheckedChange={(checked) =>
|
|
2685
|
-
setForm((current) => {
|
|
2686
|
-
const nextParties = current.parties.map(
|
|
2687
|
-
(item, itemIndex) => ({
|
|
2688
|
-
...item,
|
|
2689
|
-
isPrimary:
|
|
2690
|
-
itemIndex === index
|
|
2691
|
-
? checked
|
|
2692
|
-
: checked
|
|
2693
|
-
? false
|
|
2694
|
-
: item.isPrimary,
|
|
2695
|
-
})
|
|
2696
|
-
);
|
|
2697
|
-
|
|
2698
|
-
return {
|
|
2699
|
-
...current,
|
|
2700
|
-
clientName:
|
|
2701
|
-
checked && nextParties[index]?.displayName
|
|
2702
|
-
? nextParties[index].displayName
|
|
2703
|
-
: current.clientName,
|
|
2704
|
-
parties: nextParties,
|
|
2705
|
-
};
|
|
2706
|
-
})
|
|
2707
|
-
}
|
|
2708
|
-
/>
|
|
2709
|
-
<span className="text-sm text-muted-foreground">
|
|
2710
|
-
Parte principal
|
|
2711
|
-
</span>
|
|
2712
|
-
</div>
|
|
2713
|
-
{form.parties.length > 1 ? (
|
|
2714
|
-
<div className="flex justify-end md:col-span-2">
|
|
2715
|
-
<Button
|
|
2716
|
-
type="button"
|
|
2717
|
-
variant="destructive"
|
|
2718
|
-
className="cursor-pointer"
|
|
2719
|
-
onClick={() =>
|
|
2720
|
-
setForm((current) => ({
|
|
2721
|
-
...current,
|
|
2722
|
-
parties: current.parties.filter(
|
|
2723
|
-
(_, itemIndex) => itemIndex !== index
|
|
2724
|
-
),
|
|
2725
|
-
}))
|
|
2726
|
-
}
|
|
2727
|
-
>
|
|
2728
|
-
<Trash2 className="size-4" />
|
|
2729
|
-
Remover parte
|
|
2730
|
-
</Button>
|
|
2731
|
-
</div>
|
|
2732
|
-
) : null}
|
|
2733
|
-
</div>
|
|
2734
|
-
</div>
|
|
2735
|
-
))}
|
|
2736
|
-
|
|
2737
|
-
<div className="flex gap-2">
|
|
2738
|
-
<Button
|
|
2739
|
-
type="button"
|
|
2740
|
-
variant="outline"
|
|
2741
|
-
className="cursor-pointer"
|
|
2742
|
-
onClick={() =>
|
|
2743
|
-
setForm((current) => ({
|
|
2744
|
-
...current,
|
|
2745
|
-
parties: [...current.parties, emptyParty()],
|
|
2746
|
-
}))
|
|
2747
|
-
}
|
|
2748
|
-
>
|
|
2749
|
-
<Users className="size-4" />
|
|
2750
|
-
Adicionar parte
|
|
2751
|
-
</Button>
|
|
2752
|
-
</div>
|
|
2753
|
-
</div>
|
|
2754
|
-
</SectionCard>
|
|
2755
|
-
<SectionCard
|
|
2756
|
-
title="Assinaturas"
|
|
2757
|
-
description="Liste apenas quem precisa aparecer no fluxo de assinatura."
|
|
2758
|
-
compact
|
|
2759
|
-
descriptionMode="tooltip"
|
|
2760
|
-
>
|
|
2761
|
-
<div className="space-y-3">
|
|
2762
|
-
{form.signatures.map((signature, index) => (
|
|
2763
|
-
<div
|
|
2764
|
-
key={`signature-${index}`}
|
|
2765
|
-
className="grid gap-3 md:grid-cols-2 border-t pt-3"
|
|
2766
|
-
>
|
|
2767
|
-
<div className="space-y-1.5">
|
|
2768
|
-
<Label className="text-xs">Nome do signatario</Label>
|
|
2769
|
-
<Input
|
|
2770
|
-
value={signature.signerName}
|
|
2771
|
-
onChange={(event) =>
|
|
2772
|
-
setForm((current) => ({
|
|
2773
|
-
...current,
|
|
2774
|
-
signatures: current.signatures.map(
|
|
2775
|
-
(item, itemIndex) =>
|
|
2776
|
-
itemIndex === index
|
|
2777
|
-
? { ...item, signerName: event.target.value }
|
|
2778
|
-
: item
|
|
2779
|
-
),
|
|
2780
|
-
}))
|
|
2781
|
-
}
|
|
2782
|
-
/>
|
|
2783
|
-
</div>
|
|
2784
|
-
<div className="space-y-1.5">
|
|
2785
|
-
<Label className="text-xs">Papel</Label>
|
|
2786
|
-
<Input
|
|
2787
|
-
value={signature.signerRole}
|
|
2788
|
-
onChange={(event) =>
|
|
2789
|
-
setForm((current) => ({
|
|
2790
|
-
...current,
|
|
2791
|
-
signatures: current.signatures.map(
|
|
2792
|
-
(item, itemIndex) =>
|
|
2793
|
-
itemIndex === index
|
|
2794
|
-
? { ...item, signerRole: event.target.value }
|
|
2795
|
-
: item
|
|
2796
|
-
),
|
|
2797
|
-
}))
|
|
2798
|
-
}
|
|
2799
|
-
/>
|
|
2800
|
-
</div>
|
|
2801
|
-
<div className="space-y-1.5">
|
|
2802
|
-
<Label className="text-xs">Email</Label>
|
|
2803
|
-
<Input
|
|
2804
|
-
value={signature.signerEmail}
|
|
2805
|
-
onChange={(event) =>
|
|
2806
|
-
setForm((current) => ({
|
|
2807
|
-
...current,
|
|
2808
|
-
signatures: current.signatures.map(
|
|
2809
|
-
(item, itemIndex) =>
|
|
2810
|
-
itemIndex === index
|
|
2811
|
-
? { ...item, signerEmail: event.target.value }
|
|
2812
|
-
: item
|
|
2813
|
-
),
|
|
2814
|
-
}))
|
|
2815
|
-
}
|
|
2816
|
-
/>
|
|
2817
|
-
</div>
|
|
2818
|
-
<div className="space-y-1.5">
|
|
2819
|
-
<Label className="text-xs">Status</Label>
|
|
2820
|
-
<Select
|
|
2821
|
-
value={signature.status}
|
|
2822
|
-
onValueChange={(value) =>
|
|
2823
|
-
setForm((current) => ({
|
|
2824
|
-
...current,
|
|
2825
|
-
signatures: current.signatures.map(
|
|
2826
|
-
(item, itemIndex) =>
|
|
2827
|
-
itemIndex === index
|
|
2828
|
-
? { ...item, status: value }
|
|
2829
|
-
: item
|
|
2830
|
-
),
|
|
2831
|
-
}))
|
|
2832
|
-
}
|
|
2833
|
-
>
|
|
2834
|
-
<SelectTrigger className="w-full">
|
|
2835
|
-
<SelectValue />
|
|
2836
|
-
</SelectTrigger>
|
|
2837
|
-
<SelectContent>
|
|
2838
|
-
{['pending', 'signed', 'rejected'].map((value) => (
|
|
2839
|
-
<SelectItem key={value} value={value}>
|
|
2840
|
-
{getOptionLabel('signatureStatuses', value)}
|
|
2841
|
-
</SelectItem>
|
|
2842
|
-
))}
|
|
2843
|
-
</SelectContent>
|
|
2844
|
-
</Select>
|
|
2845
|
-
</div>
|
|
2846
|
-
{form.signatures.length > 1 ? (
|
|
2847
|
-
<div className="flex justify-end md:col-span-2">
|
|
2848
|
-
<Button
|
|
2849
|
-
type="button"
|
|
2850
|
-
variant="destructive"
|
|
2851
|
-
className="cursor-pointer"
|
|
2852
|
-
onClick={() =>
|
|
2853
|
-
setForm((current) => ({
|
|
2854
|
-
...current,
|
|
2855
|
-
signatures: current.signatures.filter(
|
|
2856
|
-
(_, itemIndex) => itemIndex !== index
|
|
2857
|
-
),
|
|
2858
|
-
}))
|
|
2859
|
-
}
|
|
2860
|
-
>
|
|
2861
|
-
<Trash2 className="size-4" />
|
|
2862
|
-
Remover signatario
|
|
2863
|
-
</Button>
|
|
2864
|
-
</div>
|
|
2865
|
-
) : null}
|
|
2866
|
-
</div>
|
|
2867
|
-
))}
|
|
2868
|
-
<Button
|
|
2869
|
-
type="button"
|
|
2870
|
-
variant="outline"
|
|
2871
|
-
className="cursor-pointer"
|
|
2872
|
-
onClick={() =>
|
|
2873
|
-
setForm((current) => ({
|
|
2874
|
-
...current,
|
|
2875
|
-
signatures: [...current.signatures, emptySignature()],
|
|
2876
|
-
}))
|
|
2877
|
-
}
|
|
2878
|
-
>
|
|
2879
|
-
<UserRound className="size-4" />
|
|
2880
|
-
Adicionar signatario
|
|
2881
|
-
</Button>
|
|
2882
|
-
</div>
|
|
2883
|
-
</SectionCard>
|
|
2884
|
-
</div>
|
|
2885
|
-
);
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
if (activeStep === 3) {
|
|
2889
|
-
return (
|
|
2890
|
-
<div className="space-y-4">
|
|
2891
|
-
<SectionCard
|
|
2892
|
-
title="Condicoes e vigencia"
|
|
2893
|
-
description="Deixe o contrato facil de criar. Tudo aqui pode ser ajustado na edicao."
|
|
2894
|
-
compact
|
|
2895
|
-
descriptionMode="tooltip"
|
|
2896
|
-
>
|
|
2897
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
2898
|
-
{contractDateFields.map(({ key, label }) => (
|
|
2899
|
-
<div key={key} className="space-y-1.5">
|
|
2900
|
-
<Label className="text-xs">{label}</Label>
|
|
2901
|
-
<Input
|
|
2902
|
-
type="date"
|
|
2903
|
-
value={form[key]}
|
|
2904
|
-
onChange={(event) =>
|
|
2905
|
-
setForm((current) => ({
|
|
2906
|
-
...current,
|
|
2907
|
-
[key]: event.target.value,
|
|
2908
|
-
}))
|
|
2909
|
-
}
|
|
2910
|
-
/>
|
|
2911
|
-
</div>
|
|
2912
|
-
))}
|
|
2913
|
-
<div className="space-y-1.5">
|
|
2914
|
-
<Label className="text-xs">Modelo de cobranca</Label>
|
|
2915
|
-
<Select
|
|
2916
|
-
value={form.billingModel}
|
|
2917
|
-
onValueChange={(value) =>
|
|
2918
|
-
setForm((current) => ({ ...current, billingModel: value }))
|
|
2919
|
-
}
|
|
2920
|
-
>
|
|
2921
|
-
<SelectTrigger className="w-full">
|
|
2922
|
-
<SelectValue />
|
|
2923
|
-
</SelectTrigger>
|
|
2924
|
-
<SelectContent>
|
|
2925
|
-
{[
|
|
2926
|
-
'time_and_material',
|
|
2927
|
-
'monthly_retainer',
|
|
2928
|
-
'fixed_price',
|
|
2929
|
-
].map((value) => (
|
|
2930
|
-
<SelectItem key={value} value={value}>
|
|
2931
|
-
{getOptionLabel('billingModels', value)}
|
|
2932
|
-
</SelectItem>
|
|
2933
|
-
))}
|
|
2934
|
-
</SelectContent>
|
|
2935
|
-
</Select>
|
|
2936
|
-
</div>
|
|
2937
|
-
<div className="space-y-1.5">
|
|
2938
|
-
<Label className="text-xs">Status</Label>
|
|
2939
|
-
<Select
|
|
2940
|
-
value={form.status}
|
|
2941
|
-
onValueChange={(value) =>
|
|
2942
|
-
setForm((current) => ({ ...current, status: value }))
|
|
2943
|
-
}
|
|
2944
|
-
>
|
|
2945
|
-
<SelectTrigger className="w-full">
|
|
2946
|
-
<SelectValue />
|
|
2947
|
-
</SelectTrigger>
|
|
2948
|
-
<SelectContent>
|
|
2949
|
-
{[
|
|
2950
|
-
'draft',
|
|
2951
|
-
'under_review',
|
|
2952
|
-
'active',
|
|
2953
|
-
'renewal',
|
|
2954
|
-
'expired',
|
|
2955
|
-
'closed',
|
|
2956
|
-
'archived',
|
|
2957
|
-
].map((value) => (
|
|
2958
|
-
<SelectItem key={value} value={value}>
|
|
2959
|
-
{getOptionLabel('statuses', value)}
|
|
2960
|
-
</SelectItem>
|
|
2961
|
-
))}
|
|
2962
|
-
</SelectContent>
|
|
2963
|
-
</Select>
|
|
2964
|
-
</div>
|
|
2965
|
-
<div className="space-y-1.5">
|
|
2966
|
-
<Label className="text-xs">Orcamento</Label>
|
|
2967
|
-
<Input
|
|
2968
|
-
value={form.budgetAmount}
|
|
2969
|
-
onChange={(event) =>
|
|
2970
|
-
setForm((current) => ({
|
|
2971
|
-
...current,
|
|
2972
|
-
budgetAmount: event.target.value,
|
|
2973
|
-
}))
|
|
2974
|
-
}
|
|
2975
|
-
/>
|
|
2976
|
-
</div>
|
|
2977
|
-
<div className="space-y-1.5">
|
|
2978
|
-
<Label className="text-xs">Limite mensal de horas</Label>
|
|
2979
|
-
<Input
|
|
2980
|
-
value={form.monthlyHourCap}
|
|
2981
|
-
onChange={(event) =>
|
|
2982
|
-
setForm((current) => ({
|
|
2983
|
-
...current,
|
|
2984
|
-
monthlyHourCap: event.target.value,
|
|
2985
|
-
}))
|
|
2986
|
-
}
|
|
2987
|
-
/>
|
|
2988
|
-
</div>
|
|
2989
|
-
<div className="flex items-center gap-2 pt-6">
|
|
2990
|
-
<Switch
|
|
2991
|
-
checked={form.isActive}
|
|
2992
|
-
onCheckedChange={(checked) =>
|
|
2993
|
-
setForm((current) => ({ ...current, isActive: checked }))
|
|
2994
|
-
}
|
|
2995
|
-
/>
|
|
2996
|
-
<span className="text-sm text-muted-foreground">
|
|
2997
|
-
Contrato ativo
|
|
2998
|
-
</span>
|
|
2999
|
-
</div>
|
|
3000
|
-
</div>
|
|
3001
|
-
</SectionCard>
|
|
3002
|
-
|
|
3003
|
-
<SectionCard
|
|
3004
|
-
title="Termos financeiros"
|
|
3005
|
-
description="Use somente os termos que fazem sentido para este rascunho."
|
|
3006
|
-
compact
|
|
3007
|
-
descriptionMode="tooltip"
|
|
3008
|
-
>
|
|
3009
|
-
<div className="space-y-3">
|
|
3010
|
-
{form.financialTerms.map((term, index) => (
|
|
3011
|
-
<div
|
|
3012
|
-
key={`term-${index}`}
|
|
3013
|
-
className="grid gap-3 border-t pt-3 md:grid-cols-2"
|
|
3014
|
-
>
|
|
3015
|
-
<div className="space-y-1.5">
|
|
3016
|
-
<Label className="text-xs">Rotulo</Label>
|
|
3017
|
-
<Input
|
|
3018
|
-
value={term.label}
|
|
3019
|
-
onChange={(event) =>
|
|
3020
|
-
setForm((current) => ({
|
|
3021
|
-
...current,
|
|
3022
|
-
financialTerms: current.financialTerms.map(
|
|
3023
|
-
(item, itemIndex) =>
|
|
3024
|
-
itemIndex === index
|
|
3025
|
-
? { ...item, label: event.target.value }
|
|
3026
|
-
: item
|
|
3027
|
-
),
|
|
3028
|
-
}))
|
|
3029
|
-
}
|
|
3030
|
-
/>
|
|
3031
|
-
</div>
|
|
3032
|
-
<div className="space-y-1.5">
|
|
3033
|
-
<Label className="text-xs">Tipo</Label>
|
|
3034
|
-
<Select
|
|
3035
|
-
value={term.termType}
|
|
3036
|
-
onValueChange={(value) =>
|
|
3037
|
-
setForm((current) => ({
|
|
3038
|
-
...current,
|
|
3039
|
-
financialTerms: current.financialTerms.map(
|
|
3040
|
-
(item, itemIndex) =>
|
|
3041
|
-
itemIndex === index
|
|
3042
|
-
? { ...item, termType: value }
|
|
3043
|
-
: item
|
|
3044
|
-
),
|
|
3045
|
-
}))
|
|
3046
|
-
}
|
|
3047
|
-
>
|
|
3048
|
-
<SelectTrigger className="w-full">
|
|
3049
|
-
<SelectValue />
|
|
3050
|
-
</SelectTrigger>
|
|
3051
|
-
<SelectContent>
|
|
3052
|
-
{['value', 'payment', 'revenue', 'fine', 'other'].map(
|
|
3053
|
-
(value) => (
|
|
3054
|
-
<SelectItem key={value} value={value}>
|
|
3055
|
-
{getOptionLabel('financialTermTypes', value)}
|
|
3056
|
-
</SelectItem>
|
|
3057
|
-
)
|
|
3058
|
-
)}
|
|
3059
|
-
</SelectContent>
|
|
3060
|
-
</Select>
|
|
3061
|
-
</div>
|
|
3062
|
-
<div className="space-y-1.5">
|
|
3063
|
-
<Label className="text-xs">Valor</Label>
|
|
3064
|
-
<InputMoney
|
|
3065
|
-
value={term.amount}
|
|
3066
|
-
onChange={(event) =>
|
|
3067
|
-
setForm((current) => ({
|
|
3068
|
-
...current,
|
|
3069
|
-
financialTerms: current.financialTerms.map(
|
|
3070
|
-
(item, itemIndex) =>
|
|
3071
|
-
itemIndex === index
|
|
3072
|
-
? { ...item, amount: event.target.value }
|
|
3073
|
-
: item
|
|
3074
|
-
),
|
|
3075
|
-
}))
|
|
3076
|
-
}
|
|
3077
|
-
/>
|
|
3078
|
-
</div>
|
|
3079
|
-
<div className="space-y-1.5">
|
|
3080
|
-
<Label className="text-xs">Recorrencia</Label>
|
|
3081
|
-
<Select
|
|
3082
|
-
value={term.recurrence}
|
|
3083
|
-
onValueChange={(value) =>
|
|
3084
|
-
setForm((current) => ({
|
|
3085
|
-
...current,
|
|
3086
|
-
financialTerms: current.financialTerms.map(
|
|
3087
|
-
(item, itemIndex) =>
|
|
3088
|
-
itemIndex === index
|
|
3089
|
-
? { ...item, recurrence: value }
|
|
3090
|
-
: item
|
|
3091
|
-
),
|
|
3092
|
-
}))
|
|
3093
|
-
}
|
|
3094
|
-
>
|
|
3095
|
-
<SelectTrigger className="w-full">
|
|
3096
|
-
<SelectValue />
|
|
3097
|
-
</SelectTrigger>
|
|
3098
|
-
<SelectContent>
|
|
3099
|
-
{[
|
|
3100
|
-
'one_time',
|
|
3101
|
-
'monthly',
|
|
3102
|
-
'quarterly',
|
|
3103
|
-
'yearly',
|
|
3104
|
-
'other',
|
|
3105
|
-
].map((value) => (
|
|
3106
|
-
<SelectItem key={value} value={value}>
|
|
3107
|
-
{getOptionLabel('recurrences', value)}
|
|
3108
|
-
</SelectItem>
|
|
3109
|
-
))}
|
|
3110
|
-
</SelectContent>
|
|
3111
|
-
</Select>
|
|
3112
|
-
</div>
|
|
3113
|
-
{form.financialTerms.length > 1 ? (
|
|
3114
|
-
<div className="flex justify-end md:col-span-2">
|
|
3115
|
-
<Button
|
|
3116
|
-
type="button"
|
|
3117
|
-
variant="outline"
|
|
3118
|
-
className="cursor-pointer"
|
|
3119
|
-
onClick={() =>
|
|
3120
|
-
setForm((current) => ({
|
|
3121
|
-
...current,
|
|
3122
|
-
financialTerms: current.financialTerms.filter(
|
|
3123
|
-
(_, itemIndex) => itemIndex !== index
|
|
3124
|
-
),
|
|
3125
|
-
}))
|
|
3126
|
-
}
|
|
3127
|
-
>
|
|
3128
|
-
Remover termo
|
|
3129
|
-
</Button>
|
|
3130
|
-
</div>
|
|
3131
|
-
) : null}
|
|
3132
|
-
</div>
|
|
3133
|
-
))}
|
|
3134
|
-
<Button
|
|
3135
|
-
type="button"
|
|
3136
|
-
variant="outline"
|
|
3137
|
-
className="cursor-pointer"
|
|
3138
|
-
onClick={() =>
|
|
3139
|
-
setForm((current) => ({
|
|
3140
|
-
...current,
|
|
3141
|
-
financialTerms: [
|
|
3142
|
-
...current.financialTerms,
|
|
3143
|
-
emptyFinancialTerm(),
|
|
3144
|
-
],
|
|
3145
|
-
}))
|
|
3146
|
-
}
|
|
3147
|
-
>
|
|
3148
|
-
<BadgeDollarSign className="size-4" />
|
|
3149
|
-
Adicionar termo financeiro
|
|
3150
|
-
</Button>
|
|
3151
|
-
</div>
|
|
3152
|
-
</SectionCard>
|
|
3153
|
-
</div>
|
|
3154
|
-
);
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
if (contentStepIndex >= 0 && activeStep === contentStepIndex) {
|
|
3158
|
-
return (
|
|
3159
|
-
<div className="space-y-4">
|
|
3160
|
-
<SectionCard
|
|
3161
|
-
title="Revisao juridica orientativa"
|
|
3162
|
-
description="Use o checklist como apoio para a revisao do texto. Ele nao bloqueia a edicao nem substitui a validacao juridica final."
|
|
3163
|
-
compact
|
|
3164
|
-
descriptionMode="tooltip"
|
|
3165
|
-
>
|
|
3166
|
-
<div className="space-y-3">
|
|
3167
|
-
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
3168
|
-
<p className="text-sm text-muted-foreground">
|
|
3169
|
-
{aiReview?.reviewedAt
|
|
3170
|
-
? `Ultima atualizacao: ${formatDate(aiReview.reviewedAt)}`
|
|
3171
|
-
: 'Atualize a revisao sempre que mudar clausulas sensiveis.'}
|
|
3172
|
-
</p>
|
|
3173
|
-
<Button
|
|
3174
|
-
type="button"
|
|
3175
|
-
variant="outline"
|
|
3176
|
-
className="cursor-pointer"
|
|
3177
|
-
disabled={isBusy || !draftId}
|
|
3178
|
-
onClick={() => void handleRunLegalReview(draftId)}
|
|
3179
|
-
>
|
|
3180
|
-
{isBusy ? (
|
|
3181
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
3182
|
-
) : (
|
|
3183
|
-
<Sparkles className="size-4" />
|
|
3184
|
-
)}
|
|
3185
|
-
Atualizar checklist
|
|
3186
|
-
</Button>
|
|
3187
|
-
</div>
|
|
3188
|
-
|
|
3189
|
-
{aiReview ? (
|
|
3190
|
-
<div className="space-y-3 rounded-2xl border bg-muted/20 p-4 text-sm">
|
|
3191
|
-
{aiReview.summary ? <p>{aiReview.summary}</p> : null}
|
|
3192
|
-
|
|
3193
|
-
{aiReview.checklist.length ? (
|
|
3194
|
-
<div className="space-y-1">
|
|
3195
|
-
<div className="font-medium">Checklist</div>
|
|
3196
|
-
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
3197
|
-
{aiReview.checklist.map((item) => (
|
|
3198
|
-
<li key={item}>{item}</li>
|
|
3199
|
-
))}
|
|
3200
|
-
</ul>
|
|
3201
|
-
</div>
|
|
3202
|
-
) : null}
|
|
3203
|
-
|
|
3204
|
-
{aiReview.missingFields.length ? (
|
|
3205
|
-
<div className="space-y-1">
|
|
3206
|
-
<div className="font-medium">Pontos faltantes</div>
|
|
3207
|
-
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
3208
|
-
{aiReview.missingFields.map((item) => (
|
|
3209
|
-
<li key={item}>{item}</li>
|
|
3210
|
-
))}
|
|
3211
|
-
</ul>
|
|
3212
|
-
</div>
|
|
3213
|
-
) : null}
|
|
3214
|
-
|
|
3215
|
-
{aiReview.warnings.length ? (
|
|
3216
|
-
<div className="space-y-1">
|
|
3217
|
-
<div className="font-medium">Alertas</div>
|
|
3218
|
-
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
3219
|
-
{aiReview.warnings.map((item) => (
|
|
3220
|
-
<li key={item}>{item}</li>
|
|
3221
|
-
))}
|
|
3222
|
-
</ul>
|
|
3223
|
-
</div>
|
|
3224
|
-
) : null}
|
|
3225
|
-
</div>
|
|
3226
|
-
) : (
|
|
3227
|
-
<p className="text-sm text-muted-foreground">
|
|
3228
|
-
Gere ou atualize o checklist para receber uma revisao juridica
|
|
3229
|
-
orientativa do contrato em edicao.
|
|
3230
|
-
</p>
|
|
3231
|
-
)}
|
|
3232
|
-
</div>
|
|
3233
|
-
</SectionCard>
|
|
3234
|
-
|
|
3235
|
-
<ContractContentEditor
|
|
3236
|
-
value={form.contentHtml}
|
|
3237
|
-
onChange={(value) =>
|
|
3238
|
-
setForm((current) => ({ ...current, contentHtml: value }))
|
|
3239
|
-
}
|
|
3240
|
-
editorTitle="Texto do contrato"
|
|
3241
|
-
editorDescription="Edite o texto final em RichText."
|
|
3242
|
-
previewTitle="Preview"
|
|
3243
|
-
previewDescription="Visualize como o contrato esta ficando."
|
|
3244
|
-
previewFallbackHtml={
|
|
3245
|
-
form.description
|
|
3246
|
-
? `<p>${form.description.replace(/\n/g, '<br />')}</p>`
|
|
3247
|
-
: undefined
|
|
3248
|
-
}
|
|
3249
|
-
promptContext={{
|
|
3250
|
-
name: form.name,
|
|
3251
|
-
client_name: form.clientName,
|
|
3252
|
-
start_date: form.startDate,
|
|
3253
|
-
end_date: form.endDate,
|
|
3254
|
-
budget_amount: form.budgetAmount,
|
|
3255
|
-
monthly_hour_cap: form.monthlyHourCap,
|
|
3256
|
-
}}
|
|
3257
|
-
compact
|
|
3258
|
-
descriptionMode="tooltip"
|
|
3259
|
-
layout="tabs"
|
|
3260
|
-
/>
|
|
3261
|
-
</div>
|
|
3262
|
-
);
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
return (
|
|
3266
|
-
<div className="space-y-4">
|
|
3267
|
-
<SectionCard
|
|
3268
|
-
title="Resumo do rascunho"
|
|
3269
|
-
description="Veja o que ja foi preenchido e conclua o contrato quando estiver satisfeito."
|
|
3270
|
-
compact
|
|
3271
|
-
descriptionMode="tooltip"
|
|
3272
|
-
>
|
|
3273
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
3274
|
-
{[
|
|
3275
|
-
['Codigo', form.code || 'Nao informado'],
|
|
3276
|
-
['Titulo', form.name || 'Nao informado'],
|
|
3277
|
-
['Cliente', form.clientName || 'Nao informado'],
|
|
3278
|
-
[
|
|
3279
|
-
'Vigencia',
|
|
3280
|
-
form.startDate
|
|
3281
|
-
? `${formatDate(form.startDate)} - ${formatDate(form.endDate)}`
|
|
3282
|
-
: 'Nao informada',
|
|
3283
|
-
],
|
|
3284
|
-
[
|
|
3285
|
-
'Financeiro',
|
|
3286
|
-
form.budgetAmount
|
|
3287
|
-
? formatCurrency(Number(form.budgetAmount || 0))
|
|
3288
|
-
: 'Opcional',
|
|
3289
|
-
],
|
|
3290
|
-
['Modo', getOptionLabel('creationModes', creationMode)],
|
|
3291
|
-
].map(([label, value]) => (
|
|
3292
|
-
<div key={label} className="rounded-2xl border bg-muted/20 p-3">
|
|
3293
|
-
<div className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
3294
|
-
{label}
|
|
3295
|
-
</div>
|
|
3296
|
-
<div className="mt-1 text-sm font-medium">{value}</div>
|
|
3297
|
-
</div>
|
|
3298
|
-
))}
|
|
3299
|
-
</div>
|
|
3300
|
-
</SectionCard>
|
|
3301
|
-
|
|
3302
|
-
{isCreateFlow ? (
|
|
3303
|
-
<SectionCard
|
|
3304
|
-
title="Proximo passo apos concluir"
|
|
3305
|
-
description="Ao concluir o cadastro, o sistema gera automaticamente o conteudo inicial do contrato e um checklist juridico orientativo para voce revisar no modo de edicao."
|
|
3306
|
-
compact
|
|
3307
|
-
descriptionMode="tooltip"
|
|
3308
|
-
>
|
|
3309
|
-
<p className="text-sm text-muted-foreground">
|
|
3310
|
-
O fluxo de criacao termina aqui. O texto do contrato fica
|
|
3311
|
-
concentrado na etapa de edicao, com apoio de IA para revisao e
|
|
3312
|
-
ajustes.
|
|
3313
|
-
</p>
|
|
3314
|
-
</SectionCard>
|
|
3315
|
-
) : null}
|
|
3316
|
-
|
|
3317
|
-
{form.sourceDocument ? (
|
|
3318
|
-
<SectionCard
|
|
3319
|
-
title="Documento fonte"
|
|
3320
|
-
description="O documento original permanece relacionado ao contrato."
|
|
3321
|
-
compact
|
|
3322
|
-
descriptionMode="tooltip"
|
|
3323
|
-
>
|
|
3324
|
-
<div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border p-4">
|
|
3325
|
-
<div>
|
|
3326
|
-
<div className="font-medium">
|
|
3327
|
-
{form.sourceDocument.fileName}
|
|
3328
|
-
</div>
|
|
3329
|
-
<div className="text-sm text-muted-foreground">
|
|
3330
|
-
Extracao:{' '}
|
|
3331
|
-
{getOptionLabel(
|
|
3332
|
-
'extractionStatuses',
|
|
3333
|
-
form.sourceDocument.extractionStatus
|
|
3334
|
-
)}
|
|
3335
|
-
</div>
|
|
3336
|
-
</div>
|
|
3337
|
-
{form.sourceDocument.fileId ? (
|
|
3338
|
-
<Button
|
|
3339
|
-
type="button"
|
|
3340
|
-
variant="outline"
|
|
3341
|
-
className="cursor-pointer"
|
|
3342
|
-
onClick={() => openStoredFile(form.sourceDocument?.fileId)}
|
|
3343
|
-
>
|
|
3344
|
-
<Download className="size-4" />
|
|
3345
|
-
Abrir fonte
|
|
3346
|
-
</Button>
|
|
3347
|
-
) : null}
|
|
3348
|
-
</div>
|
|
3349
|
-
</SectionCard>
|
|
3350
|
-
) : null}
|
|
3351
|
-
|
|
3352
|
-
{form.generatedPdfDocument ? (
|
|
3353
|
-
<SectionCard
|
|
3354
|
-
title="PDF gerado"
|
|
3355
|
-
description="O PDF timbrado mais recente fica salvo junto ao contrato."
|
|
3356
|
-
compact
|
|
3357
|
-
descriptionMode="tooltip"
|
|
3358
|
-
>
|
|
3359
|
-
<div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border p-4">
|
|
3360
|
-
<div>
|
|
3361
|
-
<div className="font-medium">
|
|
3362
|
-
{form.generatedPdfDocument.fileName}
|
|
3363
|
-
</div>
|
|
3364
|
-
<div className="text-sm text-muted-foreground">
|
|
3365
|
-
Pronto para download.
|
|
3366
|
-
</div>
|
|
3367
|
-
</div>
|
|
3368
|
-
{form.generatedPdfDocument.fileId ? (
|
|
3369
|
-
<Button
|
|
3370
|
-
type="button"
|
|
3371
|
-
variant="outline"
|
|
3372
|
-
className="cursor-pointer"
|
|
3373
|
-
onClick={() =>
|
|
3374
|
-
openStoredFile(form.generatedPdfDocument?.fileId)
|
|
3375
|
-
}
|
|
3376
|
-
>
|
|
3377
|
-
<Download className="size-4" />
|
|
3378
|
-
Baixar PDF
|
|
3379
|
-
</Button>
|
|
3380
|
-
) : null}
|
|
3381
|
-
</div>
|
|
3382
|
-
</SectionCard>
|
|
3383
|
-
) : null}
|
|
3384
|
-
</div>
|
|
3385
|
-
);
|
|
3386
|
-
})();
|
|
3387
|
-
|
|
3388
|
-
return (
|
|
3389
|
-
<div className="flex h-full flex-col">
|
|
3390
|
-
<div className="border-b px-4 py-4">
|
|
3391
|
-
<div className="flex items-start gap-3">
|
|
3392
|
-
<div className="rounded-2xl bg-primary/10 p-2.5 text-primary">
|
|
3393
|
-
{currentStep ? <currentStep.icon className="size-5" /> : null}
|
|
3394
|
-
</div>
|
|
3395
|
-
<div className="min-w-0 flex-1">
|
|
3396
|
-
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary/80">
|
|
3397
|
-
Contratos
|
|
3398
|
-
</div>
|
|
3399
|
-
<h2 className="text-lg font-semibold">
|
|
3400
|
-
{draftId
|
|
3401
|
-
? form.name || form.code || 'Rascunho de contrato'
|
|
3402
|
-
: isCreateFlow
|
|
3403
|
-
? 'Novo contrato'
|
|
3404
|
-
: 'Contrato'}
|
|
3405
|
-
</h2>
|
|
3406
|
-
</div>
|
|
3407
|
-
</div>
|
|
3408
|
-
|
|
3409
|
-
<div className="mt-4 grid grid-cols-3 gap-2 sm:grid-cols-6">
|
|
3410
|
-
{wizardSteps.map((step, index) => {
|
|
3411
|
-
const Icon = step.icon;
|
|
3412
|
-
const isCurrent = index === activeStep;
|
|
3413
|
-
const isEnabled = draftId !== null || index === 0 || contractId;
|
|
3414
|
-
return (
|
|
3415
|
-
<button
|
|
3416
|
-
key={step.key}
|
|
3417
|
-
type="button"
|
|
3418
|
-
disabled={!isEnabled}
|
|
3419
|
-
className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-left text-xs transition-all ${
|
|
3420
|
-
isCurrent
|
|
3421
|
-
? 'border-primary bg-primary/10 text-primary'
|
|
3422
|
-
: 'text-muted-foreground hover:border-primary/30 hover:text-foreground'
|
|
3423
|
-
} ${!isEnabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
|
3424
|
-
onClick={() => {
|
|
3425
|
-
if (!isEnabled) return;
|
|
3426
|
-
setActiveStep(index);
|
|
3427
|
-
}}
|
|
3428
|
-
>
|
|
3429
|
-
<Icon className="size-3.5" />
|
|
3430
|
-
<span className="truncate">{step.label}</span>
|
|
3431
|
-
</button>
|
|
3432
|
-
);
|
|
3433
|
-
})}
|
|
3434
|
-
</div>
|
|
3435
|
-
</div>
|
|
3436
|
-
|
|
3437
|
-
<div className="flex-1 overflow-y-auto px-4 py-4">{stepContent}</div>
|
|
3438
|
-
|
|
3439
|
-
<div className="border-t bg-background px-4 py-3">
|
|
3440
|
-
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
3441
|
-
<Button
|
|
3442
|
-
type="button"
|
|
3443
|
-
variant="outline"
|
|
3444
|
-
className="cursor-pointer"
|
|
3445
|
-
onClick={
|
|
3446
|
-
activeStep === 0
|
|
3447
|
-
? onCancel
|
|
3448
|
-
: () => setActiveStep((current) => Math.max(current - 1, 0))
|
|
3449
|
-
}
|
|
3450
|
-
>
|
|
3451
|
-
<ChevronLeft className="size-4" />
|
|
3452
|
-
{activeStep === 0 ? 'Cancelar' : 'Voltar'}
|
|
3453
|
-
</Button>
|
|
3454
|
-
|
|
3455
|
-
<div className="flex flex-wrap gap-2">
|
|
3456
|
-
{draftId ? (
|
|
3457
|
-
<Button
|
|
3458
|
-
type="button"
|
|
3459
|
-
variant="outline"
|
|
3460
|
-
className="cursor-pointer"
|
|
3461
|
-
disabled={isBusy}
|
|
3462
|
-
onClick={() => void saveDraft(activeStep)}
|
|
3463
|
-
>
|
|
3464
|
-
{isBusy ? (
|
|
3465
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
3466
|
-
) : (
|
|
3467
|
-
<Save className="size-4" />
|
|
3468
|
-
)}
|
|
3469
|
-
Salvar rascunho
|
|
3470
|
-
</Button>
|
|
3471
|
-
) : null}
|
|
3472
|
-
|
|
3473
|
-
{draftId &&
|
|
3474
|
-
Boolean(form.contentHtml?.trim()) &&
|
|
3475
|
-
(contentStepIndex < 0 || activeStep >= contentStepIndex) ? (
|
|
3476
|
-
<Button
|
|
3477
|
-
type="button"
|
|
3478
|
-
variant="outline"
|
|
3479
|
-
className="cursor-pointer"
|
|
3480
|
-
disabled={isGeneratingPdf}
|
|
3481
|
-
onClick={() => void handleGeneratePdf()}
|
|
3482
|
-
>
|
|
3483
|
-
{isGeneratingPdf ? (
|
|
3484
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
3485
|
-
) : (
|
|
3486
|
-
<Download className="size-4" />
|
|
3487
|
-
)}
|
|
3488
|
-
Gerar PDF
|
|
3489
|
-
</Button>
|
|
3490
|
-
) : null}
|
|
3491
|
-
|
|
3492
|
-
{activeStep === wizardSteps.length - 1 ? (
|
|
3493
|
-
<Button
|
|
3494
|
-
type="button"
|
|
3495
|
-
className="cursor-pointer"
|
|
3496
|
-
disabled={isBusy}
|
|
3497
|
-
onClick={() => void handleComplete()}
|
|
3498
|
-
>
|
|
3499
|
-
Concluir
|
|
3500
|
-
</Button>
|
|
3501
|
-
) : (
|
|
3502
|
-
<Button
|
|
3503
|
-
type="button"
|
|
3504
|
-
className="cursor-pointer"
|
|
3505
|
-
disabled={isBusy || isExtracting}
|
|
3506
|
-
onClick={() => void handleNext()}
|
|
3507
|
-
>
|
|
3508
|
-
{isBusy ? (
|
|
3509
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
3510
|
-
) : null}
|
|
3511
|
-
Proximo
|
|
3512
|
-
<ChevronRight className="size-4" />
|
|
3513
|
-
</Button>
|
|
3514
|
-
)}
|
|
3515
|
-
</div>
|
|
3516
|
-
</div>
|
|
3517
|
-
</div>
|
|
3518
|
-
</div>
|
|
3519
|
-
);
|
|
3520
|
-
}
|