@capxul/sdk-react 4.1.2 → 4.1.3

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 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 composerKey = composerIdentity(orgId, prefill);
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
- setSelected(prefill?.recipientIds ?? []);
2795
- setAmounts(initialAmounts(prefill));
2983
+ if (!frozen) {
2984
+ setSelected(prefill?.recipientIds ?? []);
2985
+ setAmounts(initialAmounts(prefill));
2986
+ }
2796
2987
  }
2797
- const toggle = useCallback((id) => setSelected((current) => current.includes(id) ? current.filter((held) => held !== id) : [...current, id]), []);
2798
- const remove = useCallback((id) => setSelected((current) => current.filter((held) => held !== id)), []);
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
- budgetCount: budgets.length,
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 (command) => {
2888
- const runAt = Date.now();
2889
- return unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.authorizeRun, orgId).payroll.authorizeRun({
2890
- permissionId: command.permissionId,
2891
- period: {
2892
- start: runAt,
2893
- end: runAt
2894
- },
2895
- items: command.items
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
- permissionId,
2928
- items
2929
- }, {
2930
- onSuccess: (run) => onRun({
2931
- id: run.id,
2932
- totalDisplay,
2933
- recipientCount
2934
- }),
2935
- onError: onFailed,
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: totalRaw === null || availableRaw === null || assetMoney === null ? null : formatMoney(fromMinorUnits(availableRaw - totalRaw, assetMoney), GRAMMAR),
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
- return /* @__PURE__ */ jsx(Fragment, { children: children(usePayrollRunContext().actions) });
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.2",
3
+ "version": "4.1.3",
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.2"
29
+ "@capxul/sdk": "4.1.3"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@tanstack/react-query": "^5.66.9",