@voyantjs/legal-ui 0.33.0 → 0.34.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.
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-dialog.d.ts","sourceRoot":"","sources":["../../src/components/attachment-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,6BAA6B,EAEnC,MAAM,uBAAuB,CAAA;AAkC9B,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,6BAA6B,CAAA;IAC1C,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,qBAAqB,2CAkIvB"}
1
+ {"version":3,"file":"attachment-dialog.d.ts","sourceRoot":"","sources":["../../src/components/attachment-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,6BAA6B,EAEnC,MAAM,uBAAuB,CAAA;AA6C9B,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,6BAA6B,CAAA;IAC1C,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,qBAAqB,2CA8OvB"}
@@ -1,32 +1,44 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useLegalContractAttachmentMutation, } from "@voyantjs/legal-react";
3
- import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, } from "@voyantjs/ui/components";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
4
  import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
5
- import { Loader2 } from "lucide-react";
6
- import { useEffect } from "react";
5
+ import { FileText, Loader2, Upload } from "lucide-react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
  import { useForm } from "react-hook-form";
8
8
  import { z } from "zod/v4";
9
9
  import { useLegalUiMessagesOrDefault } from "../i18n/index.js";
10
+ const attachmentKindValues = ["document", "appendix", "scan"];
10
11
  function createAttachmentFormSchema(messages) {
11
12
  return z.object({
12
13
  name: z.string().min(1, messages.attachmentDialog.validation.nameRequired),
13
14
  kind: z.string().min(1).optional(),
14
15
  mimeType: z.string().optional(),
15
- fileSize: z.coerce.number().int().optional(),
16
+ fileSize: z.preprocess((value) => (value === "" || value == null ? undefined : value), z.coerce.number().int().optional()),
16
17
  storageKey: z.string().optional(),
17
18
  checksum: z.string().optional(),
18
19
  });
19
20
  }
