@voyantjs/legal-ui 0.77.13 → 0.78.0

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.
@@ -0,0 +1,569 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { formatMessage } from "@voyantjs/i18n";
5
+ import { fetchWithValidation, legalContractTemplateVersionRecordSchema, singleEnvelope, useLegalContractMutation, useLegalContractNumberSeries, useLegalContractTemplate, useLegalContractTemplateAuthoring, useLegalContractTemplates, useLegalContractTemplateVersions, useVoyantLegalContext, } from "@voyantjs/legal-react";
6
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from "@voyantjs/ui/components";
7
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
8
+ import { DatePicker } from "@voyantjs/ui/components/date-picker";
9
+ import { DateTimePicker } from "@voyantjs/ui/components/date-time-picker";
10
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
11
+ import { languages } from "@voyantjs/utils/languages";
12
+ import { Loader2, Plus, Trash2 } from "lucide-react";
13
+ import { useEffect, useMemo, useRef, useState } from "react";
14
+ import { useFieldArray, useForm } from "react-hook-form";
15
+ import { z } from "zod/v4";
16
+ import { useLegalUiMessagesOrDefault } from "../i18n/index.js";
17
+ import { legalContractScopes } from "../i18n/messages.js";
18
+ const contractFormSchema = z.object({
19
+ scope: z.enum(legalContractScopes),
20
+ title: z.string().min(1, "titleRequired"),
21
+ contractNumber: z.string().optional(),
22
+ language: z.string().min(2).max(10).optional(),
23
+ templateVersionId: z.string().optional(),
24
+ seriesId: z.string().optional(),
25
+ personId: z.string().optional(),
26
+ organizationId: z.string().optional(),
27
+ supplierId: z.string().optional(),
28
+ channelId: z.string().optional(),
29
+ expiresAt: z.string().optional(),
30
+ templateVariables: z.array(z.object({
31
+ key: z.string(),
32
+ label: z.string(),
33
+ type: z.string(),
34
+ description: z.string().optional(),
35
+ example: z.string().optional(),
36
+ required: z.boolean().default(false),
37
+ value: z.string().optional(),
38
+ booleanValue: z.boolean().default(false),
39
+ includeBooleanValue: z.boolean().default(false),
40
+ })),
41
+ additionalVariables: z.array(z.object({
42
+ key: z.string().optional(),
43
+ value: z.string().optional(),
44
+ })),
45
+ metadataEntries: z.array(z.object({
46
+ key: z.string().optional(),
47
+ value: z.string().optional(),
48
+ })),
49
+ });
50
+ const PREFERRED_LANGUAGE_ORDER = ["en", "ro", "fr", "de", "es", "it"];
51
+ const languageOptions = Object.entries(languages)
52
+ .sort(([codeA, nameA], [codeB, nameB]) => {
53
+ const preferredA = PREFERRED_LANGUAGE_ORDER.indexOf(codeA);
54
+ const preferredB = PREFERRED_LANGUAGE_ORDER.indexOf(codeB);
55
+ if (preferredA !== -1 || preferredB !== -1) {
56
+ if (preferredA === -1)
57
+ return 1;
58
+ if (preferredB === -1)
59
+ return -1;
60
+ return preferredA - preferredB;
61
+ }
62
+ return nameA.localeCompare(nameB);
63
+ })
64
+ .map(([code, name]) => ({
65
+ value: code,
66
+ label: `${name} (${code})`,
67
+ description: code,
68
+ }));
69
+ function mergeUniqueOptions(...groups) {
70
+ const map = new Map();
71
+ for (const group of groups) {
72
+ for (const option of group ?? []) {
73
+ map.set(option.value, option);
74
+ }
75
+ }
76
+ return Array.from(map.values());
77
+ }
78
+ function objectToEntries(record) {
79
+ return Object.entries(record ?? {}).map(([key, value]) => ({
80
+ key,
81
+ value: typeof value === "string"
82
+ ? value
83
+ : typeof value === "number" || typeof value === "boolean"
84
+ ? String(value)
85
+ : JSON.stringify(value),
86
+ }));
87
+ }
88
+ function parseLooseValue(value) {
89
+ const trimmed = value.trim();
90
+ if (!trimmed)
91
+ return undefined;
92
+ if (trimmed === "true")
93
+ return true;
94
+ if (trimmed === "false")
95
+ return false;
96
+ if (/^-?\d+(\.\d+)?$/.test(trimmed))
97
+ return Number(trimmed);
98
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
99
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
100
+ try {
101
+ return JSON.parse(trimmed);
102
+ }
103
+ catch {
104
+ return trimmed;
105
+ }
106
+ }
107
+ return trimmed;
108
+ }
109
+ export function buildRecordFromPairs(entries) {
110
+ const record = {};
111
+ for (const entry of entries) {
112
+ const key = entry.key?.trim();
113
+ if (!key)
114
+ continue;
115
+ const parsed = parseLooseValue(entry.value ?? "");
116
+ if (parsed === undefined)
117
+ continue;
118
+ record[key] = parsed;
119
+ }
120
+ return Object.keys(record).length ? record : undefined;
121
+ }
122
+ export function buildVariablesPayload(rows, additional) {
123
+ const variables = {};
124
+ for (const row of rows) {
125
+ if (row.type === "boolean") {
126
+ if (row.booleanValue || row.required || row.includeBooleanValue) {
127
+ variables[row.key] = row.booleanValue;
128
+ }
129
+ continue;
130
+ }
131
+ const parsed = parseLooseValue(row.value ?? "");
132
+ if (parsed !== undefined)
133
+ variables[row.key] = parsed;
134
+ }
135
+ for (const entry of additional) {
136
+ const key = entry.key?.trim();
137
+ if (!key)
138
+ continue;
139
+ const parsed = parseLooseValue(entry.value ?? "");
140
+ if (parsed !== undefined)
141
+ variables[key] = parsed;
142
+ }
143
+ return Object.keys(variables).length ? variables : undefined;
144
+ }
145
+ export function clearedOptionalValue(value, isEditing) {
146
+ const trimmed = value?.trim();
147
+ if (trimmed)
148
+ return trimmed;
149
+ return isEditing ? null : undefined;
150
+ }
151
+ function parseBooleanFormValue(value) {
152
+ const parsed = parseLooseValue(value ?? "");
153
+ return typeof parsed === "boolean" ? parsed : undefined;
154
+ }
155
+ function inferTemplateVariableKeys(body, requiredKeys) {
156
+ if (!body)
157
+ return new Set(requiredKeys);
158
+ const detected = new Set(requiredKeys);
159
+ const variablePattern = /\{\{\s*([a-zA-Z0-9_.[\]]+)\s*\}\}/g;
160
+ for (const match of body.matchAll(variablePattern)) {
161
+ if (match[1])
162
+ detected.add(match[1]);
163
+ }
164
+ return detected;
165
+ }
166
+ function SearchableSelect({ value, onChange, options, placeholder, searchPlaceholder, emptyLabel, loadingLabel, loading, disabled, onSearchChange, }) {
167
+ const optionMap = useMemo(() => new Map(options.map((option) => [option.value, option])), [options]);
168
+ const selected = value ? optionMap.get(value) : undefined;
169
+ const selectedLabel = selected?.label ?? "";
170
+ const [inputValue, setInputValue] = useState(selectedLabel);
171
+ useEffect(() => {
172
+ setInputValue(selectedLabel);
173
+ }, [selectedLabel]);
174
+ return (_jsxs(Combobox, { items: options.map((option) => option.value), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => optionMap.get(id)?.label ?? "", onInputValueChange: (next) => {
175
+ setInputValue(next);
176
+ onSearchChange?.(next);
177
+ if (!next)
178
+ onChange(null);
179
+ }, onValueChange: (next) => {
180
+ const resolved = next ?? null;
181
+ onChange(resolved);
182
+ setInputValue(resolved ? (optionMap.get(resolved)?.label ?? "") : "");
183
+ }, children: [_jsx(ComboboxInput, { placeholder: searchPlaceholder ?? placeholder, showClear: !!value, disabled: disabled }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: loading ? loadingLabel : emptyLabel }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
184
+ const option = optionMap.get(id);
185
+ if (!option)
186
+ return null;
187
+ return (_jsx(ComboboxItem, { value: option.value, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: option.label }), option.description ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: option.description })) : null] }) }, option.value));
188
+ } }) })] })] }));
189
+ }
190
+ function VariableValueField({ row, index, setValue, watch, messages, }) {
191
+ const valuePath = `templateVariables.${index}.value`;
192
+ const booleanPath = `templateVariables.${index}.booleanValue`;
193
+ const includeBooleanPath = `templateVariables.${index}.includeBooleanValue`;
194
+ if (row.type === "boolean") {
195
+ return (_jsx("div", { className: "flex min-h-10 items-center rounded-md border border-input px-3 text-sm", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { "aria-label": row.label, checked: watch(booleanPath), onCheckedChange: (checked) => {
196
+ setValue(booleanPath, checked, {
197
+ shouldDirty: true,
198
+ shouldValidate: true,
199
+ });
200
+ setValue(includeBooleanPath, true, {
201
+ shouldDirty: true,
202
+ shouldValidate: true,
203
+ });
204
+ } }), _jsx("span", { children: watch(booleanPath) ? messages.booleanYes : messages.booleanNo })] }) }));
205
+ }
206
+ if (row.type === "datetime") {
207
+ return (_jsx(DateTimePicker, { value: watch(valuePath) || null, onChange: (next) => setValue(valuePath, next ?? "", {
208
+ shouldDirty: true,
209
+ shouldValidate: true,
210
+ }), placeholder: row.example || messages.datetimeFallbackPlaceholder, className: "w-full" }));
211
+ }
212
+ if (row.type === "date") {
213
+ return (_jsx(DatePicker, { value: watch(valuePath) || null, onChange: (next) => setValue(valuePath, next ?? "", {
214
+ shouldDirty: true,
215
+ shouldValidate: true,
216
+ }), placeholder: row.example || messages.dateFallbackPlaceholder, className: "w-full" }));
217
+ }
218
+ const inputType = row.type === "email"
219
+ ? "email"
220
+ : row.type === "url"
221
+ ? "url"
222
+ : row.type === "phone"
223
+ ? "tel"
224
+ : row.type === "number"
225
+ ? "number"
226
+ : "text";
227
+ return (_jsx(Input, { type: inputType, step: row.type === "number" ? "1" : undefined, value: watch(valuePath) || "", onChange: (event) => setValue(valuePath, event.target.value, {
228
+ shouldDirty: true,
229
+ shouldValidate: true,
230
+ }), placeholder: row.example || messages.valueFallbackPlaceholder }));
231
+ }
232
+ function LinkedRecordField({ label, value, scope, onChange, renderPicker, }) {
233
+ if (!renderPicker)
234
+ return null;
235
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: label }), renderPicker({
236
+ value: value ?? undefined,
237
+ onChange,
238
+ scope,
239
+ })] }));
240
+ }
241
+ export function ContractDialog({ open, onOpenChange, contract, onSuccess, renderPersonPicker, renderOrganizationPicker, renderSupplierPicker, renderChannelPicker, }) {
242
+ const isEditing = !!contract;
243
+ const messages = useLegalUiMessagesOrDefault();
244
+ const t = messages.contractDialog;
245
+ const { baseUrl, fetcher } = useVoyantLegalContext();
246
+ const { create, update } = useLegalContractMutation();
247
+ const { variableCatalog } = useLegalContractTemplateAuthoring();
248
+ const hasLinkedRecordPicker = Boolean(renderPersonPicker || renderOrganizationPicker || renderSupplierPicker || renderChannelPicker);
249
+ const validationByCode = {
250
+ titleRequired: t.validation.titleRequired,
251
+ };
252
+ const resolveValidation = (code) => (code && validationByCode[code]) || code || "";
253
+ const [templateSearch, setTemplateSearch] = useState("");
254
+ const [templateId, setTemplateId] = useState(null);
255
+ const syncedTemplateVariablesSignatureRef = useRef("");
256
+ const form = useForm({
257
+ resolver: zodResolver(contractFormSchema),
258
+ defaultValues: {
259
+ scope: "customer",
260
+ title: "",
261
+ contractNumber: "",
262
+ language: "en",
263
+ templateVersionId: "",
264
+ seriesId: "",
265
+ personId: "",
266
+ organizationId: "",
267
+ supplierId: "",
268
+ channelId: "",
269
+ expiresAt: "",
270
+ templateVariables: [],
271
+ additionalVariables: [],
272
+ metadataEntries: [],
273
+ },
274
+ });
275
+ const templateVariablesFieldArray = useFieldArray({
276
+ control: form.control,
277
+ name: "templateVariables",
278
+ });
279
+ const additionalVariablesFieldArray = useFieldArray({
280
+ control: form.control,
281
+ name: "additionalVariables",
282
+ });
283
+ const metadataEntriesFieldArray = useFieldArray({
284
+ control: form.control,
285
+ name: "metadataEntries",
286
+ });
287
+ const selectedScope = form.watch("scope");
288
+ const selectedLanguage = form.watch("language") || "en";
289
+ const selectedTemplateVersionId = form.watch("templateVersionId") || null;
290
+ const selectedSeriesId = form.watch("seriesId") || null;
291
+ const selectedPersonId = form.watch("personId") || null;
292
+ const selectedOrganizationId = form.watch("organizationId") || null;
293
+ const selectedSupplierId = form.watch("supplierId") || null;
294
+ const selectedChannelId = form.watch("channelId") || null;
295
+ const templateListQuery = useLegalContractTemplates({
296
+ search: templateSearch || undefined,
297
+ scope: selectedScope,
298
+ limit: 25,
299
+ offset: 0,
300
+ enabled: open,
301
+ });
302
+ const selectedTemplateQuery = useLegalContractTemplate(templateId ?? "", {
303
+ enabled: open && !!templateId,
304
+ });
305
+ const templateVersionsQuery = useLegalContractTemplateVersions({
306
+ templateId: templateId ?? "",
307
+ enabled: open && !!templateId,
308
+ });
309
+ const selectedTemplateVersionQuery = useQuery({
310
+ queryKey: ["legal", "template-version", selectedTemplateVersionId],
311
+ enabled: open && !!selectedTemplateVersionId,
312
+ queryFn: async () => {
313
+ const { data } = await fetchWithValidation(`/v1/admin/legal/contracts/template-versions/${selectedTemplateVersionId}`, singleEnvelope(legalContractTemplateVersionRecordSchema), { baseUrl, fetcher });
314
+ return data;
315
+ },
316
+ });
317
+ const numberSeriesQuery = useLegalContractNumberSeries({ enabled: open });
318
+ useEffect(() => {
319
+ if (!open) {
320
+ setTemplateSearch("");
321
+ setTemplateId(null);
322
+ syncedTemplateVariablesSignatureRef.current = "";
323
+ return;
324
+ }
325
+ if (contract) {
326
+ form.reset({
327
+ scope: contract.scope,
328
+ title: contract.title,
329
+ contractNumber: contract.contractNumber ?? "",
330
+ language: contract.language ?? "en",
331
+ templateVersionId: contract.templateVersionId ?? "",
332
+ seriesId: contract.seriesId ?? "",
333
+ personId: contract.personId ?? "",
334
+ organizationId: contract.organizationId ?? "",
335
+ supplierId: contract.supplierId ?? "",
336
+ channelId: contract.channelId ?? "",
337
+ expiresAt: contract.expiresAt ?? "",
338
+ templateVariables: [],
339
+ additionalVariables: objectToEntries(contract.variables),
340
+ metadataEntries: objectToEntries(contract.metadata),
341
+ });
342
+ }
343
+ else {
344
+ form.reset({
345
+ scope: "customer",
346
+ title: "",
347
+ contractNumber: "",
348
+ language: "en",
349
+ templateVersionId: "",
350
+ seriesId: "",
351
+ personId: "",
352
+ organizationId: "",
353
+ supplierId: "",
354
+ channelId: "",
355
+ expiresAt: "",
356
+ templateVariables: [],
357
+ additionalVariables: [],
358
+ metadataEntries: [],
359
+ });
360
+ }
361
+ }, [open, contract, form]);
362
+ useEffect(() => {
363
+ if (!open)
364
+ return;
365
+ if (selectedTemplateVersionQuery.data?.templateId) {
366
+ setTemplateId((current) => current ?? selectedTemplateVersionQuery.data?.templateId ?? null);
367
+ }
368
+ }, [open, selectedTemplateVersionQuery.data]);
369
+ const templateOptions = useMemo(() => {
370
+ const listOptions = templateListQuery.data?.data.map((template) => ({
371
+ value: template.id,
372
+ label: template.name,
373
+ description: `${template.slug} - ${template.language} - ${template.scope}`,
374
+ })) ?? [];
375
+ const selectedOption = selectedTemplateQuery.data
376
+ ? [
377
+ {
378
+ value: selectedTemplateQuery.data.id,
379
+ label: selectedTemplateQuery.data.name,
380
+ description: `${selectedTemplateQuery.data.slug} - ${selectedTemplateQuery.data.language} - ${selectedTemplateQuery.data.scope}`,
381
+ },
382
+ ]
383
+ : [];
384
+ return mergeUniqueOptions(listOptions, selectedOption);
385
+ }, [templateListQuery.data?.data, selectedTemplateQuery.data]);
386
+ const templateVersionOptions = useMemo(() => {
387
+ const listedOptions = templateVersionsQuery.data?.map((version) => ({
388
+ value: version.id,
389
+ label: formatMessage(t.templateVersionLabelFormat, { version: version.version }),
390
+ description: version.changelog || t.templateVersionMostRecentDraft,
391
+ })) ?? [];
392
+ const selectedOption = selectedTemplateVersionQuery.data
393
+ ? [
394
+ {
395
+ value: selectedTemplateVersionQuery.data.id,
396
+ label: formatMessage(t.templateVersionLabelFormat, {
397
+ version: selectedTemplateVersionQuery.data.version,
398
+ }),
399
+ description: selectedTemplateVersionQuery.data.changelog || t.templateVersionSelectedFallback,
400
+ },
401
+ ]
402
+ : [];
403
+ return mergeUniqueOptions(listedOptions, selectedOption);
404
+ }, [
405
+ selectedTemplateVersionQuery.data,
406
+ t.templateVersionLabelFormat,
407
+ t.templateVersionMostRecentDraft,
408
+ t.templateVersionSelectedFallback,
409
+ templateVersionsQuery.data,
410
+ ]);
411
+ const seriesOptions = useMemo(() => {
412
+ return (numberSeriesQuery.data?.data
413
+ .filter((series) => series.scope === selectedScope)
414
+ .map((series) => ({
415
+ value: series.id,
416
+ label: `${series.name} - ${series.prefix}${series.separator || ""}${String((series.currentSequence ?? 0) + 1).padStart(series.padLength, "0")}`,
417
+ description: `${series.scope} - ${series.active ? t.seriesActive : t.seriesInactive}`,
418
+ })) ?? []);
419
+ }, [numberSeriesQuery.data, selectedScope, t.seriesActive, t.seriesInactive]);
420
+ const flattenedVariableCatalog = useMemo(() => {
421
+ return variableCatalog.flatMap((group) => group.variables.map((variable) => ({
422
+ ...variable,
423
+ groupLabel: group.label,
424
+ })));
425
+ }, [variableCatalog]);
426
+ const selectedTemplateVersion = selectedTemplateVersionQuery.data;
427
+ const inferredTemplateVariableRows = useMemo(() => {
428
+ if (!selectedTemplateVersion)
429
+ return [];
430
+ const requiredKeys = Array.isArray(selectedTemplateVersion.variableSchema?.required)
431
+ ? selectedTemplateVersion.variableSchema.required
432
+ : [];
433
+ const detectedKeys = inferTemplateVariableKeys(selectedTemplateVersion.body, requiredKeys);
434
+ return flattenedVariableCatalog
435
+ .filter((variable) => detectedKeys.has(variable.key))
436
+ .map((variable) => ({
437
+ key: variable.key,
438
+ label: variable.label,
439
+ type: variable.type,
440
+ description: variable.description || variable.groupLabel,
441
+ example: String(variable.example),
442
+ required: requiredKeys.includes(variable.key),
443
+ value: "",
444
+ booleanValue: false,
445
+ includeBooleanValue: false,
446
+ }));
447
+ }, [flattenedVariableCatalog, selectedTemplateVersion]);
448
+ const templateVariablesSignature = useMemo(() => `${selectedTemplateVersionId ?? "none"}:${inferredTemplateVariableRows.map((row) => row.key).join("|")}`, [inferredTemplateVariableRows, selectedTemplateVersionId]);
449
+ useEffect(() => {
450
+ if (!open)
451
+ return;
452
+ if (syncedTemplateVariablesSignatureRef.current === templateVariablesSignature)
453
+ return;
454
+ const existingTemplateRows = form.getValues("templateVariables");
455
+ const existingAdditionalRows = form.getValues("additionalVariables");
456
+ const currentValues = new Map();
457
+ for (const row of existingTemplateRows) {
458
+ currentValues.set(row.key, {
459
+ value: row.value,
460
+ booleanValue: row.booleanValue,
461
+ includeBooleanValue: row.includeBooleanValue,
462
+ });
463
+ }
464
+ for (const row of existingAdditionalRows) {
465
+ const key = row.key?.trim();
466
+ if (!key)
467
+ continue;
468
+ const booleanValue = parseBooleanFormValue(row.value);
469
+ currentValues.set(key, {
470
+ value: row.value,
471
+ booleanValue,
472
+ includeBooleanValue: booleanValue !== undefined,
473
+ });
474
+ }
475
+ const knownKeys = new Set(inferredTemplateVariableRows.map((row) => row.key));
476
+ const nextTemplateRows = inferredTemplateVariableRows.map((row) => {
477
+ const existing = currentValues.get(row.key);
478
+ return {
479
+ ...row,
480
+ value: typeof existing?.value === "string" ? existing.value : "",
481
+ booleanValue: typeof existing?.booleanValue === "boolean" ? existing.booleanValue : false,
482
+ includeBooleanValue: row.required || existing?.includeBooleanValue === true,
483
+ };
484
+ });
485
+ const nextAdditionalRows = [
486
+ ...existingAdditionalRows.filter((row) => {
487
+ const key = row.key?.trim();
488
+ return key && !knownKeys.has(key);
489
+ }),
490
+ ...existingTemplateRows
491
+ .filter((row) => !knownKeys.has(row.key))
492
+ .map((row) => ({
493
+ key: row.key,
494
+ value: row.type === "boolean" ? String(row.booleanValue) : row.value,
495
+ })),
496
+ ];
497
+ templateVariablesFieldArray.replace(nextTemplateRows);
498
+ additionalVariablesFieldArray.replace(nextAdditionalRows.filter((row, index, rows) => rows.findIndex((candidate) => candidate.key === row.key && candidate.value === row.value) === index));
499
+ syncedTemplateVariablesSignatureRef.current = templateVariablesSignature;
500
+ }, [
501
+ additionalVariablesFieldArray,
502
+ form,
503
+ inferredTemplateVariableRows,
504
+ open,
505
+ templateVariablesSignature,
506
+ templateVariablesFieldArray,
507
+ ]);
508
+ const setLinkedRecordField = (field, value) => form.setValue(field, value ?? "", {
509
+ shouldDirty: true,
510
+ shouldValidate: true,
511
+ });
512
+ const onSubmit = async (values) => {
513
+ const variables = buildVariablesPayload(values.templateVariables, values.additionalVariables);
514
+ const metadata = buildRecordFromPairs(values.metadataEntries);
515
+ const payload = {
516
+ scope: values.scope,
517
+ title: values.title,
518
+ contractNumber: values.contractNumber?.trim() || null,
519
+ language: values.language || "en",
520
+ templateVersionId: clearedOptionalValue(values.templateVersionId, isEditing),
521
+ seriesId: clearedOptionalValue(values.seriesId, isEditing),
522
+ personId: clearedOptionalValue(values.personId, isEditing),
523
+ organizationId: clearedOptionalValue(values.organizationId, isEditing),
524
+ supplierId: clearedOptionalValue(values.supplierId, isEditing),
525
+ channelId: clearedOptionalValue(values.channelId, isEditing),
526
+ expiresAt: clearedOptionalValue(values.expiresAt, isEditing),
527
+ variables: variables ?? (isEditing ? null : undefined),
528
+ metadata: metadata ?? (isEditing ? null : undefined),
529
+ };
530
+ if (isEditing && contract) {
531
+ await update.mutateAsync({ id: contract.id, input: payload });
532
+ }
533
+ else {
534
+ await create.mutateAsync(payload);
535
+ }
536
+ onSuccess();
537
+ };
538
+ const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
539
+ const submitError = create.error ?? update.error ?? null;
540
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "xl", className: "h-[calc(100vh-2rem)] max-h-[calc(100vh-2rem)]", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? t.titleEdit : t.titleNew }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-6", children: [_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold", children: t.setupSectionTitle }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.setupSectionDescription })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.scopeLabel }), _jsxs(Select, { items: legalContractScopes.map((value) => ({
541
+ value,
542
+ label: messages.common.contractScopeLabels[value],
543
+ })), value: selectedScope, onValueChange: (value) => form.setValue("scope", value, {
544
+ shouldDirty: true,
545
+ shouldValidate: true,
546
+ }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: legalContractScopes.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.contractScopeLabels[value] }, value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.languageLabel }), _jsx(SearchableSelect, { value: selectedLanguage, onChange: (value) => form.setValue("language", value ?? "en", {
547
+ shouldDirty: true,
548
+ shouldValidate: true,
549
+ }), options: languageOptions, placeholder: t.languagePlaceholder, searchPlaceholder: t.languageSearchPlaceholder, emptyLabel: t.languageEmpty, loadingLabel: t.loading })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.titleLabel }), _jsx(Input, { ...form.register("title"), placeholder: t.titlePlaceholder }), form.formState.errors.title ? (_jsx("p", { className: "text-xs text-destructive", children: resolveValidation(form.formState.errors.title.message) })) : null] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.contractNumberLabel }), _jsx(Input, { ...form.register("contractNumber"), placeholder: t.contractNumberPlaceholder, maxLength: 100 })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.expiresAtLabel }), _jsx(DateTimePicker, { value: form.watch("expiresAt") || null, onChange: (next) => form.setValue("expiresAt", next ?? "", {
550
+ shouldValidate: true,
551
+ shouldDirty: true,
552
+ }), placeholder: t.expiresAtPlaceholder, className: "w-full" })] })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.templateLabel }), _jsx(SearchableSelect, { value: templateId, onChange: (value) => {
553
+ setTemplateId(value);
554
+ form.setValue("templateVersionId", "", {
555
+ shouldDirty: true,
556
+ shouldValidate: true,
557
+ });
558
+ syncedTemplateVariablesSignatureRef.current = "";
559
+ }, options: templateOptions, placeholder: t.templatePlaceholder, searchPlaceholder: t.templateSearchPlaceholder, emptyLabel: t.templateEmpty, loading: templateListQuery.isPending || selectedTemplateQuery.isPending, onSearchChange: setTemplateSearch, loadingLabel: t.loading })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.templateVersionLabel }), _jsx(SearchableSelect, { value: selectedTemplateVersionId, onChange: (value) => {
560
+ form.setValue("templateVersionId", value ?? "", {
561
+ shouldDirty: true,
562
+ shouldValidate: true,
563
+ });
564
+ syncedTemplateVariablesSignatureRef.current = "";
565
+ }, options: templateVersionOptions, placeholder: t.templateVersionPlaceholder, searchPlaceholder: t.templateVersionSearchPlaceholder, emptyLabel: templateId ? t.templateVersionEmpty : t.templateVersionPickTemplateFirst, loading: templateVersionsQuery.isPending || selectedTemplateVersionQuery.isPending, disabled: !templateId, loadingLabel: t.loading })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.numberSeriesLabel }), _jsx(SearchableSelect, { value: selectedSeriesId, onChange: (value) => form.setValue("seriesId", value ?? "", {
566
+ shouldDirty: true,
567
+ shouldValidate: true,
568
+ }), options: seriesOptions, placeholder: t.numberSeriesPlaceholder, searchPlaceholder: t.numberSeriesSearchPlaceholder, emptyLabel: t.numberSeriesEmpty, loading: numberSeriesQuery.isPending, loadingLabel: t.loading })] })] }), hasLinkedRecordPicker ? (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold", children: t.linkedSectionTitle }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.linkedSectionDescription })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(LinkedRecordField, { label: t.personLabel, value: selectedPersonId, scope: selectedScope, onChange: (value) => setLinkedRecordField("personId", value), renderPicker: renderPersonPicker }), _jsx(LinkedRecordField, { label: t.organizationLabel, value: selectedOrganizationId, scope: selectedScope, onChange: (value) => setLinkedRecordField("organizationId", value), renderPicker: renderOrganizationPicker }), _jsx(LinkedRecordField, { label: t.supplierLabel, value: selectedSupplierId, scope: selectedScope, onChange: (value) => setLinkedRecordField("supplierId", value), renderPicker: renderSupplierPicker }), _jsx(LinkedRecordField, { label: t.channelLabel, value: selectedChannelId, scope: selectedScope, onChange: (value) => setLinkedRecordField("channelId", value), renderPicker: renderChannelPicker })] })] })) : null, _jsxs("div", { className: "grid gap-4", children: [_jsx("h3", { className: "text-sm font-semibold", children: t.templateVariablesSectionTitle }), !selectedTemplateVersionId ? (_jsx("div", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: t.templateVariablesNoVersion })) : templateVariablesFieldArray.fields.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: t.templateVariablesNoneDetected })) : (_jsx("div", { className: "grid gap-4", children: templateVariablesFieldArray.fields.map((field, index) => (_jsxs("div", { className: "grid gap-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("div", { className: "text-sm font-medium", children: [field.label, field.required ? (_jsx("span", { className: "ml-1 text-destructive", children: "*" })) : null] }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: field.key })] }), field.description ? (_jsx("div", { className: "max-w-sm text-right text-xs text-muted-foreground", children: field.description })) : null] }), _jsx(VariableValueField, { row: field, index: index, setValue: form.setValue, watch: form.watch, messages: t })] }, field.id))) })), _jsxs("div", { className: "grid gap-3 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h4", { className: "text-sm font-medium", children: t.additionalVariablesTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: t.additionalVariablesDescription })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => additionalVariablesFieldArray.append({ key: "", value: "" }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t.addVariable] })] }), additionalVariablesFieldArray.fields.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: t.additionalVariablesEmpty })) : (_jsx("div", { className: "grid gap-3", children: additionalVariablesFieldArray.fields.map((field, index) => (_jsxs("div", { className: "grid gap-3 md:grid-cols-[1fr_1fr_auto]", children: [_jsx(Input, { ...form.register(`additionalVariables.${index}.key`), placeholder: t.variableKeyPlaceholder }), _jsx(Input, { ...form.register(`additionalVariables.${index}.value`), placeholder: t.variableValuePlaceholder }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => additionalVariablesFieldArray.remove(index), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, field.id))) }))] })] }), _jsxs("div", { className: "grid gap-3 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold", children: t.metadataSectionTitle }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.metadataSectionDescription })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => metadataEntriesFieldArray.append({ key: "", value: "" }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t.addMetadata] })] }), metadataEntriesFieldArray.fields.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: t.metadataEmpty })) : (_jsx("div", { className: "grid gap-3", children: metadataEntriesFieldArray.fields.map((field, index) => (_jsxs("div", { className: "grid gap-3 md:grid-cols-[1fr_1fr_auto]", children: [_jsx(Input, { ...form.register(`metadataEntries.${index}.key`), placeholder: t.metadataKeyPlaceholder }), _jsx(Input, { ...form.register(`metadataEntries.${index}.value`), placeholder: t.metadataValuePlaceholder }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => metadataEntriesFieldArray.remove(index), children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, field.id))) }))] }), submitError ? _jsx("p", { className: "text-xs text-destructive", children: submitError.message }) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? messages.common.saveChanges : t.createAction] })] })] })] }) }));
569
+ }
package/dist/i18n/en.d.ts CHANGED
@@ -105,6 +105,72 @@ export declare const legalUiEn: {
105
105
  send: string;
106
106
  };
