@voyantjs/legal-ui 0.52.0 → 0.52.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/contract-detail-page.d.ts +8 -2
- package/dist/components/contract-detail-page.d.ts.map +1 -1
- package/dist/components/contract-detail-page.js +26 -36
- package/dist/components/contract-send-dialog.d.ts +37 -0
- package/dist/components/contract-send-dialog.d.ts.map +1 -0
- package/dist/components/contract-send-dialog.js +56 -0
- package/dist/components/contracts-page.d.ts.map +1 -1
- package/dist/components/contracts-page.js +28 -14
- package/dist/components/policies-page.d.ts.map +1 -1
- package/dist/components/policies-page.js +17 -7
- package/dist/components/templates-page.d.ts +7 -2
- package/dist/components/templates-page.d.ts.map +1 -1
- package/dist/components/templates-page.js +32 -65
- package/dist/i18n/en.d.ts +31 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +31 -0
- package/dist/i18n/messages.d.ts +31 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +62 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +31 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +9 -9
|
@@ -7,6 +7,12 @@ export interface ContractDetailPageProps {
|
|
|
7
7
|
renderReference?: (props: ContractReferenceRenderProps) => ReactNode;
|
|
8
8
|
getAttachmentDownloadHref?: (attachment: LegalContractAttachmentRecord) => string;
|
|
9
9
|
slots?: ContractDetailPageSlots;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the recipient email for the Send-contract dialog. Operator
|
|
12
|
+
* templates wire this to the linked CRM person's primary email; if
|
|
13
|
+
* unset the dialog displays a "no recipient" warning and disables Send.
|
|
14
|
+
*/
|
|
15
|
+
resolveSendRecipientEmail?: (contract: LegalContractRecord) => string | null | undefined;
|
|
10
16
|
}
|
|
11
17
|
export interface ContractDetailPageSlots {
|
|
12
18
|
detailsContent?: ReactNode;
|
|
@@ -14,7 +20,7 @@ export interface ContractDetailPageSlots {
|
|
|
14
20
|
signaturesContent?: ReactNode;
|
|
15
21
|
documentsContent?: ReactNode;
|
|
16
22
|
}
|
|
17
|
-
export type ContractReferenceKind = "person" | "organization" | "supplier" | "channel" | "booking" | "order";
|
|
23
|
+
export type ContractReferenceKind = "person" | "organization" | "supplier" | "channel" | "booking" | "order" | "templateVersion" | "series";
|
|
18
24
|
export interface ContractReferenceRenderProps {
|
|
19
25
|
kind: ContractReferenceKind;
|
|
20
26
|
id: string;
|
|
@@ -26,5 +32,5 @@ export interface ContractDetailDialogRenderProps {
|
|
|
26
32
|
contract: LegalContractRecord;
|
|
27
33
|
onSuccess: () => void;
|
|
28
34
|
}
|
|
29
|
-
export declare function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, slots, }: ContractDetailPageProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export declare function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, slots, resolveSendRecipientEmail, }: ContractDetailPageProps): import("react/jsx-runtime").JSX.Element;
|
|
30
36
|
//# 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;
|
|
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;AAoB9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAsBtC,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;IACjF,KAAK,CAAC,EAAE,uBAAuB,CAAA;IAC/B;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CACzF;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,iBAAiB,CAAC,EAAE,SAAS,CAAA;IAC7B,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AAED,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,GACR,cAAc,GACd,UAAU,GACV,SAAS,GACT,SAAS,GACT,OAAO,GACP,iBAAiB,GACjB,QAAQ,CAAA;AAEZ,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,EACzB,KAAK,EACL,yBAAyB,GAC1B,EAAE,uBAAuB,2CAyXzB"}
|
|
@@ -4,11 +4,11 @@ import { formatMessage } from "@voyantjs/i18n";
|
|
|
4
4
|
import { useLegalContract, useLegalContractAttachmentMutation, useLegalContractAttachments, useLegalContractMutation, useLegalContractSignatures, } from "@voyantjs/legal-react";
|
|
5
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";
|
|
8
7
|
import { ArrowLeft, CheckCircle2, ExternalLink, FileText, Pencil, Plus, Send, Trash2, } from "lucide-react";
|
|
9
8
|
import { useState } from "react";
|
|
10
9
|
import { useLegalUiI18nOrDefault, useLegalUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
10
|
import { AttachmentDialog } from "./attachment-dialog.js";
|
|
11
|
+
import { ContractSendDialog } from "./contract-send-dialog.js";
|
|
12
12
|
import { SignatureDialog } from "./signature-dialog.js";
|
|
13
13
|
const statusVariant = {
|
|
14
14
|
draft: "outline",
|
|
@@ -19,14 +19,15 @@ const statusVariant = {
|
|
|
19
19
|
expired: "destructive",
|
|
20
20
|
void: "destructive",
|
|
21
21
|
};
|
|
22
|
-
export function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, slots, }) {
|
|
22
|
+
export function ContractDetailPage({ id, onBackToContracts, renderContractDialog, renderReference, getAttachmentDownloadHref, slots, resolveSendRecipientEmail, }) {
|
|
23
23
|
const queryClient = useQueryClient();
|
|
24
24
|
const i18n = useLegalUiI18nOrDefault();
|
|
25
25
|
const messages = useLegalUiMessagesOrDefault();
|
|
26
26
|
const f = messages.contractDetailPage;
|
|
27
|
-
const { remove, issue,
|
|
27
|
+
const { remove, issue, execute, voidContract } = useLegalContractMutation();
|
|
28
28
|
const { remove: removeAttachment } = useLegalContractAttachmentMutation();
|
|
29
29
|
const [editOpen, setEditOpen] = useState(false);
|
|
30
|
+
const [sendOpen, setSendOpen] = useState(false);
|
|
30
31
|
const [signOpen, setSignOpen] = useState(false);
|
|
31
32
|
const [attachOpen, setAttachOpen] = useState(false);
|
|
32
33
|
const [editingAttachment, setEditingAttachment] = useState();
|
|
@@ -46,7 +47,7 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
|
|
|
46
47
|
const status = contract.status;
|
|
47
48
|
const canAddSignature = status === "sent" || status === "signed";
|
|
48
49
|
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 === "issued" ? (_jsxs(Button, { size: "sm", onClick: () =>
|
|
50
|
+
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 === "issued" || status === "sent" ? (_jsxs(Button, { size: "sm", onClick: () => setSendOpen(true), children: [_jsx(Send, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.send] })) : null, status === "signed" ? (_jsxs(Button, { size: "sm", onClick: () => execute.mutate(id), disabled: execute.isPending, children: [_jsx(CheckCircle2, { className: "mr-2 size-4", "aria-hidden": "true" }), f.actions.execute] })) : null, status !== "void" ? (_jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
|
|
50
51
|
if (confirm(f.voidConfirm)) {
|
|
51
52
|
voidContract.mutate(id);
|
|
52
53
|
}
|
|
@@ -54,22 +55,22 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
|
|
|
54
55
|
if (confirm(formatMessage(f.deleteConfirm, { title: contract.title }))) {
|
|
55
56
|
remove.mutate(id, { onSuccess: () => onBackToContracts?.() });
|
|
56
57
|
}
|
|
57
|
-
}, disabled: remove.isPending, children: [_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.common.delete] })) : null] })] }), _jsxs(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
}, disabled: remove.isPending, children: [_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.common.delete] })) : null] })] }), _jsxs("div", { className: "flex flex-col gap-4", children: [slots?.detailsContent !== undefined ? (slots.detailsContent) : (_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: renderReferenceValue("templateVersion", contract.templateVersionId) })) : null, contract.seriesId ? (_jsx(DetailRow, { label: f.fields.series, children: renderReferenceValue("series", 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) })] })] }) })), slots?.partiesContent !== undefined ? (slots.partiesContent) : (_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 &&
|
|
59
|
+
!contract.organizationId &&
|
|
60
|
+
!contract.supplierId &&
|
|
61
|
+
!contract.channelId &&
|
|
62
|
+
!contract.bookingId &&
|
|
63
|
+
!contract.orderId ? (_jsx("p", { className: "text-muted-foreground", children: f.empty.noParties })) : null] }) })), slots?.signaturesContent !== undefined ? (slots.signaturesContent) : (_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))) })] }) })) })), slots?.documentsContent !== undefined ? (slots.documentsContent) : (_jsx(ContractSection, { title: f.sections.documents, action: _jsxs(Button, { size: "sm", onClick: () => {
|
|
64
|
+
setEditingAttachment(undefined);
|
|
65
|
+
setAttachOpen(true);
|
|
66
|
+
}, 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, { className: "w-16" })] }) }), _jsx(TableBody, { children: attachments.map((attachment) => (_jsx(AttachmentRow, { attachment: attachment, downloadHref: getAttachmentDownloadHref?.(attachment), onEdit: () => {
|
|
67
|
+
setEditingAttachment(attachment);
|
|
68
|
+
setAttachOpen(true);
|
|
69
|
+
}, onDelete: () => {
|
|
70
|
+
if (confirm(f.deleteAttachmentConfirm)) {
|
|
71
|
+
removeAttachment.mutate({ contractId: id, id: attachment.id });
|
|
72
|
+
}
|
|
73
|
+
} }, attachment.id))) })] }) })) }))] }), renderContractDialog?.({
|
|
73
74
|
open: editOpen,
|
|
74
75
|
onOpenChange: setEditOpen,
|
|
75
76
|
contract,
|
|
@@ -85,6 +86,10 @@ export function ContractDetailPage({ id, onBackToContracts, renderContractDialog
|
|
|
85
86
|
setAttachOpen(false);
|
|
86
87
|
setEditingAttachment(undefined);
|
|
87
88
|
void refetchAttachments();
|
|
89
|
+
} }), _jsx(ContractSendDialog, { open: sendOpen, onOpenChange: setSendOpen, contract: contract, defaultRecipientEmail: resolveSendRecipientEmail?.(contract) ?? null, attachments: (attachments ?? [])
|
|
90
|
+
.filter((a) => a.kind === "document")
|
|
91
|
+
.map((a) => ({ id: a.id, name: a.name })), onSent: () => {
|
|
92
|
+
void queryClient.invalidateQueries();
|
|
88
93
|
} })] }));
|
|
89
94
|
}
|
|
90
95
|
function DetailRow({ label, children }) {
|
|
@@ -94,20 +99,5 @@ function ContractSection({ title, action, children, }) {
|
|
|
94
99
|
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
100
|
}
|
|
96
101
|
function AttachmentRow({ attachment, downloadHref, onEdit, onDelete, }) {
|
|
97
|
-
|
|
98
|
-
const f = messages.contractDetailPage;
|
|
99
|
-
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: downloadHref ? (_jsxs("a", { href: downloadHref, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 hover:underline", children: [_jsx(FileText, { className: "size-3.5 shrink-0 opacity-60", "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: attachment.name }), _jsx(ExternalLink, { className: "size-3 opacity-60", "aria-hidden": "true" })] })) : (_jsx("span", { children: attachment.name })) }), _jsx(TableCell, { children: attachment.kind }), _jsx(TableCell, { children: attachment.mimeType ?? messages.common.noResultsDash }), _jsx(TableCell, { children: attachment.fileSize != null
|
|
100
|
-
? formatBytes(attachment.fileSize, {
|
|
101
|
-
bytes: f.units.bytes,
|
|
102
|
-
kilobytes: f.units.kilobytes,
|
|
103
|
-
megabytes: f.units.megabytes,
|
|
104
|
-
})
|
|
105
|
-
: messages.common.noResultsDash }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: onEdit, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "size-3", "aria-hidden": "true" }) }), _jsx("button", { type: "button", onClick: onDelete, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "size-3", "aria-hidden": "true" }) })] }) })] }));
|
|
106
|
-
}
|
|
107
|
-
function formatBytes(bytes, units) {
|
|
108
|
-
if (bytes < 1024)
|
|
109
|
-
return `${bytes} ${units.bytes}`;
|
|
110
|
-
if (bytes < 1024 * 1024)
|
|
111
|
-
return `${Math.round(bytes / 1024)} ${units.kilobytes}`;
|
|
112
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} ${units.megabytes}`;
|
|
102
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: downloadHref ? (_jsxs("a", { href: downloadHref, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 hover:underline", children: [_jsx(FileText, { className: "size-3.5 shrink-0 opacity-60", "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: attachment.name }), _jsx(ExternalLink, { className: "size-3 opacity-60", "aria-hidden": "true" })] })) : (_jsx("span", { children: attachment.name })) }), _jsx(TableCell, { children: attachment.kind }), _jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: onEdit, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "size-3", "aria-hidden": "true" }) }), _jsx("button", { type: "button", onClick: onDelete, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "size-3", "aria-hidden": "true" }) })] }) })] }));
|
|
113
103
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type LegalContractRecord } from "@voyantjs/legal-react";
|
|
2
|
+
export interface ContractSendDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
contract: LegalContractRecord;
|
|
6
|
+
/**
|
|
7
|
+
* Recipient email — typically the linked person's primary email, resolved
|
|
8
|
+
* by the caller (the operator template knows how to look it up via CRM).
|
|
9
|
+
* The dialog uses this as the read-only "To" line; if absent the dialog
|
|
10
|
+
* shows a warning and disables Send.
|
|
11
|
+
*/
|
|
12
|
+
defaultRecipientEmail?: string | null;
|
|
13
|
+
/** Optional override for the prefilled subject. */
|
|
14
|
+
defaultSubject?: string;
|
|
15
|
+
/** Optional override for the prefilled message body. */
|
|
16
|
+
defaultMessage?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional list of attachment names that will accompany the email. The
|
|
19
|
+
* dialog renders them as a paperclip-prefixed list so the operator sees
|
|
20
|
+
* what's bundled (the contract PDF + any extras).
|
|
21
|
+
*/
|
|
22
|
+
attachments?: ReadonlyArray<{
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}>;
|
|
26
|
+
onSent?: () => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Send-contract preview dialog. Renders an editable subject + message
|
|
30
|
+
* over a read-only recipient line, so the operator can tweak the
|
|
31
|
+
* customer-facing copy before flipping the contract to `sent`. The
|
|
32
|
+
* actual `POST /:id/send` accepts the subject + message + recipient
|
|
33
|
+
* overrides and forwards them on the lifecycle event for whichever
|
|
34
|
+
* notification subscriber wires up email delivery downstream.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ContractSendDialog({ open, onOpenChange, contract, defaultRecipientEmail, defaultSubject, defaultMessage, attachments, onSent, }: ContractSendDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=contract-send-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-send-dialog.d.ts","sourceRoot":"","sources":["../../src/components/contract-send-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAA4B,MAAM,uBAAuB,CAAA;AAkB1F,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,mBAAmB,CAAA;IAC7B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mDAAmD;IACnD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,WAAW,EACX,MAAM,GACP,EAAE,uBAAuB,2CAwIzB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLegalContractMutation } from "@voyantjs/legal-react";
|
|
3
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Textarea, } from "@voyantjs/ui/components";
|
|
4
|
+
import { Loader2, Mail, Paperclip } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { useLegalUiMessagesOrDefault } from "../i18n/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Send-contract preview dialog. Renders an editable subject + message
|
|
9
|
+
* over a read-only recipient line, so the operator can tweak the
|
|
10
|
+
* customer-facing copy before flipping the contract to `sent`. The
|
|
11
|
+
* actual `POST /:id/send` accepts the subject + message + recipient
|
|
12
|
+
* overrides and forwards them on the lifecycle event for whichever
|
|
13
|
+
* notification subscriber wires up email delivery downstream.
|
|
14
|
+
*/
|
|
15
|
+
export function ContractSendDialog({ open, onOpenChange, contract, defaultRecipientEmail, defaultSubject, defaultMessage, attachments, onSent, }) {
|
|
16
|
+
const { send } = useLegalContractMutation();
|
|
17
|
+
const messages = useLegalUiMessagesOrDefault().contractSendDialog;
|
|
18
|
+
const fallbackSubject = defaultSubject ??
|
|
19
|
+
messages.fallbackSubject.replace("{number}", contract.contractNumber ?? messages.title);
|
|
20
|
+
const fallbackMessage = defaultMessage ??
|
|
21
|
+
[
|
|
22
|
+
"Hi,",
|
|
23
|
+
"",
|
|
24
|
+
`Please find attached the contract${contract.contractNumber ? ` ${contract.contractNumber}` : ""} for your booking.`,
|
|
25
|
+
"",
|
|
26
|
+
"Reply to this email if you have any questions before signing.",
|
|
27
|
+
"",
|
|
28
|
+
"Thanks,",
|
|
29
|
+
].join("\n");
|
|
30
|
+
const [subject, setSubject] = useState(fallbackSubject);
|
|
31
|
+
const [message, setMessage] = useState(fallbackMessage);
|
|
32
|
+
// Reset the form when the dialog re-opens or the underlying contract /
|
|
33
|
+
// defaults change — guarantees the operator never sees stale copy from
|
|
34
|
+
// a previous Send attempt on a different contract.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (open) {
|
|
37
|
+
setSubject(fallbackSubject);
|
|
38
|
+
setMessage(fallbackMessage);
|
|
39
|
+
}
|
|
40
|
+
}, [open, fallbackSubject, fallbackMessage]);
|
|
41
|
+
const canSend = Boolean(defaultRecipientEmail) && !send.isPending;
|
|
42
|
+
const isAlreadySent = contract.status === "sent" || contract.status === "signed";
|
|
43
|
+
const handleSend = async () => {
|
|
44
|
+
await send.mutateAsync({
|
|
45
|
+
id: contract.id,
|
|
46
|
+
input: {
|
|
47
|
+
recipientEmail: defaultRecipientEmail ?? null,
|
|
48
|
+
subject: subject.trim() || null,
|
|
49
|
+
message: message.trim() || null,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
onOpenChange(false);
|
|
53
|
+
onSent?.();
|
|
54
|
+
};
|
|
55
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.title }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [!defaultRecipientEmail ? (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive", children: messages.missingRecipient })) : null, isAlreadySent ? (_jsx("div", { role: "status", className: "rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300", children: messages.alreadySentWarning })) : null, _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.fields.to }), _jsxs("div", { className: "flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "font-mono", children: defaultRecipientEmail ?? messages.recipientPlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "contract-send-subject", children: messages.fields.subject }), _jsx(Input, { id: "contract-send-subject", value: subject, onChange: (event) => setSubject(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "contract-send-message", children: messages.fields.message }), _jsx(Textarea, { id: "contract-send-message", rows: 8, value: message, onChange: (event) => setMessage(event.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: messages.messageHint })] }), attachments && attachments.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.fields.attachments }), _jsx("ul", { className: "flex flex-col gap-1 rounded-md border bg-muted/40 px-3 py-2 text-xs", children: attachments.map((attachment) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx(Paperclip, { className: "h-3 w-3 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: attachment.name })] }, attachment.id))) })] })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), disabled: send.isPending, children: messages.actions.cancel }), _jsxs(Button, { type: "button", size: "sm", onClick: handleSend, disabled: !canSend, children: [send.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, messages.actions.send] })] })] }) }));
|
|
56
|
+
}
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,2CAgSzB"}
|
|
@@ -8,7 +8,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components
|
|
|
8
8
|
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
9
9
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
10
10
|
import { cn } from "@voyantjs/ui/lib/utils";
|
|
11
|
-
import { ChevronDown, Plus, Search, User, X } from "lucide-react";
|
|
11
|
+
import { ChevronDown, ListFilter, Plus, Search, User, X } from "lucide-react";
|
|
12
12
|
import { useMemo, useState } from "react";
|
|
13
13
|
import { useLegalUiI18nOrDefault, useLegalUiMessagesOrDefault } from "../i18n/index.js";
|
|
14
14
|
import { legalContractScopes, legalContractStatuses } from "../i18n/messages.js";
|
|
@@ -34,8 +34,11 @@ export function ContractsPage({ className, defaultPersonId, personId, onPersonId
|
|
|
34
34
|
const [internalPersonId, setInternalPersonId] = useState(() => getInitialPersonId(defaultPersonId));
|
|
35
35
|
const [selectedPerson, setSelectedPerson] = useState(null);
|
|
36
36
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
37
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
37
38
|
const [pageIndex, setPageIndex] = useState(0);
|
|
38
39
|
const resolvedPersonId = personId ?? internalPersonId;
|
|
40
|
+
const activeFilterCount = (scope !== SCOPE_ALL ? 1 : 0) + (status !== STATUS_ALL ? 1 : 0) + (resolvedPersonId ? 1 : 0);
|
|
41
|
+
const hasActiveFilters = activeFilterCount > 0 || search !== "";
|
|
39
42
|
const { data, isPending, isFetching, isError, refetch } = useLegalContracts({
|
|
40
43
|
search,
|
|
41
44
|
scope: scope === SCOPE_ALL ? "all" : scope,
|
|
@@ -69,20 +72,31 @@ export function ContractsPage({ className, defaultPersonId, personId, onPersonId
|
|
|
69
72
|
onPersonIdChange?.(nextPersonId);
|
|
70
73
|
resetPage();
|
|
71
74
|
};
|
|
72
|
-
|
|
75
|
+
const clearFilters = () => {
|
|
76
|
+
setSearch("");
|
|
77
|
+
setScope(SCOPE_ALL);
|
|
78
|
+
setStatus(STATUS_ALL);
|
|
79
|
+
handlePersonChange(null, null);
|
|
80
|
+
resetPage();
|
|
81
|
+
};
|
|
82
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), 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 })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] 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) => {
|
|
73
83
|
setSearch(event.target.value);
|
|
74
84
|
resetPage();
|
|
75
|
-
}, className: "pl-9" })] }), _jsxs(Select, { value: scope, onValueChange: (value) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
}, className: "pl-9" })] }), _jsxs(Popover, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4" }), f.filters.button, activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount }))] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "contracts-filter-scope", children: f.filters.scope }), _jsxs(Select, { value: scope, onValueChange: (value) => {
|
|
86
|
+
setScope(value ?? SCOPE_ALL);
|
|
87
|
+
resetPage();
|
|
88
|
+
}, children: [_jsx(SelectTrigger, { id: "contracts-filter-scope", className: "w-full", children: _jsx(SelectValue, { children: (value) => value === SCOPE_ALL
|
|
89
|
+
? f.filters.allScopes
|
|
90
|
+
: (messages.common.contractScopeLabels[value] ?? value) }) }), _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("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "contracts-filter-status", children: f.filters.status }), _jsxs(Select, { value: status, onValueChange: (value) => {
|
|
91
|
+
setStatus(value ?? STATUS_ALL);
|
|
92
|
+
resetPage();
|
|
93
|
+
}, children: [_jsx(SelectTrigger, { id: "contracts-filter-status", className: "w-full", children: _jsx(SelectValue, { children: (value) => value === STATUS_ALL
|
|
94
|
+
? f.filters.allStatuses
|
|
95
|
+
: (messages.common.contractStatusLabels[value] ?? value) }) }), _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)))] })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: f.filters.person }), renderPersonFilter ? (renderPersonFilter({
|
|
96
|
+
personId: resolvedPersonId || null,
|
|
97
|
+
selectedPerson: contractPersonSummary,
|
|
98
|
+
onPersonChange: handlePersonChange,
|
|
99
|
+
})) : (_jsx(ContractsPersonFilter, { value: resolvedPersonId || null, selectedPerson: contractPersonSummary, onChange: handlePersonChange }))] })] }) })] }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), f.filters.clear] })), renderContractDialog ? (_jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-1 size-4", "aria-hidden": "true" }), f.create] }) })) : null] }), _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?.({
|
|
86
100
|
open: dialogOpen,
|
|
87
101
|
onOpenChange: setDialogOpen,
|
|
88
102
|
onSuccess: () => {
|
|
@@ -139,7 +153,7 @@ function ContractsPersonFilter({ value, selectedPerson, onChange, }) {
|
|
|
139
153
|
});
|
|
140
154
|
const people = peopleQuery.data?.data ?? [];
|
|
141
155
|
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-
|
|
156
|
+
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-full flex-1 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
157
|
const summary = personSummaryFromRecord(person);
|
|
144
158
|
const name = formatPersonName(summary);
|
|
145
159
|
return (_jsx(CommandItem, { value: `${name ?? ""} ${person.email ?? ""} ${person.phone ?? ""}`, onSelect: () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policies-page.d.ts","sourceRoot":"","sources":["../../src/components/policies-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policies-page.d.ts","sourceRoot":"","sources":["../../src/components/policies-page.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAStC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,SAAS,CAAA;CACnE;AAED,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,YAAY,EACZ,kBAAkB,GACnB,GAAE,iBAAsB,2CAkMxB"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { formatMessage } from "@voyantjs/i18n";
|
|
3
3
|
import { useLegalPolicies } from "@voyantjs/legal-react";
|
|
4
|
-
import { Badge, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
4
|
+
import { Badge, Button, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
|
|
5
5
|
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
6
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
7
7
|
import { cn } from "@voyantjs/ui/lib/utils";
|
|
8
|
-
import { Plus, Search } from "lucide-react";
|
|
8
|
+
import { ListFilter, Plus, Search, X } from "lucide-react";
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { useLegalUiI18nOrDefault, useLegalUiMessagesOrDefault } from "../i18n/index.js";
|
|
11
11
|
import { legalPolicyKinds } from "../i18n/messages.js";
|
|
@@ -19,6 +19,14 @@ export function PoliciesPage({ className, onOpenPolicy, renderPolicyDialog, } =
|
|
|
19
19
|
const [kind, setKind] = useState(KIND_ALL);
|
|
20
20
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
21
21
|
const [pageIndex, setPageIndex] = useState(0);
|
|
22
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
23
|
+
const activeFilterCount = kind !== KIND_ALL ? 1 : 0;
|
|
24
|
+
const hasActiveFilters = activeFilterCount > 0 || search !== "";
|
|
25
|
+
const clearFilters = () => {
|
|
26
|
+
setSearch("");
|
|
27
|
+
setKind(KIND_ALL);
|
|
28
|
+
setPageIndex(0);
|
|
29
|
+
};
|
|
22
30
|
const { data, isPending, isFetching, isError, refetch } = useLegalPolicies({
|
|
23
31
|
search,
|
|
24
32
|
kind: kind === KIND_ALL ? "all" : kind,
|
|
@@ -31,13 +39,15 @@ export function PoliciesPage({ className, onOpenPolicy, renderPolicyDialog, } =
|
|
|
31
39
|
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
32
40
|
const showSkeleton = isPending || (isFetching && policies.length === 0);
|
|
33
41
|
const resetPage = () => setPageIndex(0);
|
|
34
|
-
return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [
|
|
42
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [_jsx("div", { children: _jsx("h1", { className: "text-2xl font-bold tracking-tight", children: f.title }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [_jsx(Label, { htmlFor: "policies-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: "policies-search", placeholder: f.searchPlaceholder, value: search, onChange: (event) => {
|
|
35
43
|
setSearch(event.target.value);
|
|
36
44
|
resetPage();
|
|
37
|
-
}, className: "pl-9" })] }), _jsxs(Select, { value: kind, onValueChange: (value) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
}, className: "pl-9" })] }), _jsxs(Popover, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4" }), f.filters.button, activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount }))] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsx("div", { className: "flex flex-col gap-4", children: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "policies-filter-kind", children: f.filters.kind }), _jsxs(Select, { value: kind, onValueChange: (value) => {
|
|
46
|
+
setKind(value ?? KIND_ALL);
|
|
47
|
+
resetPage();
|
|
48
|
+
}, children: [_jsx(SelectTrigger, { id: "policies-filter-kind", className: "w-full", children: _jsx(SelectValue, { children: (value) => value === KIND_ALL
|
|
49
|
+
? f.allKinds
|
|
50
|
+
: (messages.common.policyKindLabels[value] ?? value) }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: KIND_ALL, children: f.allKinds }), legalPolicyKinds.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.policyKindLabels[value] }, value)))] })] })] }) }) })] }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), f.filters.clear] })), renderPolicyDialog ? (_jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-1 size-4", "aria-hidden": "true" }), f.create] }) })) : null] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: f.columns.name }), _jsx(TableHead, { children: f.columns.slug }), _jsx(TableHead, { children: f.columns.kind }), _jsx(TableHead, { children: f.columns.language }), _jsx(TableHead, { children: f.columns.created })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(PolicyRowSkeleton, { rows: 6 })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-destructive", children: f.loadFailed }) })) : policies.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-muted-foreground", children: f.empty }) })) : (policies.map((policy) => (_jsx(PolicyRow, { policy: policy, created: i18n.formatDate(policy.createdAt), kindLabel: messages.common.policyKindLabels[policy.kind] ?? policy.kind, onOpenPolicy: onOpenPolicy }, policy.id)))) })] }) }), _jsx(PaginationBar, { shown: policies.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 }), renderPolicyDialog?.({
|
|
41
51
|
open: dialogOpen,
|
|
42
52
|
onOpenChange: setDialogOpen,
|
|
43
53
|
onSuccess: () => {
|
|
@@ -6,6 +6,12 @@ export interface TemplateDialogRenderProps {
|
|
|
6
6
|
template?: LegalContractTemplateRecord;
|
|
7
7
|
onSuccess: () => void;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Render-prop shape for the template version dialog. Templates' index
|
|
11
|
+
* page no longer surfaces an "Add version" action (versions are managed
|
|
12
|
+
* on the template detail page), but the type is still exported for the
|
|
13
|
+
* detail page to reuse.
|
|
14
|
+
*/
|
|
9
15
|
export interface TemplateVersionDialogRenderProps {
|
|
10
16
|
open: boolean;
|
|
11
17
|
onOpenChange: (open: boolean) => void;
|
|
@@ -16,7 +22,6 @@ export interface TemplatesPageProps {
|
|
|
16
22
|
className?: string;
|
|
17
23
|
onOpenTemplate?: (templateId: string) => void;
|
|
18
24
|
renderTemplateDialog?: (props: TemplateDialogRenderProps) => ReactNode;
|
|
19
|
-
renderTemplateVersionDialog?: (props: TemplateVersionDialogRenderProps) => ReactNode;
|
|
20
25
|
}
|
|
21
|
-
export declare function TemplatesPage({ className, onOpenTemplate, renderTemplateDialog,
|
|
26
|
+
export declare function TemplatesPage({ className, onOpenTemplate, renderTemplateDialog, }?: TemplatesPageProps): import("react/jsx-runtime").JSX.Element;
|
|
22
27
|
//# sourceMappingURL=templates-page.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates-page.d.ts","sourceRoot":"","sources":["../../src/components/templates-page.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,2BAA2B,
|
|
1
|
+
{"version":3,"file":"templates-page.d.ts","sourceRoot":"","sources":["../../src/components/templates-page.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,2BAA2B,EAGjC,MAAM,uBAAuB,CAAA;AA0B9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,2BAA2B,CAAA;IACtC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,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;CACvE;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,cAAc,EACd,oBAAoB,GACrB,GAAE,kBAAuB,2CAyNzB"}
|