20
21
  export function AttachmentDialog({ open, onOpenChange, contractId, attachment, onSuccess, }) {
21
22
  const isEditing = !!attachment;
22
- const { create, update } = useLegalContractAttachmentMutation();
23
+ const { update, upload, replaceFile } = useLegalContractAttachmentMutation();
23
24
  const messages = useLegalUiMessagesOrDefault();
24
25
  const attachmentFormSchema = createAttachmentFormSchema(messages);
26
+ const fileInputRef = useRef(null);
27
+ const [selectedFile, setSelectedFile] = useState(null);
28
+ const [fileError, setFileError] = useState(null);
29
+ const [isDragging, setIsDragging] = useState(false);
30
+ const kindItems = attachmentKindValues.map((value) => ({
31
+ value,
32
+ label: messages.attachmentDialog.kindLabels[value],
33
+ }));
34
+ if (attachment?.kind && !attachmentKindValues.includes(attachment.kind)) {
35
+ kindItems.push({ value: attachment.kind, label: attachment.kind });
36
+ }
25
37
  const form = useForm({
26
38
  resolver: zodResolver(attachmentFormSchema),
27
39
  defaultValues: {
28
40
  name: "",
29
- kind: "appendix", // i18n-literal-ok domain default attachment kind
41
+ kind: "document", // i18n-literal-ok domain attachment kind
30
42
  mimeType: "",
31
43
  fileSize: undefined,
32
44
  storageKey: "",
@@ -43,29 +55,100 @@ export function AttachmentDialog({ open, onOpenChange, contractId, attachment, o
43
55
  storageKey: attachment.storageKey ?? "",
44
56
  checksum: attachment.checksum ?? "",
45
57
  });
58
+ setSelectedFile(null);
59
+ setFileError(null);
46
60
  }
47
61
  else if (open) {
48
62
  form.reset();
63
+ setSelectedFile(null);
64
+ setFileError(null);
49
65
  }
66
+ setIsDragging(false);
50
67
  }, [open, attachment, form]);
68
+ const applySelectedFile = (file) => {
69
+ setSelectedFile(file);
70
+ setFileError(null);
71
+ setIsDragging(false);
72
+ const currentName = form.getValues("name");
73
+ if (!currentName || currentName === attachment?.name) {
74
+ form.setValue("name", file.name, { shouldDirty: true, shouldValidate: true });
75
+ }
76
+ form.setValue("mimeType", file.type || "", { shouldDirty: true, shouldValidate: true });
77
+ form.setValue("fileSize", file.size, { shouldDirty: true, shouldValidate: true });
78
+ };
79
+ const onFileChange = (event) => {
80
+ const file = event.currentTarget.files?.[0];
81
+ if (!file)
82
+ return;
83
+ applySelectedFile(file);
84
+ event.currentTarget.value = "";
85
+ };
86
+ const onFileDrop = (event) => {
87
+ event.preventDefault();
88
+ event.stopPropagation();
89
+ const file = event.dataTransfer.files?.[0];
90
+ if (file)
91
+ applySelectedFile(file);
92
+ };
93
+ const onFileDragOver = (event) => {
94
+ event.preventDefault();
95
+ event.stopPropagation();
96
+ setIsDragging(true);
97
+ };
98
+ const onFileDragLeave = (event) => {
99
+ event.preventDefault();
100
+ event.stopPropagation();
101
+ setIsDragging(false);
102
+ };
51
103
  const onSubmit = async (values) => {
52
- const payload = {
53
- name: values.name,
54
- kind: values.kind || "appendix", // i18n-literal-ok domain default attachment kind
55
- mimeType: values.mimeType || undefined,
56
- fileSize: values.fileSize || undefined,
57
- storageKey: values.storageKey || undefined,
58
- checksum: values.checksum || undefined,
59
- };
60
- if (isEditing && attachment) {
61
- await update.mutateAsync({ contractId, id: attachment.id, input: payload });
104
+ const uploadInput = selectedFile
105
+ ? {
106
+ file: selectedFile,
107
+ name: values.name,
108
+ kind: values.kind || "document", // i18n-literal-ok domain attachment kind
109
+ }
110
+ : null;
111
+ if (uploadInput && isEditing && attachment) {
112
+ await replaceFile.mutateAsync({ contractId, id: attachment.id, input: uploadInput });
113
+ onSuccess();
114
+ return;
62
115
  }
63
- else {
64
- await create.mutateAsync({ contractId, input: payload });
116
+ if (uploadInput) {
117
+ await upload.mutateAsync({ contractId, input: uploadInput });
118
+ onSuccess();
119
+ return;
65
120
  }
121
+ if (!isEditing) {
122
+ setFileError(messages.attachmentDialog.validation.fileRequired);
123
+ return;
124
+ }
125
+ await update.mutateAsync({
126
+ contractId,
127
+ id: attachment.id,
128
+ input: {
129
+ name: values.name,
130
+ kind: values.kind || "document", // i18n-literal-ok domain attachment kind
131
+ mimeType: values.mimeType || undefined,
132
+ fileSize: values.fileSize,
133
+ storageKey: values.storageKey || undefined,
134
+ checksum: values.checksum || undefined,
135
+ },
136
+ });
66
137
  onSuccess();
67
138
  };
139
+ const isSubmitting = form.formState.isSubmitting || update.isPending || upload.isPending || replaceFile.isPending;
140
+ const submitError = update.error ?? upload.error ?? replaceFile.error ?? null;
68
141
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing
69
142
  ? messages.attachmentDialog.titles.edit
70
- : messages.attachmentDialog.titles.create }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.name }), _jsx(Input, { ...form.register("name"), placeholder: messages.attachmentDialog.placeholders.name }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.kind }), _jsx(Input, { ...form.register("kind"), placeholder: messages.attachmentDialog.placeholders.kind })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.mimeType }), _jsx(Input, { ...form.register("mimeType"), placeholder: messages.attachmentDialog.placeholders.mimeType })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.fileSize }), _jsx(Input, { ...form.register("fileSize"), type: "number", placeholder: messages.attachmentDialog.placeholders.fileSize })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.checksum }), _jsx(Input, { ...form.register("checksum"), placeholder: messages.attachmentDialog.placeholders.checksum })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.storageKey }), _jsx(Input, { ...form.register("storageKey"), placeholder: messages.attachmentDialog.placeholders.storageKey })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? messages.common.saveChanges : messages.attachmentDialog.actions.create] })] })] })] }) }));
143
+ : messages.attachmentDialog.titles.create }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx("input", { type: "hidden", ...form.register("mimeType") }), _jsx("input", { type: "hidden", ...form.register("fileSize") }), _jsx("input", { type: "hidden", ...form.register("storageKey") }), _jsx("input", { type: "hidden", ...form.register("checksum") }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.file }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), onDrop: onFileDrop, onDragOver: onFileDragOver, onDragLeave: onFileDragLeave, "data-dragging": isDragging, className: "flex min-h-32 flex-col items-center justify-center gap-2 rounded-md border border-dashed px-4 py-6 text-center transition-colors hover:border-foreground/30 hover:bg-muted/30 data-[dragging=true]:border-primary data-[dragging=true]:bg-primary/5", children: selectedFile ? (_jsxs(_Fragment, { children: [_jsx(FileText, { className: "size-6 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "max-w-full truncate font-medium text-sm", children: selectedFile.name }), _jsxs("span", { className: "text-muted-foreground text-xs", children: [formatUploadSize(selectedFile.size), selectedFile.type ? ` - ${selectedFile.type}` : ""] })] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "size-6 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-muted-foreground text-sm", children: messages.attachmentDialog.placeholders.file })] })) }), _jsx("input", { ref: fileInputRef, type: "file", className: "hidden", onChange: onFileChange }), fileError ? _jsx("p", { className: "text-xs text-destructive", children: fileError }) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.name }), _jsx(Input, { ...form.register("name"), placeholder: messages.attachmentDialog.placeholders.name }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.attachmentDialog.fields.kind }), _jsxs(Select, { items: kindItems, value: form.watch("kind"), onValueChange: (value) => form.setValue("kind", value ?? undefined, {
144
+ shouldDirty: true,
145
+ shouldValidate: true,
146
+ }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: kindItems.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] })] }), 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" }), isEditing ? messages.common.saveChanges : messages.attachmentDialog.actions.create] })] })] })] }) }));
147
+ }
148
+ function formatUploadSize(bytes) {
149
+ if (bytes < 1024)
150
+ return `${bytes} B`;
151
+ if (bytes < 1024 * 1024)
152
+ return `${Math.round(bytes / 1024)} KB`;
153
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
71
154
  }
@@ -4,13 +4,20 @@ export interface ContractDetailPageProps {
4
4
  id: string;
5
5
  onBackToContracts?: () => void;
6
6
  renderContractDialog?: (props: ContractDetailDialogRenderProps) => ReactNode;
7
+ renderReference?: (props: ContractReferenceRenderProps) => ReactNode;
7
8
  getAttachmentDownloadHref?: (attachment: LegalContractAttachmentRecord) => string;
8
9
  }
10
+ export type ContractReferenceKind = "person" | "organization" | "supplier" | "channel" | "booking" | "order";
11
+ export interface ContractReferenceRenderProps {
12
+ kind: ContractReferenceKind;
13
+ id: string;
14
+ contract: LegalContractRecord;
15
+ }
9
16
  export interface ContractDetailDialogRenderProps {
10
17
  open: boolean;
11
18
  onOpenChange: (open: boolean) => void;
12
19
  contract: LegalContractRecord;
13
20
  onSuccess: () => void;
14
21
  }
