@hed-hog/operations 0.0.306 → 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.
Files changed (123) hide show
  1. package/dist/controllers/operations-approvals.controller.d.ts +114 -1
  2. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-approvals.controller.js +16 -3
  4. package/dist/controllers/operations-approvals.controller.js.map +1 -1
  5. package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +16 -3
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +14 -453
  10. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-contracts.controller.js +11 -112
  12. package/dist/controllers/operations-contracts.controller.js.map +1 -1
  13. package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
  14. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
  15. package/dist/controllers/operations-org-structure.controller.js +18 -5
  16. package/dist/controllers/operations-org-structure.controller.js.map +1 -1
  17. package/dist/controllers/operations-projects.controller.d.ts +28 -4
  18. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-projects.controller.js +17 -5
  20. package/dist/controllers/operations-projects.controller.js.map +1 -1
  21. package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +16 -11
  24. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  25. package/dist/dto/list-approvals.dto.d.ts +6 -0
  26. package/dist/dto/list-approvals.dto.d.ts.map +1 -0
  27. package/dist/dto/list-approvals.dto.js +28 -0
  28. package/dist/dto/list-approvals.dto.js.map +1 -0
  29. package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
  30. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
  31. package/dist/dto/list-collaborator-types.dto.js +7 -1
  32. package/dist/dto/list-collaborator-types.dto.js.map +1 -1
  33. package/dist/dto/list-collaborators.dto.d.ts +1 -0
  34. package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
  35. package/dist/dto/list-collaborators.dto.js +5 -0
  36. package/dist/dto/list-collaborators.dto.js.map +1 -1
  37. package/dist/dto/list-contracts.dto.d.ts +8 -0
  38. package/dist/dto/list-contracts.dto.d.ts.map +1 -0
  39. package/dist/dto/list-contracts.dto.js +38 -0
  40. package/dist/dto/list-contracts.dto.js.map +1 -0
  41. package/dist/dto/list-departments.dto.d.ts +5 -0
  42. package/dist/dto/list-departments.dto.d.ts.map +1 -0
  43. package/dist/dto/list-departments.dto.js +23 -0
  44. package/dist/dto/list-departments.dto.js.map +1 -0
  45. package/dist/dto/list-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-projects.dto.js +23 -0
  48. package/dist/dto/list-projects.dto.js.map +1 -0
  49. package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
  50. package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
  51. package/dist/dto/list-schedule-adjustments.dto.js +23 -0
  52. package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
  53. package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
  54. package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
  55. package/dist/dto/list-time-off-requests.dto.js +23 -0
  56. package/dist/dto/list-time-off-requests.dto.js.map +1 -0
  57. package/dist/dto/list-timesheets.dto.d.ts +5 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
  59. package/dist/dto/list-timesheets.dto.js +23 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -0
  61. package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
  62. package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
  63. package/dist/dto/reorder-collaborator-types.dto.js +25 -0
  64. package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
  65. package/dist/operations.service.d.ts +340 -271
  66. package/dist/operations.service.d.ts.map +1 -1
  67. package/dist/operations.service.js +1007 -1043
  68. package/dist/operations.service.js.map +1 -1
  69. package/dist/operations.service.spec.js +0 -22
  70. package/dist/operations.service.spec.js.map +1 -1
  71. package/hedhog/data/menu.yaml +0 -36
  72. package/hedhog/data/route.yaml +42 -73
  73. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  74. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  75. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  76. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  77. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  78. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  79. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  80. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  81. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  82. package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
  83. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
  84. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  85. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  87. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  88. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  89. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
  90. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  91. package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
  92. package/hedhog/frontend/messages/en.json +143 -14
  93. package/hedhog/frontend/messages/pt.json +192 -54
  94. package/hedhog/table/operations_contract.yaml +0 -9
  95. package/package.json +5 -5
  96. package/src/controllers/operations-approvals.controller.ts +9 -3
  97. package/src/controllers/operations-collaborators.controller.ts +15 -2
  98. package/src/controllers/operations-contracts.controller.ts +8 -92
  99. package/src/controllers/operations-org-structure.controller.ts +17 -4
  100. package/src/controllers/operations-projects.controller.ts +10 -4
  101. package/src/controllers/operations-timesheets.controller.ts +17 -8
  102. package/src/dto/list-approvals.dto.ts +12 -0
  103. package/src/dto/list-collaborator-types.dto.ts +7 -2
  104. package/src/dto/list-collaborators.dto.ts +4 -0
  105. package/src/dto/list-contracts.dto.ts +20 -0
  106. package/src/dto/list-departments.dto.ts +8 -0
  107. package/src/dto/list-projects.dto.ts +8 -0
  108. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  109. package/src/dto/list-time-off-requests.dto.ts +8 -0
  110. package/src/dto/list-timesheets.dto.ts +8 -0
  111. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  112. package/src/operations.service.spec.ts +0 -30
  113. package/src/operations.service.ts +1557 -1806
  114. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  115. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  116. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  117. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  118. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  119. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  120. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  121. package/hedhog/table/operations_contract_revision.yaml +0 -38
  122. package/hedhog/table/operations_contract_signature.yaml +0 -38
  123. package/hedhog/table/operations_contract_template.yaml +0 -58