107
107
  };
108
+ contractDialog: {
109
+ titleNew: string;
110
+ titleEdit: string;
111
+ createAction: string;
112
+ setupSectionTitle: string;
113
+ setupSectionDescription: string;
114
+ linkedSectionTitle: string;
115
+ linkedSectionDescription: string;
116
+ templateVariablesSectionTitle: string;
117
+ templateVariablesNoVersion: string;
118
+ templateVariablesNoneDetected: string;
119
+ additionalVariablesTitle: string;
120
+ additionalVariablesDescription: string;
121
+ addVariable: string;
122
+ additionalVariablesEmpty: string;
123
+ variableKeyPlaceholder: string;
124
+ variableValuePlaceholder: string;
125
+ metadataSectionTitle: string;
126
+ metadataSectionDescription: string;
127
+ addMetadata: string;
128
+ metadataEmpty: string;
129
+ metadataKeyPlaceholder: string;
130
+ metadataValuePlaceholder: string;
131
+ titleLabel: string;
132
+ titlePlaceholder: string;
133
+ contractNumberLabel: string;
134
+ contractNumberPlaceholder: string;
135
+ scopeLabel: string;
136
+ languageLabel: string;
137
+ languagePlaceholder: string;
138
+ languageSearchPlaceholder: string;
139
+ languageEmpty: string;
140
+ templateLabel: string;
141
+ templatePlaceholder: string;
142
+ templateSearchPlaceholder: string;
143
+ templateEmpty: string;
144
+ templateVersionLabel: string;
145
+ templateVersionPlaceholder: string;
146
+ templateVersionSearchPlaceholder: string;
147
+ templateVersionEmpty: string;
148
+ templateVersionPickTemplateFirst: string;
149
+ templateVersionMostRecentDraft: string;
150
+ templateVersionSelectedFallback: string;
151
+ templateVersionLabelFormat: string;
152
+ numberSeriesLabel: string;
153
+ numberSeriesPlaceholder: string;
154
+ numberSeriesSearchPlaceholder: string;
155
+ numberSeriesEmpty: string;
156
+ seriesActive: string;
157
+ seriesInactive: string;
158
+ expiresAtLabel: string;
159
+ expiresAtPlaceholder: string;
160
+ personLabel: string;
161
+ organizationLabel: string;
162
+ supplierLabel: string;
163
+ channelLabel: string;
164
+ loading: string;
165
+ booleanYes: string;
166
+ booleanNo: string;
167
+ dateFallbackPlaceholder: string;
168
+ datetimeFallbackPlaceholder: string;
169
+ valueFallbackPlaceholder: string;
170
+ validation: {
171
+ titleRequired: string;
172
+ };
173
+ };
108
174
  contractsPage: {
109
175
  title: string;
110
176
  description: string;
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4dK,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgiBK,CAAA"}