15
- export declare function ContractDetailPage({ id, onBackToContracts, renderContractDialog, getAttachmentDownloadHref, }: ContractDetailPageProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, }: ContractDetailPageProps): import("react/jsx-runtime").JSX.Element;
16
23
  //# sourceMappingURL=contract-detail-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/contract-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EAMzB,MAAM,uBAAuB,CAAA;AAW9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAqBtC,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC9B,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,SAAS,CAAA;IAC5E,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,6BAA6B,KAAK,MAAM,CAAA;CAClF;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,EAAE,EACF,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,GAC1B,EAAE,uBAAuB,2CA0TzB"}
1
+ {"version":3,"file":"contract-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/contract-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EAMzB,MAAM,uBAAuB,CAAA;AAY9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAqBtC,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC9B,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,SAAS,CAAA;IAC5E,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,4BAA4B,KAAK,SAAS,CAAA;IACpE,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,6BAA6B,KAAK,MAAM,CAAA;CAClF;AAED,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,GACR,cAAc,GACd,UAAU,GACV,SAAS,GACT,SAAS,GACT,OAAO,CAAA;AAEX,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,qBAAqB,CAAA;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,mBAAmB,CAAA;CAC9B;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,EAAE,EACF,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,yBAAyB,GAC1B,EAAE,uBAAuB,2CAwVzB"}
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQueryClient } from "@tanstack/react-query";
3
3
  import { formatMessage } from "@voyantjs/i18n";
4
4
  import { useLegalContract, useLegalContractAttachmentMutation, useLegalContractAttachments, useLegalContractMutation, useLegalContractSignatures, } from "@voyantjs/legal-react";
5
- import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
5
+ import { Badge, Button } from "@voyantjs/ui/components";
6
6
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
7
8
  import { ArrowLeft, ExternalLink, FileText, Pencil, Plus, Trash2 } from "lucide-react";
8
9
  import { useState } from "react";
9
10
  import { useLegalUiI18nOrDefault, useLegalUiMessagesOrDefault } from "../i18n/index.js";
@@ -18,7 +19,7 @@ const statusVariant = {
18
19
  expired: "destructive",
19
20
  void: "destructive",
20
21
  };
21
- export function ContractDetailPage({ id, onBackToContracts, renderContractDialog, getAttachmentDownloadHref, }) {
22
+ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, }) {
22
23
  const queryClient = useQueryClient();
23
24
  const i18n = useLegalUiI18nOrDefault();
24
25
  const messages = useLegalUiMessagesOrDefault();
@@ -44,7 +45,8 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
44
45
  }
45
46
  const status = contract.status;
46
47
  const canAddSignature = status === "issued" || status === "sent" || status === "signed";
