@capxul/sdk-react 4.1.2 → 4.1.4
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/README.md +7 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +302 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -73,6 +73,13 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
73
73
|
and an amount the caller passed in `prefill` wins over it. Both engine hooks
|
|
74
74
|
stay module-scoped.
|
|
75
75
|
|
|
76
|
+
Payroll confirmation restores a saved run before enabling submission. An
|
|
77
|
+
uncertain run keeps its period, amounts, recipients, and SDK request key across
|
|
78
|
+
close/reopen and reload. Its fields remain fixed until Core confirms success or terminal closure.
|
|
79
|
+
Use `.Actions.recovery` and `.Actions.recoveryError` to present recovery and
|
|
80
|
+
storage errors. Group editors without `.Actions` keep their editable drafts.
|
|
81
|
+
The application still owns the surrounding sheet and error copy.
|
|
82
|
+
|
|
76
83
|
The packed npm package exposes `@capxul/sdk-react` and
|
|
77
84
|
`@capxul/sdk-react/testing`. The workspace-only `@capxul/sdk-react/headless`
|
|
78
85
|
subpath is not published.
|
package/dist/index.d.mts
CHANGED
|
@@ -946,6 +946,8 @@ interface PayrollRunSummarySlice {
|
|
|
946
946
|
readonly funds: "available" | "insufficient" | "unknown";
|
|
947
947
|
}
|
|
948
948
|
interface PayrollRunActionsSlice {
|
|
949
|
+
readonly recovery: "none" | "restoring" | "required" | "unavailable";
|
|
950
|
+
readonly recoveryError: CapxulError | null;
|
|
949
951
|
readonly submit: () => void;
|
|
950
952
|
readonly isSubmitting: boolean;
|
|
951
953
|
readonly blocked: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -86,7 +86,7 @@ async function invalidateMoneyState(queryClient, input) {
|
|
|
86
86
|
const STORAGE_PREFIX = "capxul.payment.request-key.v3";
|
|
87
87
|
const attemptReleases = /* @__PURE__ */ new Set();
|
|
88
88
|
let pagehideInstalled = false;
|
|
89
|
-
async function storageKey(operation, intent) {
|
|
89
|
+
async function storageKey$1(operation, intent) {
|
|
90
90
|
return `${STORAGE_PREFIX}:${await fingerprintPaymentIntent({
|
|
91
91
|
operation,
|
|
92
92
|
intent
|
|
@@ -145,7 +145,7 @@ async function beginPaymentRequestKey(operation, intent) {
|
|
|
145
145
|
let releaseAttemptLock;
|
|
146
146
|
let forgetPagehideRelease;
|
|
147
147
|
try {
|
|
148
|
-
const storageSlot = await storageKey(operation, intent);
|
|
148
|
+
const storageSlot = await storageKey$1(operation, intent);
|
|
149
149
|
const attemptId = `attempt_${crypto.randomUUID()}`;
|
|
150
150
|
releaseAttemptLock = await holdAttemptLock(attemptLockName(storageSlot, attemptId));
|
|
151
151
|
forgetPagehideRelease = releaseAttemptOnPagehide(releaseAttemptLock);
|
|
@@ -1443,7 +1443,7 @@ const PLAIN_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
|
1443
1443
|
function escape(field) {
|
|
1444
1444
|
return `"${(FORMULA_LEAD.test(field) && !PLAIN_NUMBER.test(field) ? `'${field}` : field).replaceAll("\"", "\"\"")}"`;
|
|
1445
1445
|
}
|
|
1446
|
-
function record(fields) {
|
|
1446
|
+
function record$1(fields) {
|
|
1447
1447
|
return fields.map(escape).join(",");
|
|
1448
1448
|
}
|
|
1449
1449
|
/**
|
|
@@ -1452,11 +1452,11 @@ function record(fields) {
|
|
|
1452
1452
|
* sign lives in `Direction`, which is the code the row carries.
|
|
1453
1453
|
*/
|
|
1454
1454
|
function buildCsv(items, actor, search) {
|
|
1455
|
-
const records = [record(COLUMNS)];
|
|
1455
|
+
const records = [record$1(COLUMNS)];
|
|
1456
1456
|
for (const item of items) {
|
|
1457
1457
|
const row = toActivityRow(item, actor);
|
|
1458
1458
|
if (!matchesSearch(row, search)) continue;
|
|
1459
|
-
records.push(record([
|
|
1459
|
+
records.push(record$1([
|
|
1460
1460
|
new Date(row.createdAt).toISOString(),
|
|
1461
1461
|
row.kind,
|
|
1462
1462
|
row.kind === "payment" ? row.paymentType : row.classification,
|
|
@@ -2648,6 +2648,180 @@ const CapxulPayroll = Object.assign(Root$1, {
|
|
|
2648
2648
|
Groups
|
|
2649
2649
|
});
|
|
2650
2650
|
//#endregion
|
|
2651
|
+
//#region src/headless/payroll/run-draft.ts
|
|
2652
|
+
function record(value) {
|
|
2653
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
|
|
2654
|
+
return value;
|
|
2655
|
+
}
|
|
2656
|
+
function identifier(value) {
|
|
2657
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
|
|
2658
|
+
return value;
|
|
2659
|
+
}
|
|
2660
|
+
function minorAmount(value) {
|
|
2661
|
+
if (typeof value !== "string" || !/^\d+$/u.test(value) || BigInt(value) <= 0n) throw Errors.invalidInput("payrollDraft", "Invalid draft amount");
|
|
2662
|
+
return value;
|
|
2663
|
+
}
|
|
2664
|
+
function decodeDraft(value, scope) {
|
|
2665
|
+
const row = record(value);
|
|
2666
|
+
const asset = record(row.asset);
|
|
2667
|
+
const input = record(row.input);
|
|
2668
|
+
const period = record(input.period);
|
|
2669
|
+
if (row.version !== 1 || row.scope !== scope || typeof asset.currency !== "string" || !/^[A-Z][A-Z0-9]{1,11}$/u.test(asset.currency) || typeof asset.decimals !== "number" || !Number.isInteger(asset.decimals) || asset.decimals < 0 || asset.decimals > 255 || typeof period.start !== "number" || !Number.isSafeInteger(period.start) || period.start <= 0 || period.end !== period.start || !Array.isArray(input.items) || input.items.length === 0) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
|
|
2670
|
+
const items = input.items.map((raw) => {
|
|
2671
|
+
const item = record(raw);
|
|
2672
|
+
const to = record(item.to);
|
|
2673
|
+
const partyId = toPartyId(identifier(item.partyId));
|
|
2674
|
+
if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
|
|
2675
|
+
const net = minorAmount(item.net);
|
|
2676
|
+
return {
|
|
2677
|
+
to: {
|
|
2678
|
+
kind: "party",
|
|
2679
|
+
partyId
|
|
2680
|
+
},
|
|
2681
|
+
partyId,
|
|
2682
|
+
gross: net,
|
|
2683
|
+
net,
|
|
2684
|
+
adjustments: []
|
|
2685
|
+
};
|
|
2686
|
+
});
|
|
2687
|
+
if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
|
|
2688
|
+
return {
|
|
2689
|
+
version: 1,
|
|
2690
|
+
scope,
|
|
2691
|
+
asset: {
|
|
2692
|
+
id: identifier(asset.id),
|
|
2693
|
+
currency: asset.currency,
|
|
2694
|
+
decimals: asset.decimals
|
|
2695
|
+
},
|
|
2696
|
+
input: {
|
|
2697
|
+
permissionId: identifier(input.permissionId),
|
|
2698
|
+
period: {
|
|
2699
|
+
start: period.start,
|
|
2700
|
+
end: period.start
|
|
2701
|
+
},
|
|
2702
|
+
items,
|
|
2703
|
+
...input.requestKey === void 0 ? {} : { requestKey: identifier(input.requestKey) }
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function runDraftError(cause) {
|
|
2708
|
+
return cause instanceof CapxulError ? cause : Errors.providerError("sdk-react", "payrollDraft", cause);
|
|
2709
|
+
}
|
|
2710
|
+
function storageKey(scope) {
|
|
2711
|
+
return `capxul.payroll.run-draft.v1:${scope}`;
|
|
2712
|
+
}
|
|
2713
|
+
function readRunDraft(scope) {
|
|
2714
|
+
try {
|
|
2715
|
+
const raw = localStorage.getItem(storageKey(scope));
|
|
2716
|
+
if (raw === null) return null;
|
|
2717
|
+
let value;
|
|
2718
|
+
try {
|
|
2719
|
+
value = JSON.parse(raw);
|
|
2720
|
+
} catch {
|
|
2721
|
+
throw Errors.invalidInput("payrollDraft", "Invalid run draft JSON");
|
|
2722
|
+
}
|
|
2723
|
+
return decodeDraft(value, scope);
|
|
2724
|
+
} catch (cause) {
|
|
2725
|
+
throw runDraftError(cause);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
function sameDraft(left, right) {
|
|
2729
|
+
const { requestKey: _leftKey, ...leftInput } = left.input;
|
|
2730
|
+
const { requestKey: _rightKey, ...rightInput } = right.input;
|
|
2731
|
+
return left.scope === right.scope && JSON.stringify(left.asset) === JSON.stringify(right.asset) && JSON.stringify(leftInput) === JSON.stringify(rightInput);
|
|
2732
|
+
}
|
|
2733
|
+
/** Hold the draft across the Core call, including component unmount. A reload releases the native lock. */
|
|
2734
|
+
async function executeRunDraft(candidate, work) {
|
|
2735
|
+
try {
|
|
2736
|
+
if (navigator.locks === void 0) throw Errors.invalidInput("payrollDraft", "Web Locks are required for run drafts");
|
|
2737
|
+
return await navigator.locks.request(storageKey(candidate.scope), { ifAvailable: true }, async (lock) => {
|
|
2738
|
+
if (lock === null) throw Errors.wrongState({
|
|
2739
|
+
method: CAPXUL_OPERATIONS.payroll.authorizeRun,
|
|
2740
|
+
currentState: "run-in-flight",
|
|
2741
|
+
validStates: ["run-idle"]
|
|
2742
|
+
});
|
|
2743
|
+
let draft = readRunDraft(candidate.scope) ?? decodeDraft(candidate, candidate.scope);
|
|
2744
|
+
if (!sameDraft(draft, candidate)) throw Errors.wrongState({
|
|
2745
|
+
method: CAPXUL_OPERATIONS.payroll.authorizeRun,
|
|
2746
|
+
currentState: "unresolved-run-draft",
|
|
2747
|
+
validStates: ["resolved-run-draft"]
|
|
2748
|
+
});
|
|
2749
|
+
localStorage.setItem(storageKey(draft.scope), JSON.stringify(draft));
|
|
2750
|
+
const clear = () => {
|
|
2751
|
+
const stored = readRunDraft(draft.scope);
|
|
2752
|
+
if (stored === null || !sameDraft(stored, draft) || stored.input.requestKey !== draft.input.requestKey) throw Errors.invalidInput("payrollDraft", "Run draft changed during submission");
|
|
2753
|
+
localStorage.removeItem(storageKey(draft.scope));
|
|
2754
|
+
};
|
|
2755
|
+
const run = await work(draft, (key) => {
|
|
2756
|
+
const next = decodeDraft({
|
|
2757
|
+
...draft,
|
|
2758
|
+
input: {
|
|
2759
|
+
...draft.input,
|
|
2760
|
+
requestKey: key
|
|
2761
|
+
}
|
|
2762
|
+
}, draft.scope);
|
|
2763
|
+
localStorage.setItem(storageKey(draft.scope), JSON.stringify(next));
|
|
2764
|
+
draft = next;
|
|
2765
|
+
}, clear);
|
|
2766
|
+
clear();
|
|
2767
|
+
return run;
|
|
2768
|
+
});
|
|
2769
|
+
} catch (cause) {
|
|
2770
|
+
throw runDraftError(cause);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
/** Persistence becomes active only in a payable composer, never in the group editor. */
|
|
2774
|
+
function useRunDraft(client, orgId, apply) {
|
|
2775
|
+
const identity = useCapxulIdentityOrNull();
|
|
2776
|
+
const ownerId = identity?.phase === "authenticated" ? identity.session.authUserId : null;
|
|
2777
|
+
const draftScope = client === null || ownerId === null || orgId === void 0 ? null : JSON.stringify([
|
|
2778
|
+
client._internal.bootstrap.convexUrl,
|
|
2779
|
+
client._internal.bootstrap.chainId,
|
|
2780
|
+
ownerId,
|
|
2781
|
+
orgId
|
|
2782
|
+
]);
|
|
2783
|
+
const currentScope = useRef(draftScope);
|
|
2784
|
+
currentScope.current = draftScope;
|
|
2785
|
+
const [active, setActive] = useState(false);
|
|
2786
|
+
const activateDraft = useCallback(() => setActive(true), []);
|
|
2787
|
+
const [restoredScope, setRestoredScope] = useState(null);
|
|
2788
|
+
const [stored, setDraft] = useState(null);
|
|
2789
|
+
const [draftError, setDraftError] = useState(null);
|
|
2790
|
+
const draft = active && stored?.scope === draftScope ? stored : null;
|
|
2791
|
+
const restorationPending = active && (draftScope === null || restoredScope !== draftScope);
|
|
2792
|
+
const restoreDraft = useCallback((scope) => {
|
|
2793
|
+
try {
|
|
2794
|
+
const restored = readRunDraft(scope);
|
|
2795
|
+
setDraft(restored);
|
|
2796
|
+
setDraftError(null);
|
|
2797
|
+
if (restored !== null) apply(restored);
|
|
2798
|
+
} catch (cause) {
|
|
2799
|
+
setDraft(null);
|
|
2800
|
+
setDraftError(runDraftError(cause));
|
|
2801
|
+
}
|
|
2802
|
+
setRestoredScope(scope);
|
|
2803
|
+
}, [apply]);
|
|
2804
|
+
useEffect(() => {
|
|
2805
|
+
if (active && draftScope !== null) restoreDraft(draftScope);
|
|
2806
|
+
}, [
|
|
2807
|
+
active,
|
|
2808
|
+
draftScope,
|
|
2809
|
+
restoreDraft
|
|
2810
|
+
]);
|
|
2811
|
+
return {
|
|
2812
|
+
draftScope,
|
|
2813
|
+
currentScope,
|
|
2814
|
+
draft,
|
|
2815
|
+
draftError,
|
|
2816
|
+
restorationPending,
|
|
2817
|
+
activateDraft,
|
|
2818
|
+
restoreDraft,
|
|
2819
|
+
setDraft,
|
|
2820
|
+
recovery: !active || restorationPending ? "restoring" : draftError !== null ? "unavailable" : draft !== null ? "required" : "none",
|
|
2821
|
+
draftBlocked: !active || restorationPending || draftError !== null
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
//#endregion
|
|
2651
2825
|
//#region src/headless/payroll/terms-prefill.ts
|
|
2652
2826
|
/**
|
|
2653
2827
|
* Is this period exactly one UTC calendar month, CLOSED at both ends?
|
|
@@ -2757,13 +2931,15 @@ function amountText(amounts, seeds, recipientId) {
|
|
|
2757
2931
|
* employer's in-progress edits with it. An absent `prefill` and one that seeds
|
|
2758
2932
|
* nothing open the same empty composer, so they share a key on purpose.
|
|
2759
2933
|
*/
|
|
2760
|
-
function composerIdentity(orgId, prefill) {
|
|
2934
|
+
function composerIdentity(orgId, prefill, scope, frozen) {
|
|
2935
|
+
if (frozen) prefill = void 0;
|
|
2761
2936
|
const seeds = Object.entries(initialAmounts(prefill)).map(([recipientId, draft]) => `${recipientId}=${draft.value}`).sort();
|
|
2762
2937
|
return [
|
|
2763
2938
|
orgId ?? "",
|
|
2764
2939
|
...prefill?.recipientIds ?? [],
|
|
2765
2940
|
"|",
|
|
2766
|
-
...seeds
|
|
2941
|
+
...seeds,
|
|
2942
|
+
scope ?? "pending"
|
|
2767
2943
|
].join("\0");
|
|
2768
2944
|
}
|
|
2769
2945
|
/** Seeded from `prefill` on first render; the employer owns it after that. */
|
|
@@ -2779,6 +2955,7 @@ function usePayrollRun(input) {
|
|
|
2779
2955
|
const { orgId, prefill, onRun, onFailed } = input;
|
|
2780
2956
|
const client = useCapxulClientOrNull();
|
|
2781
2957
|
const queryClient = useQueryClient();
|
|
2958
|
+
const submitGate = useRef(false);
|
|
2782
2959
|
const signerStatus = useSignerStatus(client);
|
|
2783
2960
|
const roster = useOrgRoster(orgId);
|
|
2784
2961
|
const groups = usePayrollGroups(orgId);
|
|
@@ -2787,24 +2964,55 @@ function usePayrollRun(input) {
|
|
|
2787
2964
|
const terms = useOrgTerms(orgId);
|
|
2788
2965
|
const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
|
|
2789
2966
|
const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
|
|
2790
|
-
const
|
|
2967
|
+
const restoreCurrency = treasury.data?.available.currency;
|
|
2968
|
+
const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
|
|
2969
|
+
setSelected(restored.input.items.map((item) => item.partyId));
|
|
2970
|
+
if (restoreCurrency !== void 0 && restoreCurrency === restored.asset.currency) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
|
|
2971
|
+
value: fromMinorUnits(BigInt(item.net), {
|
|
2972
|
+
currency: restoreCurrency,
|
|
2973
|
+
decimals: restored.asset.decimals
|
|
2974
|
+
}).value,
|
|
2975
|
+
prefilled: false
|
|
2976
|
+
}])));
|
|
2977
|
+
}, [restoreCurrency]));
|
|
2978
|
+
const frozen = draft !== null;
|
|
2979
|
+
const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
|
|
2791
2980
|
const [seededKey, setSeededKey] = useState(composerKey);
|
|
2792
2981
|
if (seededKey !== composerKey) {
|
|
2793
2982
|
setSeededKey(composerKey);
|
|
2794
|
-
|
|
2795
|
-
|
|
2983
|
+
if (!frozen) {
|
|
2984
|
+
setSelected(prefill?.recipientIds ?? []);
|
|
2985
|
+
setAmounts(initialAmounts(prefill));
|
|
2986
|
+
}
|
|
2796
2987
|
}
|
|
2797
|
-
const toggle = useCallback((id) =>
|
|
2798
|
-
|
|
2988
|
+
const toggle = useCallback((id) => {
|
|
2989
|
+
if (submitGate.current || frozen || restorationPending || draftError !== null) return;
|
|
2990
|
+
setSelected((current) => current.includes(id) ? current.filter((held) => held !== id) : [...current, id]);
|
|
2991
|
+
}, [
|
|
2992
|
+
frozen,
|
|
2993
|
+
restorationPending,
|
|
2994
|
+
draftError
|
|
2995
|
+
]);
|
|
2996
|
+
const remove = useCallback((id) => {
|
|
2997
|
+
if (!submitGate.current && !frozen && !restorationPending && draftError === null) setSelected((current) => current.filter((held) => held !== id));
|
|
2998
|
+
}, [
|
|
2999
|
+
frozen,
|
|
3000
|
+
restorationPending,
|
|
3001
|
+
draftError
|
|
3002
|
+
]);
|
|
2799
3003
|
const change = useCallback((id, text) => {
|
|
2800
|
-
setAmounts((current) => ({
|
|
3004
|
+
if (!submitGate.current && !frozen && !restorationPending && draftError === null) setAmounts((current) => ({
|
|
2801
3005
|
...current,
|
|
2802
3006
|
[id]: {
|
|
2803
3007
|
value: text,
|
|
2804
3008
|
prefilled: false
|
|
2805
3009
|
}
|
|
2806
3010
|
}));
|
|
2807
|
-
}, [
|
|
3011
|
+
}, [
|
|
3012
|
+
frozen,
|
|
3013
|
+
restorationPending,
|
|
3014
|
+
draftError
|
|
3015
|
+
]);
|
|
2808
3016
|
const account = treasury.data ?? null;
|
|
2809
3017
|
const assetOptions = useMemo(() => account === null ? [] : [{
|
|
2810
3018
|
id: account.id,
|
|
@@ -2817,8 +3025,8 @@ function usePayrollRun(input) {
|
|
|
2817
3025
|
}, [assetOptions, selectedAssetId]);
|
|
2818
3026
|
const selectedAsset = assetOptions.find((option) => option.id === selectedAssetId) ?? null;
|
|
2819
3027
|
const selectAsset = useCallback((id) => {
|
|
2820
|
-
setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
|
|
2821
|
-
}, [assetOptions]);
|
|
3028
|
+
if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
|
|
3029
|
+
}, [assetOptions, frozen]);
|
|
2822
3030
|
const assetMoney = selectedAsset !== null && account !== null ? account.available : null;
|
|
2823
3031
|
const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
|
|
2824
3032
|
const refs = roster.refs;
|
|
@@ -2877,34 +3085,34 @@ function usePayrollRun(input) {
|
|
|
2877
3085
|
const budgets = me.data?.budgets ?? [];
|
|
2878
3086
|
const blockedReason = firstFalseFact({
|
|
2879
3087
|
signerReady: signerStatus === "ready",
|
|
2880
|
-
|
|
2881
|
-
hasAsset: selectedAsset !== null,
|
|
3088
|
+
...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
|
|
2882
3089
|
recipientCount: payableRefs === null ? 0 : selected.length,
|
|
2883
|
-
amountsParse: total !== null
|
|
2884
|
-
funds
|
|
3090
|
+
amountsParse: total !== null
|
|
2885
3091
|
});
|
|
2886
3092
|
const authorize = useMutation({
|
|
2887
|
-
mutationFn: async (
|
|
2888
|
-
|
|
2889
|
-
return
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3093
|
+
mutationFn: async (candidate) => {
|
|
3094
|
+
if (candidate.scope !== currentScope.current) throw Errors.notAuthenticated();
|
|
3095
|
+
return executeRunDraft(candidate, async (persisted, onRequestKey, onResolved) => {
|
|
3096
|
+
if (candidate.scope === currentScope.current) setDraft(persisted);
|
|
3097
|
+
return unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.authorizeRun, orgId).payroll.authorizeRun(persisted.input, {
|
|
3098
|
+
onResolved,
|
|
3099
|
+
onRequestKey: (key) => {
|
|
3100
|
+
if (candidate.scope !== currentScope.current) throw Errors.notAuthenticated();
|
|
3101
|
+
onRequestKey(key);
|
|
3102
|
+
}
|
|
3103
|
+
}));
|
|
3104
|
+
});
|
|
2897
3105
|
},
|
|
2898
|
-
onSettled: () => {
|
|
3106
|
+
onSettled: (_run, _error, candidate) => {
|
|
2899
3107
|
queryClient.invalidateQueries({ queryKey: capxulKeys.payrollRuns(orgId) });
|
|
3108
|
+
if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
|
|
2900
3109
|
}
|
|
2901
3110
|
});
|
|
2902
|
-
const submitGate = useRef(false);
|
|
2903
3111
|
const totalDisplay = total === null ? null : formatMoney(total, GRAMMAR);
|
|
2904
3112
|
const authorizeMutate = authorize.mutate;
|
|
2905
3113
|
const submit = useCallback(() => {
|
|
2906
3114
|
const permissionId = budgets[0]?.id;
|
|
2907
|
-
if (blockedReason !== null || submitGate.current) return;
|
|
3115
|
+
if (blockedReason !== null || submitGate.current || draftBlocked || draftScope === null || assetMoney === null || selectedAsset === null) return;
|
|
2908
3116
|
if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
|
|
2909
3117
|
if (payableRefs === null) return;
|
|
2910
3118
|
const recipientCount = selected.length;
|
|
@@ -2915,30 +3123,60 @@ function usePayrollRun(input) {
|
|
|
2915
3123
|
if (to === void 0 || parsed === void 0) return;
|
|
2916
3124
|
const raw = toMinorUnits(parsed).toString();
|
|
2917
3125
|
items.push({
|
|
2918
|
-
to
|
|
3126
|
+
to: {
|
|
3127
|
+
kind: "party",
|
|
3128
|
+
partyId: toPartyId(recipientId)
|
|
3129
|
+
},
|
|
2919
3130
|
partyId: recipientId,
|
|
2920
3131
|
gross: raw,
|
|
2921
3132
|
net: raw,
|
|
2922
3133
|
adjustments: []
|
|
2923
3134
|
});
|
|
2924
3135
|
}
|
|
3136
|
+
const runAt = Date.now();
|
|
3137
|
+
const candidate = frozen ? draft : {
|
|
3138
|
+
version: 1,
|
|
3139
|
+
scope: draftScope,
|
|
3140
|
+
asset: {
|
|
3141
|
+
id: selectedAsset.id,
|
|
3142
|
+
currency: assetMoney.currency,
|
|
3143
|
+
decimals: assetMoney.decimals
|
|
3144
|
+
},
|
|
3145
|
+
input: {
|
|
3146
|
+
permissionId,
|
|
3147
|
+
period: {
|
|
3148
|
+
start: runAt,
|
|
3149
|
+
end: runAt
|
|
3150
|
+
},
|
|
3151
|
+
items
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
2925
3154
|
submitGate.current = true;
|
|
2926
|
-
authorizeMutate({
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3155
|
+
authorizeMutate(candidate, {
|
|
3156
|
+
onSuccess: (run) => {
|
|
3157
|
+
if (candidate.scope === currentScope.current) onRun({
|
|
3158
|
+
id: run.id,
|
|
3159
|
+
totalDisplay,
|
|
3160
|
+
recipientCount
|
|
3161
|
+
});
|
|
3162
|
+
},
|
|
3163
|
+
onError: (error) => {
|
|
3164
|
+
if (candidate.scope === currentScope.current) onFailed(error);
|
|
3165
|
+
},
|
|
2936
3166
|
onSettled: () => {
|
|
2937
3167
|
submitGate.current = false;
|
|
2938
3168
|
}
|
|
2939
3169
|
});
|
|
2940
3170
|
}, [
|
|
2941
3171
|
authorizeMutate,
|
|
3172
|
+
draft,
|
|
3173
|
+
frozen,
|
|
3174
|
+
draftScope,
|
|
3175
|
+
restorationPending,
|
|
3176
|
+
draftError,
|
|
3177
|
+
draftBlocked,
|
|
3178
|
+
assetMoney,
|
|
3179
|
+
selectedAsset,
|
|
2942
3180
|
blockedReason,
|
|
2943
3181
|
budgets,
|
|
2944
3182
|
onFailed,
|
|
@@ -2949,6 +3187,7 @@ function usePayrollRun(input) {
|
|
|
2949
3187
|
totalDisplay
|
|
2950
3188
|
]);
|
|
2951
3189
|
return {
|
|
3190
|
+
activateDraft,
|
|
2952
3191
|
recipients: {
|
|
2953
3192
|
options: roster.options,
|
|
2954
3193
|
groups: (groups.data ?? []).map((group) => ({
|
|
@@ -2974,17 +3213,30 @@ function usePayrollRun(input) {
|
|
|
2974
3213
|
summary: {
|
|
2975
3214
|
recipientCount: selected.length,
|
|
2976
3215
|
totalDisplay,
|
|
2977
|
-
balanceAfterDisplay:
|
|
3216
|
+
balanceAfterDisplay: remainingBalanceDisplay(availableRaw, totalRaw, assetMoney),
|
|
2978
3217
|
funds
|
|
2979
3218
|
},
|
|
2980
3219
|
actions: {
|
|
2981
3220
|
submit,
|
|
2982
3221
|
isSubmitting: authorize.isPending,
|
|
2983
|
-
blocked: blockedReason !== null,
|
|
2984
|
-
blockedReason
|
|
3222
|
+
blocked: blockedReason !== null || draftBlocked,
|
|
3223
|
+
blockedReason,
|
|
3224
|
+
recovery,
|
|
3225
|
+
recoveryError: draftError
|
|
2985
3226
|
}
|
|
2986
3227
|
};
|
|
2987
3228
|
}
|
|
3229
|
+
function remainingBalanceDisplay(available, total, asset) {
|
|
3230
|
+
return available === null || total === null || asset === null ? null : formatMoney(fromMinorUnits(available - total, asset), GRAMMAR);
|
|
3231
|
+
}
|
|
3232
|
+
/** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
|
|
3233
|
+
function runAuthorityFacts(draft, budgets, asset, money, funds) {
|
|
3234
|
+
return {
|
|
3235
|
+
budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
|
|
3236
|
+
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset.currency === money?.currency && draft.asset.decimals === money?.decimals),
|
|
3237
|
+
funds: draft?.input.requestKey === void 0 ? funds : "available"
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
2988
3240
|
/**
|
|
2989
3241
|
* The seven facts, in the ONE order the contract fixes. Exactly one code comes
|
|
2990
3242
|
* out, and it is always the first thing that is false — so two problems never
|
|
@@ -3032,7 +3284,9 @@ function Summary({ children }) {
|
|
|
3032
3284
|
return /* @__PURE__ */ jsx(Fragment, { children: children(usePayrollRunContext().summary) });
|
|
3033
3285
|
}
|
|
3034
3286
|
function Actions({ children }) {
|
|
3035
|
-
|
|
3287
|
+
const engine = usePayrollRunContext();
|
|
3288
|
+
useEffect(engine.activateDraft, [engine.activateDraft]);
|
|
3289
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
|
|
3036
3290
|
}
|
|
3037
3291
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
3038
3292
|
const CapxulPayrollRun = Object.assign(Root, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capxul/sdk-react",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/Xelmar-tech/infrastructure.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@capxul/sdk": "4.1.
|
|
29
|
+
"@capxul/sdk": "4.1.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@tanstack/react-query": "^5.66.9",
|