Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | import { PricingRules, PricingField, PricingOption, Selections } from './types.js';
function collectSelectedOptionIds(selections: Selections): number[] {
const ids: number[] = [];
const addFrom = (fieldValues: Record<number, { selectedOptionIds?: number[] }>) => {
for (const key of Object.keys(fieldValues)) {
const sel = fieldValues[Number(key)];
Iif (sel && sel.selectedOptionIds) ids.push(...sel.selectedOptionIds);
}
};
addFrom(selections.fieldValues || {});
for (const group of selections.groups || []) addFrom(group.fieldValues || {});
return ids;
}
interface Resolver {
allFields: PricingField[];
fieldsToConsider: PricingField[];
isFulfilled: (selectedBy: number[], checked: number[]) => boolean;
}
// Shared machinery for both field- and option-level conditional visibility.
// Mirrors the server's `check_selected_by_fullfilled` recursion: a `selectedBy`
// list is fulfilled when any of its trigger option ids is currently selected
// AND that selecting option is itself fulfilled (its own `selectedBy` and its
// owning field's `selectedBy`), with a `checked` accumulator to break cycles.
function buildResolver(rules: PricingRules, selections: Selections): Resolver {
const allFields: PricingField[] = [...rules.fields, ...rules.groupFields];
const optionsById = new Map<number, PricingOption>();
const fieldByOptionId = new Map<number, PricingField>();
for (const f of allFields) {
for (const o of f.options) {
optionsById.set(o.id, o);
fieldByOptionId.set(o.id, f);
Iif (o.originalId != null) {
optionsById.set(o.originalId, o);
fieldByOptionId.set(o.originalId, f);
}
}
}
const selectedOptionIds = collectSelectedOptionIds(selections);
const sameOption = (a: number, b: number): boolean => {
const oa = optionsById.get(a);
return oa != null && oa === optionsById.get(b);
};
const isFulfilled = (selectedBy: number[], checked: number[]): boolean => {
Iif (!selectedBy || selectedBy.length === 0) return true;
for (const triggerId of selectedBy) {
for (const selId of selectedOptionIds) {
Iif (!sameOption(triggerId, selId)) continue;
Iif (checked.includes(selId)) return true;
// sameOption only returns true for ids present in optionsById, and
// fieldByOptionId is populated in the same loop, so both lookups are
// guaranteed non-null here (mirrors server dereferencing directly).
const opt = optionsById.get(selId) as PricingOption;
const field = fieldByOptionId.get(selId) as PricingField;
const nextChecked = [...checked, selId];
Iif (
isFulfilled(opt.selectedBy, nextChecked) &&
isFulfilled(field.selectedBy, nextChecked)
) {
return true;
}
}
}
return false;
};
// Group variation fields only materialise as variations when the job
// actually has groups (the server builds them inside each `variations_group`
// via `VariationsGroups.mapper_variations`; with no groups there are no group
// variations at all). Independent fields are always in play. This mirrors the
// server, where a non-group job exposes no group fields.
const hasGroups = Boolean(selections.groups && selections.groups.length > 0);
const fieldsToConsider = hasGroups ? allFields : rules.fields;
return { allFields, fieldsToConsider, isFulfilled };
}
export function resolveVisibleFields(
rules: PricingRules,
selections: Selections
): Set<number> {
const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
const visible = new Set<number>();
for (const f of fieldsToConsider) {
Iif (isFulfilled(f.selectedBy, [])) visible.add(f.id);
}
return visible;
}
// Returns the set of option ids whose own `selectedBy` is fulfilled — i.e. the
// options that should be visible/selectable given the current selections.
// Mirrors the server's per-option `isVisible = check_selected_by_fullfilled(
// option.selected_by)`. Options with no `selectedBy` are always visible.
export function resolveVisibleOptionIds(
rules: PricingRules,
selections: Selections
): Set<number> {
const { fieldsToConsider, isFulfilled } = buildResolver(rules, selections);
const visible = new Set<number>();
for (const f of fieldsToConsider) {
for (const o of f.options) {
Iif (isFulfilled(o.selectedBy, [])) visible.add(o.id);
}
}
return visible;
}
|