47
- return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [onBackToContracts ? (_jsx(Button, { variant: "ghost", size: "icon", onClick: onBackToContracts, children: _jsx(ArrowLeft, { className: "size-4", "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: contract.title }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: messages.common.contractScopeLabels[contract.scope] ?? contract.scope }), _jsx(Badge, { variant: statusVariant[status] ?? "secondary", children: messages.common.contractStatusLabels[status] ?? status }), contract.contractNumber ? (_jsx("span", { className: "font-mono text-xs text-muted-foreground", children: contract.contractNumber })) : null] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [status === "draft" ? (_jsx(Button, { size: "sm", onClick: () => issue.mutate(id), disabled: issue.isPending, children: f.actions.issue })) : null, status !== "void" ? (_jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
48
+ const renderReferenceValue = (kind, referenceId) => renderReference?.({ kind, id: referenceId, contract }) ?? (_jsx("span", { className: "font-mono text-xs", children: referenceId }));
49
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-4", children: [onBackToContracts ? (_jsx(Button, { variant: "ghost", size: "icon", onClick: onBackToContracts, children: _jsx(ArrowLeft, { className: "size-4", "aria-hidden": "true" }) })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: f.title }), _jsx(Badge, { variant: statusVariant[status] ?? "secondary", children: messages.common.contractStatusLabels[status] ?? status }), _jsx(Badge, { variant: "outline", children: messages.common.contractScopeLabels[contract.scope] ?? contract.scope })] }), _jsx("p", { className: "mt-1 truncate font-mono text-muted-foreground text-sm", children: contract.contractNumber ?? contract.title }), contract.contractNumber ? (_jsx("p", { className: "mt-1 truncate text-muted-foreground text-xs", children: contract.title })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [status === "draft" ? (_jsx(Button, { size: "sm", onClick: () => issue.mutate(id), disabled: issue.isPending, children: f.actions.issue })) : null, status !== "void" ? (_jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
48
50
  if (confirm(f.voidConfirm)) {
49
51
  voidContract.mutate(id);
50
52
  }
@@ -52,20 +54,22 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
52
54
  if (confirm(formatMessage(f.deleteConfirm, { title: contract.title }))) {
53
55
  remove.mutate(id, { onSuccess: () => onBackToContracts?.() });
54
56
  }
55
- }, disabled: remove.isPending, children: [_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.common.delete] })) : null] })] }), _jsxs("div", { className: "grid gap-6 md:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: f.sections.details }) }), _jsxs(CardContent, { className: "grid gap-3 text-sm", children: [_jsx(DetailRow, { label: f.fields.language, children: contract.language }), contract.templateVersionId ? (_jsx(DetailRow, { label: f.fields.templateVersion, children: _jsx("span", { className: "font-mono text-xs", children: contract.templateVersionId }) })) : null, contract.seriesId ? (_jsx(DetailRow, { label: f.fields.series, children: _jsx("span", { className: "font-mono text-xs", children: contract.seriesId }) })) : null, contract.expiresAt ? (_jsx(DetailRow, { label: f.fields.expires, children: i18n.formatDate(contract.expiresAt) })) : null, _jsxs("div", { className: "mt-2 border-t pt-3", children: [_jsx(DetailRow, { label: f.fields.created, children: i18n.formatDate(contract.createdAt) }), _jsx(DetailRow, { label: f.fields.updated, children: i18n.formatDate(contract.updatedAt) })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: f.sections.parties }) }), _jsxs(CardContent, { className: "grid gap-3 text-sm", children: [contract.personId ? (_jsx(DetailRow, { label: f.fields.person, children: _jsx("span", { className: "font-mono text-xs", children: contract.personId }) })) : null, contract.organizationId ? (_jsx(DetailRow, { label: f.fields.organization, children: _jsx("span", { className: "font-mono text-xs", children: contract.organizationId }) })) : null, contract.supplierId ? (_jsx(DetailRow, { label: f.fields.supplier, children: _jsx("span", { className: "font-mono text-xs", children: contract.supplierId }) })) : null, contract.channelId ? (_jsx(DetailRow, { label: f.fields.channel, children: _jsx("span", { className: "font-mono text-xs", children: contract.channelId }) })) : null, !contract.personId &&
57
+ }, disabled: remove.isPending, children: [_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.common.delete] })) : null] })] }), _jsxs(Tabs, { defaultValue: "details", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "details", children: f.sections.details }), _jsx(TabsTrigger, { value: "parties", children: f.sections.parties }), _jsx(TabsTrigger, { value: "signatures", children: f.sections.signatures }), _jsx(TabsTrigger, { value: "documents", children: f.sections.documents })] }), _jsx(TabsContent, { value: "details", className: "mt-4", children: _jsx(ContractSection, { title: f.sections.details, children: _jsxs("div", { className: "grid gap-3 text-sm", children: [_jsx(DetailRow, { label: f.fields.language, children: contract.language }), contract.templateVersionId ? (_jsx(DetailRow, { label: f.fields.templateVersion, children: _jsx("span", { className: "font-mono text-xs", children: contract.templateVersionId }) })) : null, contract.seriesId ? (_jsx(DetailRow, { label: f.fields.series, children: _jsx("span", { className: "font-mono text-xs", children: contract.seriesId }) })) : null, contract.expiresAt ? (_jsx(DetailRow, { label: f.fields.expires, children: i18n.formatDate(contract.expiresAt) })) : null, _jsxs("div", { className: "mt-2 border-t pt-3", children: [_jsx(DetailRow, { label: f.fields.created, children: i18n.formatDate(contract.createdAt) }), _jsx(DetailRow, { label: f.fields.updated, children: i18n.formatDate(contract.updatedAt) })] })] }) }) }), _jsx(TabsContent, { value: "parties", className: "mt-4", children: _jsx(ContractSection, { title: f.sections.parties, children: _jsxs("div", { className: "grid gap-3 text-sm", children: [contract.personId ? (_jsx(DetailRow, { label: f.fields.person, children: renderReferenceValue("person", contract.personId) })) : null, contract.organizationId ? (_jsx(DetailRow, { label: f.fields.organization, children: renderReferenceValue("organization", contract.organizationId) })) : null, contract.supplierId ? (_jsx(DetailRow, { label: f.fields.supplier, children: renderReferenceValue("supplier", contract.supplierId) })) : null, contract.channelId ? (_jsx(DetailRow, { label: f.fields.channel, children: renderReferenceValue("channel", contract.channelId) })) : null, contract.bookingId ? (_jsx(DetailRow, { label: f.fields.booking, children: renderReferenceValue("booking", contract.bookingId) })) : null, contract.orderId ? (_jsx(DetailRow, { label: f.fields.order, children: renderReferenceValue("order", contract.orderId) })) : null, !contract.personId &&
56
58
  !contract.organizationId &&
57
59
  !contract.supplierId &&
58
- !contract.channelId ? (_jsx("p", { className: "text-muted-foreground", children: f.empty.noParties })) : null] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { children: f.sections.signatures }), canAddSignature ? (_jsxs(Button, { size: "sm", onClick: () => setSignOpen(true), children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.addSignature] })) : null] }), _jsx(CardContent, { children: !signatures || signatures.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: f.empty.noSignatures })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.fields.name }), _jsx(TableHead, { children: f.fields.email }), _jsx(TableHead, { children: f.fields.role }), _jsx(TableHead, { children: f.fields.method }), _jsx(TableHead, { children: f.fields.signedAt })] }) }), _jsx(TableBody, { children: signatures.map((signature) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: signature.signerName }), _jsx(TableCell, { children: signature.signerEmail ?? messages.common.noResultsDash }), _jsx(TableCell, { children: signature.signerRole ?? messages.common.noResultsDash }), _jsx(TableCell, { children: signature.method }), _jsx(TableCell, { children: i18n.formatDateTime(signature.signedAt) })] }, signature.id))) })] }) })) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(CardTitle, { children: f.sections.attachments }), _jsxs(Button, { size: "sm", onClick: () => {
60
+ !contract.channelId &&
61
+ !contract.bookingId &&
62
+ !contract.orderId ? (_jsx("p", { className: "text-muted-foreground", children: f.empty.noParties })) : null] }) }) }), _jsx(TabsContent, { value: "signatures", className: "mt-4", children: _jsx(ContractSection, { title: f.sections.signatures, action: canAddSignature ? (_jsxs(Button, { size: "sm", onClick: () => setSignOpen(true), children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.addSignature] })) : null, children: !signatures || signatures.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: f.empty.noSignatures })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.fields.name }), _jsx(TableHead, { children: f.fields.email }), _jsx(TableHead, { children: f.fields.role }), _jsx(TableHead, { children: f.fields.method }), _jsx(TableHead, { children: f.fields.signedAt })] }) }), _jsx(TableBody, { children: signatures.map((signature) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: signature.signerName }), _jsx(TableCell, { children: signature.signerEmail ?? messages.common.noResultsDash }), _jsx(TableCell, { children: signature.signerRole ?? messages.common.noResultsDash }), _jsx(TableCell, { children: signature.method }), _jsx(TableCell, { children: i18n.formatDateTime(signature.signedAt) })] }, signature.id))) })] }) })) }) }), _jsx(TabsContent, { value: "documents", className: "mt-4", children: _jsx(ContractSection, { title: f.sections.documents, action: _jsxs(Button, { size: "sm", onClick: () => {
59
63
  setEditingAttachment(undefined);
60
64
  setAttachOpen(true);
61
- }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.addAttachment] })] }), _jsx(CardContent, { children: !attachments || attachments.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: f.empty.noAttachments })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.fields.name }), _jsx(TableHead, { children: f.fields.kind }), _jsx(TableHead, { children: f.fields.mimeType }), _jsx(TableHead, { children: f.fields.size }), _jsx(TableHead, { className: "w-16" })] }) }), _jsx(TableBody, { children: attachments.map((attachment) => (_jsx(AttachmentRow, { attachment: attachment, downloadHref: getAttachmentDownloadHref?.(attachment), onEdit: () => {
62
- setEditingAttachment(attachment);
63
- setAttachOpen(true);
64
- }, onDelete: () => {
65
- if (confirm(f.deleteAttachmentConfirm)) {
66
- removeAttachment.mutate({ contractId: id, id: attachment.id });
67
- }
68
- } }, attachment.id))) })] }) })) })] }), renderContractDialog?.({
65
+ }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.addDocument] }), children: !attachments || attachments.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: f.empty.noAttachments })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.fields.name }), _jsx(TableHead, { children: f.fields.kind }), _jsx(TableHead, { children: f.fields.mimeType }), _jsx(TableHead, { children: f.fields.size }), _jsx(TableHead, { className: "w-16" })] }) }), _jsx(TableBody, { children: attachments.map((attachment) => (_jsx(AttachmentRow, { attachment: attachment, downloadHref: getAttachmentDownloadHref?.(attachment), onEdit: () => {
66
+ setEditingAttachment(attachment);
67
+ setAttachOpen(true);
68
+ }, onDelete: () => {
69
+ if (confirm(f.deleteAttachmentConfirm)) {
70
+ removeAttachment.mutate({ contractId: id, id: attachment.id });
71
+ }
72
+ } }, attachment.id))) })] }) })) }) })] }), renderContractDialog?.({
69
73
  open: editOpen,
70
74
  onOpenChange: setEditOpen,
71
75
  contract,
@@ -86,6 +90,9 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
86
90
  function DetailRow({ label, children }) {
87
91
  return (_jsxs("div", { children: [_jsxs("span", { className: "text-muted-foreground", children: [label, ":"] }), " ", _jsx("span", { children: children })] }));
88
92
  }
93
+ function ContractSection({ title, action, children, }) {
94
+ return (_jsxs("section", { className: "rounded-md border bg-background", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3", children: [_jsx("h2", { className: "font-semibold text-sm", children: title }), action] }), _jsx("div", { className: "p-4", children: children })] }));
95
+ }
89
96
  function AttachmentRow({ attachment, downloadHref, onEdit, onDelete, }) {
90
97
  const messages = useLegalUiMessagesOrDefault();
91
98
  const f = messages.contractDetailPage;
@@ -4,11 +4,27 @@ export interface ContractDialogRenderProps {
4
4
  onOpenChange: (open: boolean) => void;
5
5
  onSuccess: () => void;
6
6
  }
7
+ export interface ContractPersonSummary {
8
+ id: string;
9
+ firstName?: string | null;
10
+ lastName?: string | null;
11
+ email?: string | null;
12
+ phone?: string | null;
13
+ }
14
+ export interface ContractPersonFilterRenderProps {
15
+ personId: string | null;
16
+ selectedPerson: ContractPersonSummary | null;
17
+ onPersonChange: (personId: string | null, person?: ContractPersonSummary | null) => void;
18
+ }
7
19
  export interface ContractsPageProps {
8
20
  className?: string;
21
+ defaultPersonId?: string | null;
22
+ personId?: string | null;
23
+ onPersonIdChange?: (personId: string | null) => void;
9
24
  onOpenContract?: (contractId: string) => void;
10
25
  renderContractDialog?: (props: ContractDialogRenderProps) => ReactNode;
11
- renderPersonCell?: (personId: string | null) => ReactNode;
26
+ renderPersonCell?: (personId: string | null, person: ContractPersonSummary | null) => ReactNode;
27
+ renderPersonFilter?: (props: ContractPersonFilterRenderProps) => ReactNode;
12
28
  }
13
- export declare function ContractsPage({ className, onOpenContract, renderContractDialog, renderPersonCell, }?: ContractsPageProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function ContractsPage({ className, defaultPersonId, personId, onPersonIdChange, onOpenContract, renderContractDialog, renderPersonCell, renderPersonFilter, }?: ContractsPageProps): import("react/jsx-runtime").JSX.Element;
14
30
  //# sourceMappingURL=contracts-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contracts-page.d.ts","sourceRoot":"","sources":["../../src/components/contracts-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAoBtC,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,SAAS,CAAA;IACtE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,SAAS,CAAA;CAC1D;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GACjB,GAAE,kBAAuB,2CA8LzB"}
1
+ {"version":3,"file":"contracts-page.d.ts","sourceRoot":"","sources":["../../src/components/contracts-page.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAoBtC,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,cAAc,EAAE,qBAAqB,GAAG,IAAI,CAAA;IAC5C,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,KAAK,IAAI,CAAA;CACzF;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACpD,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,SAAS,CAAA;IACtE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,KAAK,SAAS,CAAA;IAC/F,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,SAAS,CAAA;CAC3E;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,eAAe,EACf,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACnB,GAAE,kBAAuB,2CA6NzB"}
@@ -1,12 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { usePeople } from "@voyantjs/crm-react";
2
3
  import { formatMessage } from "@voyantjs/i18n";
3
4
  import { useLegalContracts } from "@voyantjs/legal-react";
4
5
  import { Badge, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
6
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@voyantjs/ui/components/command";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
5
8
  import { Skeleton } from "@voyantjs/ui/components/skeleton";
6
9
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
7
10
  import { cn } from "@voyantjs/ui/lib/utils";
8
- import { Plus, Search } from "lucide-react";
9
- import { useState } from "react";
11
+ import { ChevronDown, Plus, Search, User, X } from "lucide-react";
12
+ import { useMemo, useState } from "react";
10
13
  import { useLegalUiI18nOrDefault, useLegalUiMessagesOrDefault } from "../i18n/index.js";
11
14
  import { legalContractScopes, legalContractStatuses } from "../i18n/messages.js";
12
15
  const PAGE_SIZE = 25;
@@ -21,19 +24,23 @@ const statusVariant = {
21
24
  expired: "destructive",
22
25
  void: "destructive",
23
26
  };
24
- export function ContractsPage({ className, onOpenContract, renderContractDialog, renderPersonCell, } = {}) {
27
+ export function ContractsPage({ className, defaultPersonId, personId, onPersonIdChange, onOpenContract, renderContractDialog, renderPersonCell, renderPersonFilter, } = {}) {
25
28
  const i18n = useLegalUiI18nOrDefault();
26
29
  const messages = useLegalUiMessagesOrDefault();
27
30
  const f = messages.contractsPage;
28
31
  const [search, setSearch] = useState("");
29
32
  const [scope, setScope] = useState(SCOPE_ALL);
30
33
  const [status, setStatus] = useState(STATUS_ALL);
34
+ const [internalPersonId, setInternalPersonId] = useState(() => getInitialPersonId(defaultPersonId));
35
+ const [selectedPerson, setSelectedPerson] = useState(null);
31
36
  const [dialogOpen, setDialogOpen] = useState(false);
32
37
  const [pageIndex, setPageIndex] = useState(0);
38
+ const resolvedPersonId = personId ?? internalPersonId;
33
39
  const { data, isPending, isFetching, isError, refetch } = useLegalContracts({
34
40
  search,
35
41
  scope: scope === SCOPE_ALL ? "all" : scope,
36
42
  status: status === STATUS_ALL ? "all" : status,
43
+ personId: resolvedPersonId || undefined,
37
44
  limit: PAGE_SIZE,
38
45
  offset: pageIndex * PAGE_SIZE,
39
46
  });
@@ -43,6 +50,25 @@ export function ContractsPage({ className, onOpenContract, renderContractDialog,
43
50
  const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
44
51
  const showSkeleton = isPending || (isFetching && contracts.length === 0);
45
52
  const resetPage = () => setPageIndex(0);
53
+ const contractPersonSummary = useMemo(() => {
54
+ if (!resolvedPersonId)
55
+ return null;
56
+ if (selectedPerson?.id === resolvedPersonId)
57
+ return selectedPerson;
58
+ for (const contract of contracts) {
59
+ const person = getContractPersonSummary(contract);
60
+ if (person?.id === resolvedPersonId)
61
+ return person;
62
+ }
63
+ return { id: resolvedPersonId };
64
+ }, [contracts, resolvedPersonId, selectedPerson]);
65
+ const handlePersonChange = (nextPersonId, person) => {
66
+ if (personId === undefined)
67
+ setInternalPersonId(nextPersonId ?? "");
68
+ setSelectedPerson(person ?? null);
69
+ onPersonIdChange?.(nextPersonId);
70
+ resetPage();
71
+ };
46
72
  return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: f.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: f.description })] }), renderContractDialog ? (_jsxs(Button, { onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), f.create] })) : null] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "relative min-w-[14rem] max-w-sm flex-1", children: [_jsx(Label, { htmlFor: "contracts-search", className: "sr-only", children: f.searchPlaceholder }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "contracts-search", placeholder: f.searchPlaceholder, value: search, onChange: (event) => {
47
73
  setSearch(event.target.value);
48
74
  resetPage();
@@ -52,7 +78,11 @@ export function ContractsPage({ className, onOpenContract, renderContractDialog,
52
78
  }, children: [_jsx(SelectTrigger, { className: "w-[12.5rem]", children: _jsx(SelectValue, { placeholder: f.filters.scope }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: SCOPE_ALL, children: f.filters.allScopes }), legalContractScopes.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.contractScopeLabels[value] }, value)))] })] }), _jsxs(Select, { value: status, onValueChange: (value) => {
53
79
  setStatus(value ?? STATUS_ALL);
54
80
  resetPage();
55
- }, children: [_jsx(SelectTrigger, { className: "w-[12.5rem]", children: _jsx(SelectValue, { placeholder: f.filters.status }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: STATUS_ALL, children: f.filters.allStatuses }), legalContractStatuses.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.contractStatusLabels[value] }, value)))] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.columns.number }), _jsx(TableHead, { children: f.columns.title }), _jsx(TableHead, { children: f.columns.scope }), _jsx(TableHead, { children: f.columns.status }), _jsx(TableHead, { children: f.columns.person }), _jsx(TableHead, { children: f.columns.created })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(ContractRowSkeleton, { rows: 6 })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "h-24 text-center text-sm text-destructive", children: f.loadFailed }) })) : contracts.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "h-24 text-center text-sm text-muted-foreground", children: f.empty }) })) : (contracts.map((contract) => (_jsxs(TableRow, { onClick: () => onOpenContract?.(contract.id), className: cn(onOpenContract && "cursor-pointer"), children: [_jsx(TableCell, { className: "font-mono text-xs", children: contract.contractNumber ?? messages.common.noResultsDash }), _jsx(TableCell, { className: "font-medium", children: contract.title }), _jsx(TableCell, { children: _jsx(Badge, { variant: "outline", children: messages.common.contractScopeLabels[contract.scope] ?? contract.scope }) }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusVariant[contract.status] ?? "secondary", children: messages.common.contractStatusLabels[contract.status] }) }), _jsx(TableCell, { children: renderPersonCell ? (renderPersonCell(contract.personId)) : (_jsx("span", { className: "font-mono text-xs", children: contract.personId ?? messages.common.noResultsDash })) }), _jsx(TableCell, { children: i18n.formatDate(contract.createdAt) })] }, contract.id)))) })] }) }), _jsx(PaginationBar, { shown: contracts.length, total: total, page: page, pageCount: pageCount, onPrevious: () => setPageIndex((prev) => Math.max(0, prev - 1)), onNext: () => setPageIndex((prev) => prev + 1), canGoBack: pageIndex > 0, canGoForward: (pageIndex + 1) * PAGE_SIZE < total }), renderContractDialog?.({
81
+ }, children: [_jsx(SelectTrigger, { className: "w-[12.5rem]", children: _jsx(SelectValue, { placeholder: f.filters.status }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: STATUS_ALL, children: f.filters.allStatuses }), legalContractStatuses.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.contractStatusLabels[value] }, value)))] })] }), renderPersonFilter ? (renderPersonFilter({
82
+ personId: resolvedPersonId || null,
83
+ selectedPerson: contractPersonSummary,
84
+ onPersonChange: handlePersonChange,
85
+ })) : (_jsx(ContractsPersonFilter, { value: resolvedPersonId || null, selectedPerson: contractPersonSummary, onChange: handlePersonChange }))] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.columns.number }), _jsx(TableHead, { children: f.columns.title }), _jsx(TableHead, { children: f.columns.scope }), _jsx(TableHead, { children: f.columns.status }), _jsx(TableHead, { children: f.columns.person }), _jsx(TableHead, { children: f.columns.created })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(ContractRowSkeleton, { rows: 6 })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "h-24 text-center text-sm text-destructive", children: f.loadFailed }) })) : contracts.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "h-24 text-center text-sm text-muted-foreground", children: f.empty }) })) : (contracts.map((contract) => (_jsxs(TableRow, { onClick: () => onOpenContract?.(contract.id), className: cn(onOpenContract && "cursor-pointer"), children: [_jsx(TableCell, { className: "font-mono text-xs", children: contract.contractNumber ?? messages.common.noResultsDash }), _jsx(TableCell, { className: "font-medium", children: contract.title }), _jsx(TableCell, { children: _jsx(Badge, { variant: "outline", children: messages.common.contractScopeLabels[contract.scope] ?? contract.scope }) }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusVariant[contract.status] ?? "secondary", children: messages.common.contractStatusLabels[contract.status] }) }), _jsx(TableCell, { children: _jsx(ContractPersonCell, { contract: contract, renderPersonCell: renderPersonCell }) }), _jsx(TableCell, { children: i18n.formatDate(contract.createdAt) })] }, contract.id)))) })] }) }), _jsx(PaginationBar, { shown: contracts.length, total: total, page: page, pageCount: pageCount, onPrevious: () => setPageIndex((prev) => Math.max(0, prev - 1)), onNext: () => setPageIndex((prev) => prev + 1), canGoBack: pageIndex > 0, canGoForward: (pageIndex + 1) * PAGE_SIZE < total }), renderContractDialog?.({
56
86
  open: dialogOpen,
57
87
  onOpenChange: setDialogOpen,
58
88
  onSuccess: () => {
@@ -62,6 +92,81 @@ export function ContractsPage({ className, onOpenContract, renderContractDialog,
62
92
  },
63
93
  })] }));
