@voyantjs/finance-ui 0.19.0 → 0.21.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.
- package/dist/components/payment-policy-form.d.ts +49 -0
- package/dist/components/payment-policy-form.d.ts.map +1 -0
- package/dist/components/payment-policy-form.js +82 -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 +14 -8
- package/src/styles.css +11 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PaymentPolicy } from "@voyantjs/finance";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Reusable payment-policy editor.
|
|
5
|
+
*
|
|
6
|
+
* Renders the deposit kind toggle (None / Percent / Fixed amount), the
|
|
7
|
+
* conditional deposit value input, and the three day-window controls
|
|
8
|
+
* (deposit gate, balance offset, balance grace). When `inheritable`
|
|
9
|
+
* is true (the default for non-operator-default layers), an "Inherit
|
|
10
|
+
* from parent" toggle is shown — flipping it on writes `null` and
|
|
11
|
+
* disables the rest of the form.
|
|
12
|
+
*
|
|
13
|
+
* Controlled component: parent owns the value via `value` / `onChange`.
|
|
14
|
+
* The bundled `<PaymentPolicyPreview />` reads the same value so you
|
|
15
|
+
* can render the live preview alongside the form.
|
|
16
|
+
*/
|
|
17
|
+
export interface PaymentPolicyFormProps {
|
|
18
|
+
/** Current value. `null` means "inherit from parent". */
|
|
19
|
+
value: PaymentPolicy | null;
|
|
20
|
+
onChange: (next: PaymentPolicy | null) => void;
|
|
21
|
+
/**
|
|
22
|
+
* When true, shows the "Inherit from parent" toggle. Operator-
|
|
23
|
+
* default forms set this to false (no parent to inherit from).
|
|
24
|
+
*/
|
|
25
|
+
inheritable?: boolean;
|
|
26
|
+
/** Currency used for the fixed-amount deposit input + the preview. */
|
|
27
|
+
currency?: string;
|
|
28
|
+
/** Disabled by ancestor (e.g. while the mutation is in flight). */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function PaymentPolicyForm({ value, onChange, inheritable, currency, disabled, className, }: PaymentPolicyFormProps): React.ReactElement;
|
|
33
|
+
/**
|
|
34
|
+
* Live preview of what the policy will produce on a sample booking.
|
|
35
|
+
* Pair with `<PaymentPolicyForm />` so the operator sees the
|
|
36
|
+
* deposit + balance amounts shift in real time as they tweak the
|
|
37
|
+
* fields.
|
|
38
|
+
*/
|
|
39
|
+
export interface PaymentPolicyPreviewProps {
|
|
40
|
+
policy: PaymentPolicy | null;
|
|
41
|
+
currency?: string;
|
|
42
|
+
/** Total used for the preview math. Default 1,000.00 in major units. */
|
|
43
|
+
sampleTotalCents?: number;
|
|
44
|
+
/** Days before departure to use for the preview. Default 60. */
|
|
45
|
+
sampleDaysBeforeDeparture?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function PaymentPolicyPreview({ policy, currency, sampleTotalCents, sampleDaysBeforeDeparture, className, }: PaymentPolicyPreviewProps): React.ReactElement;
|
|
49
|
+
//# sourceMappingURL=payment-policy-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-policy-form.d.ts","sourceRoot":"","sources":["../../src/components/payment-policy-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,sBAAsB;IACrC,yDAAyD;IACzD,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IAC3B,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAA;IAC9C;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AASD,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,WAAkB,EAClB,QAAgB,EAChB,QAAQ,EACR,SAAS,GACV,EAAE,sBAAsB,GAAG,KAAK,CAAC,YAAY,CAgL7C;AA8BD;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gEAAgE;IAChE,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,QAAgB,EAChB,gBAA0B,EAC1B,yBAA8B,EAC9B,SAAS,GACV,EAAE,yBAAyB,GAAG,KAAK,CAAC,YAAY,CAgDhD"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { computePaymentSchedule, noDepositPolicy, } from "@voyantjs/finance";
|
|
4
|
+
import { Input, Label, RadioGroup, RadioGroupItem, Switch } from "@voyantjs/ui/components";
|
|
5
|
+
import { cn } from "@voyantjs/ui/lib/utils";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
const DEFAULT_POLICY = {
|
|
8
|
+
deposit: { kind: "percent", percent: 50 },
|
|
9
|
+
minDaysBeforeDepartureForDeposit: 30,
|
|
10
|
+
balanceDueDaysBeforeDeparture: 30,
|
|
11
|
+
balanceDueMinDaysFromNow: 7,
|
|
12
|
+
};
|
|
13
|
+
export function PaymentPolicyForm({ value, onChange, inheritable = true, currency = "EUR", disabled, className, }) {
|
|
14
|
+
const isInheriting = value === null;
|
|
15
|
+
const policy = value ?? noDepositPolicy;
|
|
16
|
+
const setPolicyField = (key, next) => {
|
|
17
|
+
onChange({ ...(value ?? DEFAULT_POLICY), [key]: next });
|
|
18
|
+
};
|
|
19
|
+
return (_jsxs("div", { className: cn("space-y-5", className), children: [inheritable ? (_jsxs("div", { className: "flex items-start gap-3 rounded-md border bg-muted/20 p-3", children: [_jsx(Switch, { id: "payment-policy-inherit", checked: isInheriting, onCheckedChange: (checked) => {
|
|
20
|
+
if (checked) {
|
|
21
|
+
onChange(null);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
onChange(value ?? DEFAULT_POLICY);
|
|
25
|
+
}
|
|
26
|
+
}, disabled: disabled }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "payment-policy-inherit", className: "text-sm font-medium", children: "Inherit from parent" }), _jsx("p", { className: "text-muted-foreground text-xs", children: "When on, this layer falls back to the next-broader policy (operator default, category, supplier, \u2026). Switch off to set an explicit policy here." })] })] })) : null, _jsxs("fieldset", { disabled: disabled || isInheriting, className: "space-y-5 disabled:opacity-60", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { className: "text-sm font-medium", children: "Deposit" }), _jsxs(RadioGroup, { value: policy.deposit.kind, onValueChange: (kind) => {
|
|
27
|
+
if (kind === "none") {
|
|
28
|
+
setPolicyField("deposit", { kind: "none" });
|
|
29
|
+
}
|
|
30
|
+
else if (kind === "percent") {
|
|
31
|
+
setPolicyField("deposit", {
|
|
32
|
+
kind: "percent",
|
|
33
|
+
percent: policy.deposit.percent ?? 50,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (kind === "fixed_cents") {
|
|
37
|
+
setPolicyField("deposit", {
|
|
38
|
+
kind: "fixed_cents",
|
|
39
|
+
amountCents: policy.deposit.amountCents ?? 10_000,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}, className: "grid grid-cols-1 gap-2 md:grid-cols-3", children: [_jsx(DepositKindOption, { value: "none", label: "None", hint: "Customer pays the full amount up-front.", checked: policy.deposit.kind === "none" }), _jsx(DepositKindOption, { value: "percent", label: "Percent of total", hint: "e.g. 50% deposit at booking.", checked: policy.deposit.kind === "percent" }), _jsx(DepositKindOption, { value: "fixed_cents", label: "Fixed amount", hint: "A flat per-booking amount.", checked: policy.deposit.kind === "fixed_cents" })] }), policy.deposit.kind === "percent" ? (_jsx("div", { className: "grid grid-cols-1 gap-3 pt-2 md:grid-cols-2", children: _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "payment-policy-percent", children: "Deposit percent" }), _jsx(Input, { id: "payment-policy-percent", type: "number", min: 0, max: 100, step: 1, value: policy.deposit.percent ?? 50, onChange: (e) => setPolicyField("deposit", {
|
|
43
|
+
kind: "percent",
|
|
44
|
+
percent: Number(e.target.value),
|
|
45
|
+
}) }), _jsx("p", { className: "text-muted-foreground text-xs", children: "0\u2013100. Whole numbers recommended." })] }) })) : null, policy.deposit.kind === "fixed_cents" ? (_jsx("div", { className: "grid grid-cols-1 gap-3 pt-2 md:grid-cols-2", children: _jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { htmlFor: "payment-policy-fixed", children: ["Deposit amount (", currency, ")"] }), _jsx(Input, { id: "payment-policy-fixed", type: "number", min: 0, step: 0.01, value: (policy.deposit.amountCents ?? 0) / 100, onChange: (e) => setPolicyField("deposit", {
|
|
46
|
+
kind: "fixed_cents",
|
|
47
|
+
amountCents: Math.round(Number(e.target.value) * 100),
|
|
48
|
+
}) }), _jsx("p", { className: "text-muted-foreground text-xs", children: "Capped at the booking total when the booking is smaller than this amount." })] }) })) : null] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "payment-policy-min-days", children: "Minimum days before departure" }), _jsx(Input, { id: "payment-policy-min-days", type: "number", min: 0, step: 1, value: policy.minDaysBeforeDepartureForDeposit, onChange: (e) => setPolicyField("minDaysBeforeDepartureForDeposit", Number(e.target.value)) }), _jsx("p", { className: "text-muted-foreground text-xs", children: "If departure is closer than this, the booking requires the full amount up-front." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "payment-policy-balance-days", children: "Balance due days before departure" }), _jsx(Input, { id: "payment-policy-balance-days", type: "number", min: 0, step: 1, value: policy.balanceDueDaysBeforeDeparture, onChange: (e) => setPolicyField("balanceDueDaysBeforeDeparture", Number(e.target.value)) }), _jsx("p", { className: "text-muted-foreground text-xs", children: "When the balance is due." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "payment-policy-grace", children: "Balance grace days from now" }), _jsx(Input, { id: "payment-policy-grace", type: "number", min: 0, step: 1, value: policy.balanceDueMinDaysFromNow, onChange: (e) => setPolicyField("balanceDueMinDaysFromNow", Number(e.target.value)) }), _jsx("p", { className: "text-muted-foreground text-xs", children: "Floor on the balance due date so the customer always gets at least this long to pay it." })] })] })] })] }));
|
|
49
|
+
}
|
|
50
|
+
function DepositKindOption({ value, label, hint, checked, }) {
|
|
51
|
+
return (_jsxs(Label, { htmlFor: `payment-policy-deposit-${value}`, className: cn("flex cursor-pointer items-start gap-3 rounded-md border p-3", checked ? "border-primary bg-primary/5" : "bg-background"), children: [_jsx(RadioGroupItem, { id: `payment-policy-deposit-${value}`, value: value, className: "mt-1" }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-sm font-medium", children: label }), _jsx("p", { className: "text-muted-foreground text-xs", children: hint })] })] }));
|
|
52
|
+
}
|
|
53
|
+
export function PaymentPolicyPreview({ policy, currency = "EUR", sampleTotalCents = 100_000, sampleDaysBeforeDeparture = 60, className, }) {
|
|
54
|
+
const today = React.useMemo(() => new Date(), []);
|
|
55
|
+
const departureDate = React.useMemo(() => {
|
|
56
|
+
const d = new Date(today.getTime() + sampleDaysBeforeDeparture * 24 * 60 * 60 * 1000);
|
|
57
|
+
return d.toISOString().slice(0, 10);
|
|
58
|
+
}, [today, sampleDaysBeforeDeparture]);
|
|
59
|
+
const schedule = React.useMemo(() => policy
|
|
60
|
+
? computePaymentSchedule({ totalCents: sampleTotalCents, currency, departureDate, today }, policy)
|
|
61
|
+
: [], [sampleTotalCents, currency, departureDate, policy, today]);
|
|
62
|
+
if (!policy) {
|
|
63
|
+
return (_jsx("div", { className: cn("rounded-md border border-dashed bg-muted/20 p-3 text-muted-foreground text-xs", className), children: "Inheriting from parent \u2014 no preview at this layer." }));
|
|
64
|
+
}
|
|
65
|
+
return (_jsxs("div", { className: cn("space-y-2 rounded-md border bg-muted/20 p-3 text-sm", className), children: [_jsxs("div", { className: "text-muted-foreground text-xs", children: ["Sample: ", formatMoney(sampleTotalCents, currency), " booking, departure in", " ", sampleDaysBeforeDeparture, " days"] }), _jsx("ul", { className: "space-y-1", children: schedule.map((entry) => (_jsx(ScheduleRow, { entry: entry, currency: currency }, `${entry.scheduleType}-${entry.dueDate}`))) })] }));
|
|
66
|
+
}
|
|
67
|
+
function ScheduleRow({ entry, currency }) {
|
|
68
|
+
const label = entry.scheduleType === "deposit"
|
|
69
|
+
? "Deposit"
|
|
70
|
+
: entry.scheduleType === "balance"
|
|
71
|
+
? "Balance"
|
|
72
|
+
: "Full payment";
|
|
73
|
+
return (_jsxs("li", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium", children: label }), _jsx("span", { className: "font-mono text-xs", children: formatMoney(entry.amountCents, currency) }), _jsxs("span", { className: "text-muted-foreground text-xs", children: ["due ", entry.dueDate] })] }));
|
|
74
|
+
}
|
|
75
|
+
function formatMoney(cents, currency) {
|
|
76
|
+
try {
|
|
77
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { InvoiceDialog, type InvoiceDialogProps } from "./components/invoice-dialog";
|
|
2
|
+
export { PaymentPolicyForm, type PaymentPolicyFormProps, PaymentPolicyPreview, type PaymentPolicyPreviewProps, } from "./components/payment-policy-form";
|
|
2
3
|
export { SupplierPaymentDialog, type SupplierPaymentDialogProps, } from "./components/supplier-payment-dialog";
|
|
3
4
|
export { type FinanceUiMessageOverrides, type FinanceUiMessages, FinanceUiMessagesProvider, financeUiEn, financeUiMessageDefinitions, financeUiRo, getFinanceUiI18n, resolveFinanceUiMessages, useFinanceUiI18n, useFinanceUiI18nOrDefault, useFinanceUiMessages, useFinanceUiMessagesOrDefault, } from "./i18n";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AACpF,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,WAAW,EACX,2BAA2B,EAC3B,WAAW,EACX,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,QAAQ,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AACpF,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,yBAAyB,GAC/B,MAAM,kCAAkC,CAAA;AACzC,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,WAAW,EACX,2BAA2B,EAC3B,WAAW,EACX,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,QAAQ,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { InvoiceDialog } from "./components/invoice-dialog";
|
|
2
|
+
export { PaymentPolicyForm, PaymentPolicyPreview, } from "./components/payment-policy-form";
|
|
2
3
|
export { SupplierPaymentDialog, } from "./components/supplier-payment-dialog";
|
|
3
4
|
export { FinanceUiMessagesProvider, financeUiEn, financeUiMessageDefinitions, financeUiRo, getFinanceUiI18n, resolveFinanceUiMessages, useFinanceUiI18n, useFinanceUiI18nOrDefault, useFinanceUiMessages, useFinanceUiMessagesOrDefault, } from "./i18n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/finance-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"import": "./dist/index.js",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./styles.css": {
|
|
19
|
+
"default": "./src/styles.css"
|
|
20
|
+
},
|
|
18
21
|
"./i18n": {
|
|
19
22
|
"types": "./dist/i18n/index.d.ts",
|
|
20
23
|
"import": "./dist/i18n/index.js",
|
|
@@ -42,11 +45,12 @@
|
|
|
42
45
|
"react-dom": "^19.0.0",
|
|
43
46
|
"react-hook-form": "^7.60.0",
|
|
44
47
|
"zod": "^4.3.6",
|
|
45
|
-
"@voyantjs/finance
|
|
46
|
-
"@voyantjs/
|
|
48
|
+
"@voyantjs/finance": "0.21.0",
|
|
49
|
+
"@voyantjs/finance-react": "0.21.0",
|
|
50
|
+
"@voyantjs/ui": "0.21.0"
|
|
47
51
|
},
|
|
48
52
|
"dependencies": {
|
|
49
|
-
"@voyantjs/i18n": "0.
|
|
53
|
+
"@voyantjs/i18n": "0.21.0"
|
|
50
54
|
},
|
|
51
55
|
"devDependencies": {
|
|
52
56
|
"@tanstack/react-query": "^5.96.2",
|
|
@@ -59,13 +63,15 @@
|
|
|
59
63
|
"typescript": "^6.0.2",
|
|
60
64
|
"vitest": "^4.1.2",
|
|
61
65
|
"zod": "^4.3.6",
|
|
62
|
-
"@voyantjs/finance
|
|
63
|
-
"@voyantjs/
|
|
66
|
+
"@voyantjs/finance": "0.21.0",
|
|
67
|
+
"@voyantjs/finance-react": "0.21.0",
|
|
68
|
+
"@voyantjs/i18n": "0.21.0",
|
|
64
69
|
"@voyantjs/voyant-typescript-config": "0.1.0",
|
|
65
|
-
"@voyantjs/ui": "0.
|
|
70
|
+
"@voyantjs/ui": "0.21.0"
|
|
66
71
|
},
|
|
67
72
|
"files": [
|
|
68
|
-
"dist"
|
|
73
|
+
"dist",
|
|
74
|
+
"src/styles.css"
|
|
69
75
|
],
|
|
70
76
|
"publishConfig": {
|
|
71
77
|
"access": "public"
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* Tailwind v4 source-detection helper.
|
|
2
|
+
*
|
|
3
|
+
* Templates that consume this package should `@import` this CSS so Tailwind
|
|
4
|
+
* scans these component files for utility classes:
|
|
5
|
+
*
|
|
6
|
+
* @import "@voyantjs/<domain>-ui/styles.css";
|
|
7
|
+
*
|
|
8
|
+
* Without it, classes that only appear inside this package (e.g. data-attr
|
|
9
|
+
* variants on primitives) won't be compiled into the final bundle.
|
|
10
|
+
*/
|
|
11
|
+
@source "./components/**/*.{ts,tsx}";
|