@@ -1,631 +0,0 @@
1
- 'use client';
2
-
3
- import { Button } from '@/components/ui/button';
4
- import {
5
- Select,
6
- SelectContent,
7
- SelectItem,
8
- SelectTrigger,
9
- SelectValue,
10
- } from '@/components/ui/select';
11
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
12
- import {
13
- FileText,
14
- LayoutTemplate,
15
- LoaderCircle,
16
- Sparkles,
17
- WandSparkles,
18
- } from 'lucide-react';
19
- import { useTranslations } from 'next-intl';
20
- import { useMemo, useState } from 'react';
21
- import { fetchOperations, mutateOperations } from '../_lib/api';
22
- import type { OperationsContractTemplate } from '../_lib/types';
23
- import type { ContractFormState } from './contract-form-screen';
24
- import { SectionCard } from './section-card';
25
-
26
- type CreationMode = 'upload' | 'template' | 'blank';
27
-
28
- type ExtractedContractDraft = Partial<ContractFormState> & {
29
- summary?: string;
30
- missingFields?: string[];
31
- warnings?: string[];
32
- };
33
-
34
- type ContractCreationWizardProps = {
35
- onCancel?: () => void;
36
- onComplete: (options: {
37
- initialTemplateId?: number;
38
- initialValues?: Partial<ContractFormState>;
39
- }) => void;
40
- };
41
-
42
- function fileToBase64(file: File) {
43
- return new Promise<string>((resolve, reject) => {
44
- const reader = new FileReader();
45
- reader.onload = () => {
46
- const result = String(reader.result ?? '');
47
- const [, base64 = ''] = result.split(',');
48
- resolve(base64);
49
- };
50
- reader.onerror = reject;
51
- reader.readAsDataURL(file);
52
- });
53
- }
54
-
55
- function inferMimeType(file: File) {
56
- if (file.type) {
57
- return file.type;
58
- }
59
-
60
- const name = file.name.toLowerCase();
61
- if (name.endsWith('.docx')) {
62
- return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
63
- }
64
- if (name.endsWith('.doc')) {
65
- return 'application/msword';
66
- }
67
-
68
- return 'application/pdf';
69
- }
70
-
71
- function toTextValue(value: unknown) {
72
- return typeof value === 'string' ? value : value == null ? '' : String(value);
73
- }
74
-
75
- function toTextArray(value: unknown) {
76
- return Array.isArray(value)
77
- ? value.map((item) => toTextValue(item).trim()).filter(Boolean)
78
- : [];
79
- }
80
-
81
- function ensureHtml(value: unknown) {
82
- const content = toTextValue(value).trim();
83
- if (!content) {
84
- return '';
85
- }
86
-
87
- if (/<[a-z][\s\S]*>/i.test(content)) {
88
- return content;
89
- }
90
-
91
- return content
92
- .split(/\n{2,}/)
93
- .map((paragraph) => `<p>${paragraph.replace(/\n/g, '<br />')}</p>`)
94
- .join('');
95
- }
96
-
97
- function asRecord(value: unknown): Record<string, unknown> {
98
- return value && typeof value === 'object'
99
- ? (value as Record<string, unknown>)
100
- : {};
101
- }
102
-
103
- function buildInitialValues(
104
- draft: ExtractedContractDraft,
105
- filePayload?: ContractFormState['pdfDocument']
106
- ): Partial<ContractFormState> {
107
- const parties = Array.isArray(draft.parties)
108
- ? draft.parties
109
- .map((party) => {
110
- const partyRecord = asRecord(party);
111
-
112
- return {
113
- displayName: toTextValue(partyRecord.displayName),
114
- partyRole: toTextValue(partyRecord.partyRole || 'client'),
115
- partyType: toTextValue(partyRecord.partyType || 'company'),
116
- documentNumber: toTextValue(partyRecord.documentNumber),
117
- email: toTextValue(partyRecord.email),
118
- phone: toTextValue(partyRecord.phone),
119
- isPrimary: Boolean(partyRecord.isPrimary),
120
- };
121
- })
122
- .filter((party) => party.displayName)
123
- : [];
124
-
125
- const signatures = Array.isArray(draft.signatures)
126
- ? draft.signatures
127
- .map((signature) => {
128
- const signatureRecord = asRecord(signature);
129
-
130
- return {
131
- signerName: toTextValue(signatureRecord.signerName),
132
- signerRole: toTextValue(signatureRecord.signerRole),
133
- signerEmail: toTextValue(signatureRecord.signerEmail),
134
- status: toTextValue(signatureRecord.status || 'pending'),
135
- signedAt: toTextValue(signatureRecord.signedAt),
136
- };
137
- })
138
- .filter((signature) => signature.signerName)
139
- : [];
140
-
141
- const financialTerms = Array.isArray(draft.financialTerms)
142
- ? draft.financialTerms
143
- .map((term) => {
144
- const termRecord = asRecord(term);
145
-
146
- return {
147
- label: toTextValue(termRecord.label),
148
- termType: toTextValue(termRecord.termType || 'value'),
149
- amount: toTextValue(termRecord.amount),
150
- recurrence: toTextValue(termRecord.recurrence || 'one_time'),
151
- dueDay: toTextValue(termRecord.dueDay),
152
- notes: toTextValue(termRecord.notes),
153
- };
154
- })
155
- .filter((term) => term.label)
156
- : [];
157
-
158
- const revisions = Array.isArray(draft.revisions)
159
- ? draft.revisions
160
- .map((revision) => {
161
- const revisionRecord = asRecord(revision);
162
-
163
- return {
164
- title: toTextValue(revisionRecord.title),
165
- revisionType: toTextValue(
166
- revisionRecord.revisionType || 'revision'
167
- ),
168
- effectiveDate: toTextValue(revisionRecord.effectiveDate),
169
- status: toTextValue(revisionRecord.status || 'draft'),
170
- summary: toTextValue(revisionRecord.summary),
171
- };
172
- })
173
- .filter((revision) => revision.title)
174
- : [];
175
-
176
- return {
177
- code: toTextValue(draft.code),
178
- name: toTextValue(draft.name),
179
- clientName: toTextValue(draft.clientName),
180
- contractCategory:
181
- toTextValue(draft.contractCategory || 'client') || 'client',
182
- contractType:
183
- toTextValue(draft.contractType || 'service_agreement') ||
184
- 'service_agreement',
185
- signatureStatus:
186
- toTextValue(draft.signatureStatus || 'not_started') || 'not_started',
187
- isActive: draft.isActive ?? true,
188
- billingModel:
189
- toTextValue(draft.billingModel || 'time_and_material') ||
190
- 'time_and_material',
191
- originType: toTextValue(draft.originType || 'manual') || 'manual',
192
- originId: toTextValue(draft.originId),
193
- startDate: toTextValue(draft.startDate),
194
- endDate: toTextValue(draft.endDate),
195
- signedAt: toTextValue(draft.signedAt),
196
- effectiveDate: toTextValue(draft.effectiveDate),
197
- budgetAmount: toTextValue(draft.budgetAmount),
198
- monthlyHourCap: toTextValue(draft.monthlyHourCap),
199
- status: toTextValue(draft.status || 'draft') || 'draft',
200
- description: toTextValue(draft.description || draft.summary),
201
- contentHtml: ensureHtml(draft.contentHtml),
202
- parties,
203
- signatures,
204
- financialTerms,
205
- revisions,
206
- pdfDocument: filePayload ?? null,
207
- };
208
- }
209
-
210
- export function ContractCreationWizard({
211
- onCancel,
212
- onComplete,
213
- }: ContractCreationWizardProps) {
214
- const t = useTranslations('operations.ContractsPage');
215
- const commonT = useTranslations('operations.Common');
216
- const { request, showToastHandler, getSettingValue, currentLocaleCode } =
217
- useApp();
218
- const [mode, setMode] = useState<CreationMode | null>(null);
219
- const [selectedTemplateId, setSelectedTemplateId] = useState<string>('none');
220
- const [selectedFile, setSelectedFile] = useState<File | null>(null);
221
- const [draft, setDraft] = useState<ExtractedContractDraft | null>(null);
222
- const [isExtracting, setIsExtracting] = useState(false);
223
- const [isContinuing, setIsContinuing] = useState(false);
224
-
225
- const { data: contractTemplates = [] } = useQuery<
226
- OperationsContractTemplate[]
227
- >({
228
- queryKey: [
229
- 'operations-contract-create-wizard-templates',
230
- currentLocaleCode,
231
- ],
232
- queryFn: () =>
233
- fetchOperations<OperationsContractTemplate[]>(
234
- request,
235
- '/operations/contract-templates'
236
- ),
237
- });
238
-
239
- const hasOpenAi = Boolean(getSettingValue('ai-openai-api-key-enabled'));
240
- const hasGemini = Boolean(getSettingValue('ai-gemini-api-key-enabled'));
241
- const provider = hasOpenAi ? 'openai' : hasGemini ? 'gemini' : null;
242
- const providerLabel = provider === 'openai' ? 'OpenAI' : 'Gemini';
243
-
244
- const selectedTemplate = useMemo(
245
- () =>
246
- contractTemplates.find(
247
- (template) => String(template.id) === selectedTemplateId
248
- ) ?? null,
249
- [contractTemplates, selectedTemplateId]
250
- );
251
-
252
- const resetUploadState = () => {
253
- setSelectedFile(null);
254
- setDraft(null);
255
- setIsExtracting(false);
256
- };
257
-
258
- const handleModeChange = (nextMode: CreationMode) => {
259
- setMode(nextMode);
260
- if (nextMode !== 'upload') {
261
- resetUploadState();
262
- }
263
- if (nextMode !== 'template') {
264
- setSelectedTemplateId('none');
265
- }
266
- };
267
-
268
- const handleExtractDraft = async () => {
269
- if (!selectedFile) {
270
- showToastHandler?.('error', t('wizard.messages.missingFile'));
271
- return;
272
- }
273
-
274
- if (!provider) {
275
- showToastHandler?.('error', t('wizard.messages.missingAiConfiguration'));
276
- return;
277
- }
278
-
279
- setIsExtracting(true);
280
-
281
- try {
282
- const fileContentBase64 = await fileToBase64(selectedFile);
283
- const parsedDraft = await mutateOperations<ExtractedContractDraft>(
284
- request,
285
- '/operations/contracts/extract-draft',
286
- 'POST',
287
- {
288
- provider,
289
- promptMessage: t('wizard.upload.promptMessage'),
290
- fileName: selectedFile.name,
291
- mimeType: inferMimeType(selectedFile),
292
- fileContentBase64,
293
- }
294
- );
295
-
296
- setDraft(parsedDraft);
297
- showToastHandler?.(
298
- 'success',
299
- t('wizard.messages.extractSuccess', { provider: providerLabel })
300
- );
301
- } catch {
302
- setDraft(null);
303
- showToastHandler?.('error', t('wizard.messages.extractError'));
304
- } finally {
305
- setIsExtracting(false);
306
- }
307
- };
308
-
309
- const continueWithUpload = async () => {
310
- if (!selectedFile) {
311
- showToastHandler?.('error', t('wizard.messages.missingFile'));
312
- return;
313
- }
314
-
315
- setIsContinuing(true);
316
-
317
- try {
318
- const fileContentBase64 = await fileToBase64(selectedFile);
319
- const initialValues = buildInitialValues(draft ?? {}, {
320
- fileName: selectedFile.name,
321
- mimeType: inferMimeType(selectedFile),
322
- fileContentBase64,
323
- });
324
-
325
- onComplete({ initialValues });
326
- } finally {
327
- setIsContinuing(false);
328
- }
329
- };
330
-
331
- const renderModeCard = (
332
- value: CreationMode,
333
- icon: React.ReactNode,
334
- titleKey: string,
335
- descriptionKey: string
336
- ) => {
337
- const isSelected = mode === value;
338
-
339
- return (
340
- <button
341
- key={value}
342
- type="button"
343
- className={`cursor-pointer rounded-xl border p-3.5 text-left transition-all hover:border-primary/40 hover:bg-accent/20 ${
344
- isSelected ? 'border-primary bg-accent/30 ring-1 ring-primary/30' : ''
345
- }`}
346
- onClick={() => handleModeChange(value)}
347
- >
348
- <div className="flex items-start gap-3">
349
- <div className="mt-0.5 rounded-lg border p-2 text-primary">
350
- {icon}
351
- </div>
352
- <div className="space-y-1">
353
- <div className="font-medium">{t(titleKey)}</div>
354
- <p className="text-sm text-muted-foreground">{t(descriptionKey)}</p>
355
- </div>
356
- </div>
357
- </button>
358
- );
359
- };
360
-
361
- return (
362
- <div className="mt-3 space-y-3 pb-4">
363
- <SectionCard
364
- title={t('wizard.title')}
365
- description={t('wizard.description')}
366
- compact
367
- descriptionMode="tooltip"
368
- >
369
- <div className="grid gap-2.5 xl:grid-cols-3">
370
- {renderModeCard(
371
- 'upload',
372
- <Sparkles className="size-5" />,
373
- 'wizard.options.upload.title',
374
- 'wizard.options.upload.description'
375
- )}
376
- {renderModeCard(
377
- 'template',
378
- <LayoutTemplate className="size-5" />,
379
- 'wizard.options.template.title',
380
- 'wizard.options.template.description'
381
- )}
382
- {renderModeCard(
383
- 'blank',
384
- <FileText className="size-5" />,
385
- 'wizard.options.blank.title',
386
- 'wizard.options.blank.description'
387
- )}
388
- </div>
389
- </SectionCard>
390
-
391
- {mode === 'upload' ? (
392
- <SectionCard
393
- title={t('wizard.upload.title')}
394
- description={t('wizard.upload.description')}
395
- compact
396
- descriptionMode="tooltip"
397
- >
398
- <div className="space-y-3">
399
- <label className="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed px-4 py-6 text-center transition-colors hover:border-primary/40 hover:bg-accent/20">
400
- <WandSparkles className="size-6 text-primary" />
401
- <div className="space-y-1">
402
- <div className="font-medium">
403
- {t('wizard.upload.selectFile')}
404
- </div>
405
- <p className="text-sm text-muted-foreground">
406
- {t('wizard.upload.supportedFiles')}
407
- </p>
408
- </div>
409
- <input
410
- type="file"
411
- className="hidden"
412
- accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
413
- onChange={(event) => {
414
- const file = event.target.files?.[0] ?? null;
415
- setSelectedFile(file);
416
- setDraft(null);
417
- }}
418
- />
419
- </label>
420
-
421
- {selectedFile ? (
422
- <div className="rounded-lg border px-3 py-3 text-sm">
423
- <div className="font-medium">{selectedFile.name}</div>
424
- <div className="text-muted-foreground">
425
- {Math.max(selectedFile.size / 1024, 1).toFixed(0)} KB
426
- </div>
427
- </div>
428
- ) : null}
429
-
430
- <div className="rounded-lg border border-dashed px-3 py-3 text-sm text-muted-foreground">
431
- {provider
432
- ? t('wizard.upload.aiReady', { provider: providerLabel })
433
- : t('wizard.upload.aiUnavailable')}
434
- </div>
435
-
436
- {draft ? (
437
- <div className="space-y-3 rounded-xl border bg-muted/20 p-4">
438
- <div>
439
- <div className="font-medium">{t('wizard.review.title')}</div>
440
- <p className="text-sm text-muted-foreground">
441
- {t('wizard.review.description')}
442
- </p>
443
- </div>
444
-
445
- {draft.summary ? (
446
- <div className="rounded-lg border bg-background px-3 py-3 text-sm">
447
- {draft.summary}
448
- </div>
449
- ) : null}
450
-
451
- {toTextArray(draft.missingFields).length ? (
452
- <div className="space-y-1">
453
- <div className="text-sm font-medium">
454
- {t('wizard.review.missingFields')}
455
- </div>
456
- <ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
457
- {toTextArray(draft.missingFields).map((item) => (
458
- <li key={item}>{item}</li>
459
- ))}
460
- </ul>
461
- </div>
462
- ) : null}
463
-
464
- {toTextArray(draft.warnings).length ? (
465
- <div className="space-y-1">
466
- <div className="text-sm font-medium">
467
- {t('wizard.review.warnings')}
468
- </div>
469
- <ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
470
- {toTextArray(draft.warnings).map((item) => (
471
- <li key={item}>{item}</li>
472
- ))}
473
- </ul>
474
- </div>
475
- ) : null}
476
- </div>
477
- ) : null}
478
-
479
- <div className="flex flex-wrap justify-between gap-2">
480
- <Button
481
- type="button"
482
- variant="outline"
483
- className="cursor-pointer"
484
- onClick={() => handleModeChange('blank')}
485
- >
486
- {t('wizard.actions.skipToBlank')}
487
- </Button>
488
-
489
- <div className="flex flex-wrap gap-2">
490
- {provider && selectedFile && !draft ? (
491
- <Button
492
- type="button"
493
- className="cursor-pointer"
494
- disabled={isExtracting}
495
- onClick={() => void handleExtractDraft()}
496
- >
497
- {isExtracting ? (
498
- <LoaderCircle className="size-4 animate-spin" />
499
- ) : (
500
- <Sparkles className="size-4" />
501
- )}
502
- {t('wizard.actions.extractWithAi')}
503
- </Button>
504
- ) : null}
505
-
506
- {selectedFile ? (
507
- <Button
508
- type="button"
509
- className="cursor-pointer"
510
- disabled={isContinuing || isExtracting}
511
- onClick={() => void continueWithUpload()}
512
- >
513
- {isContinuing ? (
514
- <LoaderCircle className="size-4 animate-spin" />
515
- ) : null}
516
- {draft
517
- ? t('wizard.actions.reviewInForm')
518
- : t('wizard.actions.continueManual')}
519
- </Button>
520
- ) : null}
521
- </div>
522
- </div>
523
- </div>
524
- </SectionCard>
525
- ) : null}
526
-
527
- {mode === 'template' ? (
528
- <SectionCard
529
- title={t('wizard.template.title')}
530
- description={t('wizard.template.description')}
531
- compact
532
- descriptionMode="tooltip"
533
- >
534
- <div className="space-y-4">
535
- <Select
536
- value={selectedTemplateId}
537
- onValueChange={setSelectedTemplateId}
538
- >
539
- <SelectTrigger>
540
- <SelectValue placeholder={t('wizard.template.placeholder')} />
541
- </SelectTrigger>
542
- <SelectContent>
543
- <SelectItem value="none">
544
- {t('wizard.template.placeholder')}
545
- </SelectItem>
546
- {contractTemplates.map((template) => (
547
- <SelectItem key={template.id} value={String(template.id)}>
548
- {template.name}
549
- </SelectItem>
550
- ))}
551
- </SelectContent>
552
- </Select>
553
-
554
- {selectedTemplate ? (
555
- <div className="rounded-xl border p-4">
556
- <div className="font-medium">{selectedTemplate.name}</div>
557
- <p className="mt-1 text-sm text-muted-foreground">
558
- {selectedTemplate.description ||
559
- t('wizard.template.noDescription')}
560
- </p>
561
- <div className="mt-2 text-xs text-muted-foreground">
562
- {[selectedTemplate.code, selectedTemplate.contractType]
563
- .filter(Boolean)
564
- .join(' • ')}
565
- </div>
566
- </div>
567
- ) : null}
568
-
569
- <div className="flex justify-end gap-2">
570
- <Button
571
- type="button"
572
- variant="outline"
573
- className="cursor-pointer"
574
- onClick={onCancel}
575
- >
576
- {commonT('actions.cancel')}
577
- </Button>
578
- <Button
579
- type="button"
580
- className="cursor-pointer"
581
- disabled={selectedTemplateId === 'none'}
582
- onClick={() =>
583
- onComplete({ initialTemplateId: Number(selectedTemplateId) })
584
- }
585
- >
586
- {t('wizard.actions.useTemplate')}
587
- </Button>
588
- </div>
589
- </div>
590
- </SectionCard>
591
- ) : null}
592
-
593
- {mode === 'blank' ? (
594
- <SectionCard
595
- title={t('wizard.blank.title')}
596
- description={t('wizard.blank.description')}
597
- compact
598
- descriptionMode="tooltip"
599
- >
600
- <div className="flex flex-wrap items-center justify-between gap-3">
601
- <p className="text-sm text-muted-foreground">
602
- {t('wizard.blank.helper')}
603
- </p>
604
- <Button
605
- type="button"
606
- className="cursor-pointer"
607
- onClick={() =>
608
- onComplete({ initialValues: { originType: 'manual' } })
609
- }
610
- >
611
- {t('wizard.actions.startBlank')}
612
- </Button>
613
- </div>
614
- </SectionCard>
615
- ) : null}
616
-
617
- {!mode ? (
618
- <div className="flex justify-end gap-2">
619
- <Button
620
- type="button"
621
- variant="outline"
622
- className="cursor-pointer"
623
- onClick={onCancel}
624
- >
625
- {commonT('actions.cancel')}
626
- </Button>
627
- </div>
628
- ) : null}
629
- </div>
630
- );
631
- }