64
94
  }
95
+ function getInitialPersonId(defaultPersonId) {
96
+ if (defaultPersonId !== undefined)
97
+ return defaultPersonId ?? "";
98
+ if (typeof window === "undefined")
99
+ return "";
100
+ return new URLSearchParams(window.location.search).get("personId") ?? "";
101
+ }
102
+ function personSummaryFromRecord(person) {
103
+ return {
104
+ id: person.id,
105
+ firstName: person.firstName,
106
+ lastName: person.lastName,
107
+ email: person.email,
108
+ phone: person.phone,
109
+ };
110
+ }
111
+ function getContractPersonSummary(contract) {
112
+ if (!contract.personId)
113
+ return null;
114
+ return {
115
+ id: contract.personId,
116
+ firstName: contract.personFirstName,
117
+ lastName: contract.personLastName,
118
+ email: contract.personEmail,
119
+ phone: contract.personPhone,
120
+ };
121
+ }
122
+ function formatPersonName(person) {
123
+ if (!person)
124
+ return null;
125
+ const name = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
126
+ return name || null;
127
+ }
128
+ function formatPersonLabel(person, fallback) {
129
+ return formatPersonName(person) ?? person?.email ?? person?.phone ?? fallback ?? null;
130
+ }
131
+ function ContractsPersonFilter({ value, selectedPerson, onChange, }) {
132
+ const messages = useLegalUiMessagesOrDefault().contractsPage.filters;
133
+ const [open, setOpen] = useState(false);
134
+ const [search, setSearch] = useState("");
135
+ const peopleQuery = usePeople({
136
+ search: search.trim() || undefined,
137
+ limit: 25,
138
+ enabled: open,
139
+ });
140
+ const people = peopleQuery.data?.data ?? [];
141
+ const label = formatPersonLabel(selectedPerson, value) ?? messages.allPeople;
142
+ return (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", className: "w-[14rem] justify-between gap-2 px-3" }), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(User, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: label })] }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }), _jsx(PopoverContent, { className: "w-[20rem] p-0", align: "start", children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { value: search, onValueChange: setSearch, placeholder: messages.personSearchPlaceholder }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: peopleQuery.isLoading ? messages.personSearching : messages.personEmpty }), _jsx(CommandGroup, { children: people.map((person) => {
143
+ const summary = personSummaryFromRecord(person);
144
+ const name = formatPersonName(summary);
145
+ return (_jsx(CommandItem, { value: `${name ?? ""} ${person.email ?? ""} ${person.phone ?? ""}`, onSelect: () => {
146
+ onChange(person.id, summary);
147
+ setOpen(false);
148
+ setSearch("");
149
+ }, children: _jsxs("div", { className: "flex min-w-0 flex-1 flex-col leading-tight", children: [_jsx("span", { className: "truncate font-medium text-sm", children: name ?? person.id }), (person.email || person.phone) && (_jsx("span", { className: "truncate text-muted-foreground text-xs", children: person.email ?? person.phone }))] }) }, person.id));
150
+ }) }), value ? (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsxs(CommandItem, { value: "__clear_person_filter", onSelect: () => {
151
+ onChange(null, null);
152
+ setOpen(false);
153
+ setSearch("");
154
+ }, children: [_jsx(X, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.clearPerson] }) })] })) : null] })] }) })] }), value ? (_jsx(Button, { type: "button", variant: "outline", size: "icon", "aria-label": messages.clearPerson, onClick: () => onChange(null, null), children: _jsx(X, { className: "size-4", "aria-hidden": "true" }) })) : null] }));
155
+ }
156
+ function ContractPersonCell({ contract, renderPersonCell, }) {
157
+ const messages = useLegalUiMessagesOrDefault();
158
+ const person = getContractPersonSummary(contract);
159
+ if (renderPersonCell)
160
+ return renderPersonCell(contract.personId, person);
161
+ if (!contract.personId) {
162
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: messages.common.noResultsDash });
163
+ }
164
+ const name = formatPersonName(person);
165
+ if (!name && !person?.email && !person?.phone) {
166
+ return _jsx("span", { className: "font-mono text-xs", children: contract.personId });
167
+ }
168
+ return (_jsxs("div", { className: "flex min-w-0 flex-col leading-tight", children: [_jsx("span", { className: "truncate font-medium text-sm", children: name ?? contract.personId }), (person?.email || person?.phone) && (_jsx("span", { className: "truncate text-muted-foreground text-xs", children: person.email ?? person.phone }))] }));
169
+ }
65
170
  function PaginationBar({ shown, total, page, pageCount, onPrevious, onNext, canGoBack, canGoForward, }) {
66
171
  const messages = useLegalUiMessagesOrDefault();
67
172
  const f = messages.contractsPage.pagination;
@@ -1 +1 @@
1
- {"version":3,"file":"policy-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/policy-rule-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAA8B,MAAM,uBAAuB,CAAA;AAqD9F,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAA;AAE5C,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,qBAAqB,2CAkMvB"}
1
+ {"version":3,"file":"policy-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/policy-rule-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAA8B,MAAM,uBAAuB,CAAA;AAsD9F,MAAM,MAAM,QAAQ,GAAG,qBAAqB,CAAA;AAE5C,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,qBAAqB,2CAwMvB"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useLegalPolicyRuleMutation } from "@voyantjs/legal-react";
3
3
  import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
4
  import { CurrencyCombobox } from "@voyantjs/ui/components/currency-combobox";
5
+ import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
5
6
  import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
6
7
  import { Loader2 } from "lucide-react";
7
8
  import { useEffect } from "react";
@@ -93,5 +94,8 @@ export function PolicyRuleDialog({ open, onOpenChange, versionId, rule, onSucces
93
94
  : messages.policyRuleDialog.titles.create }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.ruleType }), _jsxs(Select, { items: ruleTypeItems, value: form.watch("ruleType"), onValueChange: (v) => form.setValue("ruleType", v, { shouldValidate: true }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ruleTypeItems.map((t) => (_jsx(SelectItem, { value: t.value, children: t.label }, t.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.sortOrder }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.label }), _jsx(Input, { ...form.register("label"), placeholder: messages.policyRuleDialog.placeholders.label })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.daysBeforeDeparture }), _jsx(Input, { ...form.register("daysBeforeDeparture"), type: "number", placeholder: messages.policyRuleDialog.placeholders.daysBeforeDeparture })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.refundPercent }), _jsx(Input, { ...form.register("refundPercent"), type: "number", placeholder: messages.policyRuleDialog.placeholders.refundPercent })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.refundType }), _jsxs(Select, { items: refundTypeItems, value: form.watch("refundType") ?? "", onValueChange: (v) => form.setValue("refundType", (v || undefined)), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: messages.common.selectPlaceholder }) }), _jsx(SelectContent, { children: refundTypeItems.map((t) => (_jsx(SelectItem, { value: t.value, children: t.label }, t.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.currency }), _jsx(CurrencyCombobox, { value: form.watch("currency") || null, onChange: (next) => form.setValue("currency", next ?? "EUR" /* i18n-literal-ok domain default currency */, {
94
95
  shouldValidate: true,
95
96
  shouldDirty: true,
96
- }) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.flatAmountCents }), _jsx(Input, { ...form.register("flatAmountCents"), type: "number", placeholder: messages.policyRuleDialog.placeholders.flatAmountCents })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? messages.common.saveChanges : messages.policyRuleDialog.actions.create] })] })] })] }) }));
97
+ }) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.policyRuleDialog.fields.flatAmountCents }), _jsx(CurrencyInput, { value: form.watch("flatAmountCents"), onChange: (next) => form.setValue("flatAmountCents", next ?? undefined, {
98
+ shouldDirty: true,
99
+ shouldValidate: true,
100
+ }), currency: form.watch("currency"), placeholder: messages.policyRuleDialog.placeholders.flatAmountCents })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? messages.common.saveChanges : messages.policyRuleDialog.actions.create] })] })] })] }) }));
97
101
  }