@bygd/nc-report-ui 0.1.40 → 0.1.41
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/app/esm/index.js +309 -227
- package/dist/default/cjs/index.cjs +308 -226
- package/dist/default/esm/index.js +308 -226
- package/package.json +1 -1
|
@@ -1574,6 +1574,39 @@ const ReportingProvider = ({
|
|
|
1574
1574
|
}, children);
|
|
1575
1575
|
};
|
|
1576
1576
|
|
|
1577
|
+
/**
|
|
1578
|
+
* useReportingContext - Hook to access reporting context
|
|
1579
|
+
*
|
|
1580
|
+
* @returns {Object} Context value with parameters, api, and setter functions
|
|
1581
|
+
* @returns {Object} return.parameters - Current parameters object
|
|
1582
|
+
* @returns {Function} return.setParameters - Function to update parameters
|
|
1583
|
+
* @returns {Object} return.api - Current API configuration
|
|
1584
|
+
* @returns {Function} return.setApi - Function to update API configuration
|
|
1585
|
+
* @returns {Function} return.setReportingContext - Function to update both parameters and api
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* const { parameters, setParameters, api, setApi, setReportingContext } = useReportingContext();
|
|
1589
|
+
*
|
|
1590
|
+
* // Update parameters only
|
|
1591
|
+
* setParameters({ base_currency: 'USD', merchants: [...] });
|
|
1592
|
+
*
|
|
1593
|
+
* // Update API only
|
|
1594
|
+
* setApi({ token: 'new-token', base_url: 'https://api.example.com' });
|
|
1595
|
+
*
|
|
1596
|
+
* // Update both at once
|
|
1597
|
+
* setReportingContext({
|
|
1598
|
+
* parameters: { base_currency: 'EUR' },
|
|
1599
|
+
* api: { token: 'token', base_url: 'https://api.example.com' }
|
|
1600
|
+
* });
|
|
1601
|
+
*/
|
|
1602
|
+
const useReportingContext = () => {
|
|
1603
|
+
const context = useContext(ReportingContext);
|
|
1604
|
+
if (context === undefined) {
|
|
1605
|
+
throw new Error('useReportingContext must be used within a ReportingProvider');
|
|
1606
|
+
}
|
|
1607
|
+
return context;
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1577
1610
|
/**
|
|
1578
1611
|
* useReportingContextOptional - Hook to optionally access reporting context
|
|
1579
1612
|
* Returns null if used outside of ReportingProvider (useful for optional fallback)
|
|
@@ -2817,7 +2850,6 @@ const SortableChip$1 = ({
|
|
|
2817
2850
|
// the top-of-page Add form, so it stays visible regardless of scroll position.
|
|
2818
2851
|
const SortableComputedChip = ({
|
|
2819
2852
|
id,
|
|
2820
|
-
index,
|
|
2821
2853
|
name,
|
|
2822
2854
|
value,
|
|
2823
2855
|
onDelete,
|
|
@@ -2870,13 +2902,13 @@ const SortableComputedChip = ({
|
|
|
2870
2902
|
setEditValue(value);
|
|
2871
2903
|
setIsEditing(true);
|
|
2872
2904
|
};
|
|
2873
|
-
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(),
|
|
2905
|
+
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
|
|
2874
2906
|
const handleSave = () => {
|
|
2875
2907
|
const trimmedName = editName.trim();
|
|
2876
2908
|
const trimmedValue = editValue.trim();
|
|
2877
2909
|
if (!trimmedName || !trimmedValue) return;
|
|
2878
|
-
if (isNameTaken(trimmedName,
|
|
2879
|
-
onUpdate(
|
|
2910
|
+
if (isNameTaken(trimmedName, name)) return;
|
|
2911
|
+
onUpdate(name, {
|
|
2880
2912
|
name: trimmedName,
|
|
2881
2913
|
value: trimmedValue
|
|
2882
2914
|
});
|
|
@@ -3074,7 +3106,7 @@ const Dimensions = ({
|
|
|
3074
3106
|
savedDimensions = [],
|
|
3075
3107
|
onSaveDimension,
|
|
3076
3108
|
onRemoveDimension,
|
|
3077
|
-
|
|
3109
|
+
onUpdateDimensionSortOrder,
|
|
3078
3110
|
titleOverrides = {},
|
|
3079
3111
|
onUpdateTitle,
|
|
3080
3112
|
onResetTitle,
|
|
@@ -3084,7 +3116,8 @@ const Dimensions = ({
|
|
|
3084
3116
|
onSaveComputedDimension,
|
|
3085
3117
|
onUpdateComputedDimension,
|
|
3086
3118
|
onRemoveComputedDimension,
|
|
3087
|
-
|
|
3119
|
+
dimensionOrder = [],
|
|
3120
|
+
onReorderDimensionItems
|
|
3088
3121
|
}) => {
|
|
3089
3122
|
const reportingContext = useReportingContextOptional();
|
|
3090
3123
|
const parameters = reportingContext?.parameters;
|
|
@@ -3100,49 +3133,56 @@ const Dimensions = ({
|
|
|
3100
3133
|
coordinateGetter: sortableKeyboardCoordinates
|
|
3101
3134
|
}));
|
|
3102
3135
|
|
|
3103
|
-
//
|
|
3136
|
+
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3137
|
+
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3138
|
+
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3139
|
+
const dimensionByFullPath = Object.fromEntries(savedDimensions.map(d => [d.fullPath, d]));
|
|
3140
|
+
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3141
|
+
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3142
|
+
kind: "dimension",
|
|
3143
|
+
key: entry.key,
|
|
3144
|
+
data: dimensionByFullPath[entry.key]
|
|
3145
|
+
} : {
|
|
3146
|
+
kind: "computed",
|
|
3147
|
+
key: entry.key,
|
|
3148
|
+
data: computedDimensionByName[entry.key]
|
|
3149
|
+
}).filter(item => item.data);
|
|
3150
|
+
|
|
3151
|
+
// Handle drag end for the unified list
|
|
3104
3152
|
const handleDragEnd = event => {
|
|
3105
3153
|
const {
|
|
3106
3154
|
active,
|
|
3107
3155
|
over
|
|
3108
3156
|
} = event;
|
|
3109
3157
|
if (over && active.id !== over.id) {
|
|
3110
|
-
const oldIndex =
|
|
3111
|
-
const newIndex =
|
|
3112
|
-
|
|
3113
|
-
onReorderDimensions(newOrder);
|
|
3158
|
+
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3159
|
+
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3160
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
3114
3161
|
}
|
|
3115
3162
|
};
|
|
3116
3163
|
|
|
3117
3164
|
// Handle move up
|
|
3118
3165
|
const handleMoveUp = index => {
|
|
3119
3166
|
if (index > 0) {
|
|
3120
|
-
|
|
3121
|
-
onReorderDimensions(newOrder);
|
|
3167
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
|
|
3122
3168
|
}
|
|
3123
3169
|
};
|
|
3124
3170
|
|
|
3125
3171
|
// Handle move down
|
|
3126
3172
|
const handleMoveDown = index => {
|
|
3127
|
-
if (index <
|
|
3128
|
-
|
|
3129
|
-
onReorderDimensions(newOrder);
|
|
3173
|
+
if (index < dimensionOrder.length - 1) {
|
|
3174
|
+
onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
|
|
3130
3175
|
}
|
|
3131
3176
|
};
|
|
3132
3177
|
|
|
3133
|
-
// Handle sort order change
|
|
3134
|
-
const handleSortOrderChange = (
|
|
3135
|
-
|
|
3136
|
-
newDimensions[index] = {
|
|
3137
|
-
...newDimensions[index],
|
|
3138
|
-
sortOrder
|
|
3139
|
-
};
|
|
3140
|
-
onReorderDimensions(newDimensions);
|
|
3178
|
+
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3179
|
+
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
3180
|
+
onUpdateDimensionSortOrder(fullPath, sortOrder);
|
|
3141
3181
|
};
|
|
3142
3182
|
|
|
3143
3183
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
3144
3184
|
// Their `value` is a raw SQL literal/expression sent to the API as-is.
|
|
3145
|
-
const isComputedNameTaken = (name,
|
|
3185
|
+
const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
|
|
3146
3186
|
const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
|
|
3147
3187
|
const handleAddComputedClick = () => {
|
|
3148
3188
|
setIsAddingComputed(true);
|
|
@@ -3166,27 +3206,6 @@ const Dimensions = ({
|
|
|
3166
3206
|
});
|
|
3167
3207
|
handleCancelComputed();
|
|
3168
3208
|
};
|
|
3169
|
-
const handleComputedDragEnd = event => {
|
|
3170
|
-
const {
|
|
3171
|
-
active,
|
|
3172
|
-
over
|
|
3173
|
-
} = event;
|
|
3174
|
-
if (over && active.id !== over.id) {
|
|
3175
|
-
const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
|
|
3176
|
-
const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
|
|
3177
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, oldIndex, newIndex));
|
|
3178
|
-
}
|
|
3179
|
-
};
|
|
3180
|
-
const handleMoveComputedUp = index => {
|
|
3181
|
-
if (index > 0) {
|
|
3182
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index - 1));
|
|
3183
|
-
}
|
|
3184
|
-
};
|
|
3185
|
-
const handleMoveComputedDown = index => {
|
|
3186
|
-
if (index < savedComputedDimensions.length - 1) {
|
|
3187
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index + 1));
|
|
3188
|
-
}
|
|
3189
|
-
};
|
|
3190
3209
|
|
|
3191
3210
|
// Get the current provider based on selection chain
|
|
3192
3211
|
const getCurrentProvider = () => {
|
|
@@ -3629,7 +3648,7 @@ const Dimensions = ({
|
|
|
3629
3648
|
boxShadow: "none"
|
|
3630
3649
|
}
|
|
3631
3650
|
}
|
|
3632
|
-
}, "Save Dimension"))),
|
|
3651
|
+
}, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3633
3652
|
sx: {
|
|
3634
3653
|
marginTop: 0
|
|
3635
3654
|
}
|
|
@@ -3652,7 +3671,7 @@ const Dimensions = ({
|
|
|
3652
3671
|
collisionDetection: closestCenter,
|
|
3653
3672
|
onDragEnd: handleDragEnd
|
|
3654
3673
|
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3655
|
-
items:
|
|
3674
|
+
items: orderedItems.map((_, index) => index),
|
|
3656
3675
|
strategy: verticalListSortingStrategy
|
|
3657
3676
|
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3658
3677
|
sx: {
|
|
@@ -3660,67 +3679,35 @@ const Dimensions = ({
|
|
|
3660
3679
|
flexDirection: "column",
|
|
3661
3680
|
gap: 1
|
|
3662
3681
|
}
|
|
3663
|
-
},
|
|
3664
|
-
key:
|
|
3682
|
+
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3683
|
+
key: `dim-${item.key}`,
|
|
3665
3684
|
id: index,
|
|
3666
|
-
label:
|
|
3667
|
-
fullLabel: `${formatProviderPath(
|
|
3668
|
-
onDelete: () => onRemoveDimension(
|
|
3685
|
+
label: item.data.dimensionTitle,
|
|
3686
|
+
fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
|
|
3687
|
+
onDelete: () => onRemoveDimension(item.key),
|
|
3669
3688
|
onMoveUp: () => handleMoveUp(index),
|
|
3670
3689
|
onMoveDown: () => handleMoveDown(index),
|
|
3671
3690
|
isFirst: index === 0,
|
|
3672
|
-
isLast: index ===
|
|
3673
|
-
sortOrder:
|
|
3674
|
-
onSortOrderChange: sortOrder => handleSortOrderChange(
|
|
3675
|
-
fullPath:
|
|
3676
|
-
defaultTitle:
|
|
3677
|
-
customTitle: titleOverrides[
|
|
3691
|
+
isLast: index === orderedItems.length - 1,
|
|
3692
|
+
sortOrder: item.data.sortOrder || null,
|
|
3693
|
+
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3694
|
+
fullPath: item.data.fullPath,
|
|
3695
|
+
defaultTitle: item.data.dimensionTitle,
|
|
3696
|
+
customTitle: titleOverrides[item.data.fullPath],
|
|
3678
3697
|
onUpdateTitle: onUpdateTitle,
|
|
3679
3698
|
onResetTitle: onResetTitle
|
|
3680
|
-
})
|
|
3681
|
-
|
|
3682
|
-
marginTop: 2
|
|
3683
|
-
}
|
|
3684
|
-
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3685
|
-
variant: "h6",
|
|
3686
|
-
sx: {
|
|
3687
|
-
fontSize: "16px",
|
|
3688
|
-
fontWeight: 600,
|
|
3689
|
-
marginTop: 2,
|
|
3690
|
-
color: "rgb(37, 37, 37)"
|
|
3691
|
-
}
|
|
3692
|
-
}, "Saved Computed Dimensions"), /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3693
|
-
sx: {
|
|
3694
|
-
fontSize: "13px",
|
|
3695
|
-
color: "#666",
|
|
3696
|
-
marginBottom: 2
|
|
3697
|
-
}
|
|
3698
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(DndContext, {
|
|
3699
|
-
sensors: sensors,
|
|
3700
|
-
collisionDetection: closestCenter,
|
|
3701
|
-
onDragEnd: handleComputedDragEnd
|
|
3702
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3703
|
-
items: savedComputedDimensions.map((_, index) => index),
|
|
3704
|
-
strategy: verticalListSortingStrategy
|
|
3705
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3706
|
-
sx: {
|
|
3707
|
-
display: "flex",
|
|
3708
|
-
flexDirection: "column",
|
|
3709
|
-
gap: 1
|
|
3710
|
-
}
|
|
3711
|
-
}, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
3712
|
-
key: index,
|
|
3699
|
+
}) : /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
3700
|
+
key: `comp-${item.key}`,
|
|
3713
3701
|
id: index,
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
value: cd.value,
|
|
3702
|
+
name: item.data.name,
|
|
3703
|
+
value: item.data.value,
|
|
3717
3704
|
onUpdate: onUpdateComputedDimension,
|
|
3718
3705
|
isNameTaken: isComputedNameTaken,
|
|
3719
|
-
onDelete: () => onRemoveComputedDimension(
|
|
3720
|
-
onMoveUp: () =>
|
|
3721
|
-
onMoveDown: () =>
|
|
3706
|
+
onDelete: () => onRemoveComputedDimension(item.key),
|
|
3707
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3708
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3722
3709
|
isFirst: index === 0,
|
|
3723
|
-
isLast: index ===
|
|
3710
|
+
isLast: index === orderedItems.length - 1
|
|
3724
3711
|
})))))));
|
|
3725
3712
|
};
|
|
3726
3713
|
|
|
@@ -5703,9 +5690,8 @@ const formatValue = (value, def) => {
|
|
|
5703
5690
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5704
5691
|
const ReportDataGrid = ({
|
|
5705
5692
|
reportData,
|
|
5706
|
-
|
|
5693
|
+
orderedDimensionItems = [],
|
|
5707
5694
|
metrics,
|
|
5708
|
-
computedDimensions = [],
|
|
5709
5695
|
loading,
|
|
5710
5696
|
onPageChange,
|
|
5711
5697
|
totalRows = 0,
|
|
@@ -5725,11 +5711,26 @@ const ReportDataGrid = ({
|
|
|
5725
5711
|
const columns = React__default.useMemo(() => {
|
|
5726
5712
|
const cols = [];
|
|
5727
5713
|
|
|
5728
|
-
// Add dimension columns
|
|
5729
|
-
//
|
|
5714
|
+
// Add dimension + computed-dimension columns, in the single unified
|
|
5715
|
+
// order the user arranged them in on the Dimensions tab.
|
|
5716
|
+
// Field naming logic (dimensions):
|
|
5730
5717
|
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5731
5718
|
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5732
|
-
dimensions
|
|
5719
|
+
// Computed dimensions have no provider/relations chain, so the API
|
|
5720
|
+
// returns each row keyed by the computed dimension's own `name`.
|
|
5721
|
+
orderedDimensionItems.forEach(item => {
|
|
5722
|
+
if (item.kind === 'computed') {
|
|
5723
|
+
const cd = item.data;
|
|
5724
|
+
cols.push({
|
|
5725
|
+
field: cd.name,
|
|
5726
|
+
headerName: cd.name,
|
|
5727
|
+
flex: 1,
|
|
5728
|
+
minWidth: 150,
|
|
5729
|
+
type: 'string'
|
|
5730
|
+
});
|
|
5731
|
+
return;
|
|
5732
|
+
}
|
|
5733
|
+
const dim = item.data;
|
|
5733
5734
|
let fieldName;
|
|
5734
5735
|
let headerName;
|
|
5735
5736
|
|
|
@@ -5767,18 +5768,6 @@ const ReportDataGrid = ({
|
|
|
5767
5768
|
});
|
|
5768
5769
|
});
|
|
5769
5770
|
|
|
5770
|
-
// Add computed dimension columns. These have no provider/relations chain,
|
|
5771
|
-
// so the API returns each row keyed by the computed dimension's own `name`.
|
|
5772
|
-
computedDimensions.forEach(cd => {
|
|
5773
|
-
cols.push({
|
|
5774
|
-
field: cd.name,
|
|
5775
|
-
headerName: cd.name,
|
|
5776
|
-
flex: 1,
|
|
5777
|
-
minWidth: 150,
|
|
5778
|
-
type: 'string'
|
|
5779
|
-
});
|
|
5780
|
-
});
|
|
5781
|
-
|
|
5782
5771
|
// Add metric columns
|
|
5783
5772
|
// The API returns metric keys in two different forms depending on the path depth:
|
|
5784
5773
|
//
|
|
@@ -5838,7 +5827,7 @@ const ReportDataGrid = ({
|
|
|
5838
5827
|
});
|
|
5839
5828
|
});
|
|
5840
5829
|
return cols;
|
|
5841
|
-
}, [
|
|
5830
|
+
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
5842
5831
|
|
|
5843
5832
|
// Transform report data to rows with unique IDs.
|
|
5844
5833
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -5945,7 +5934,10 @@ const ReportBuilder = ({
|
|
|
5945
5934
|
dimensions: [],
|
|
5946
5935
|
metrics: [],
|
|
5947
5936
|
filters: {},
|
|
5948
|
-
computedDimensions: []
|
|
5937
|
+
computedDimensions: [],
|
|
5938
|
+
// Unified display/execution order across dimensions + computedDimensions.
|
|
5939
|
+
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
5940
|
+
dimensionOrder: []
|
|
5949
5941
|
});
|
|
5950
5942
|
const [titleOverrides, setTitleOverrides] = useState({
|
|
5951
5943
|
dimensions: {},
|
|
@@ -5991,6 +5983,16 @@ const ReportBuilder = ({
|
|
|
5991
5983
|
}
|
|
5992
5984
|
}, [autoRun, reportLoaded]);
|
|
5993
5985
|
|
|
5986
|
+
// Re-run the currently displayed report when base_currency changes in context
|
|
5987
|
+
const currentBaseCurrency = reportingContext?.parameters?.base_currency;
|
|
5988
|
+
const prevBaseCurrencyRef = useRef(currentBaseCurrency);
|
|
5989
|
+
useEffect(() => {
|
|
5990
|
+
if (reportData && prevBaseCurrencyRef.current !== undefined && prevBaseCurrencyRef.current !== currentBaseCurrency) {
|
|
5991
|
+
runReportWithPagination(currentPage, pageSize);
|
|
5992
|
+
}
|
|
5993
|
+
prevBaseCurrencyRef.current = currentBaseCurrency;
|
|
5994
|
+
}, [currentBaseCurrency]);
|
|
5995
|
+
|
|
5994
5996
|
// Utility function to reconstruct dimension object from fullPath
|
|
5995
5997
|
const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
|
|
5996
5998
|
try {
|
|
@@ -6120,6 +6122,52 @@ const ReportBuilder = ({
|
|
|
6120
6122
|
return null;
|
|
6121
6123
|
}
|
|
6122
6124
|
};
|
|
6125
|
+
|
|
6126
|
+
// Build the unified display/execution order for dimensions + computed
|
|
6127
|
+
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6128
|
+
// to "dimensions first, then computed dimensions" for report definitions
|
|
6129
|
+
// saved before this field existed.
|
|
6130
|
+
const buildDimensionOrder = (rawOrderedDimensions, dimensions, computedDimensions) => {
|
|
6131
|
+
const dimensionOrder = [];
|
|
6132
|
+
const seenDimensionKeys = new Set();
|
|
6133
|
+
const seenComputedKeys = new Set();
|
|
6134
|
+
if (Array.isArray(rawOrderedDimensions)) {
|
|
6135
|
+
rawOrderedDimensions.forEach(entry => {
|
|
6136
|
+
if (entry.type === "dimension" && dimensions.some(d => d.fullPath === entry.ref)) {
|
|
6137
|
+
dimensionOrder.push({
|
|
6138
|
+
type: "dimension",
|
|
6139
|
+
key: entry.ref
|
|
6140
|
+
});
|
|
6141
|
+
seenDimensionKeys.add(entry.ref);
|
|
6142
|
+
} else if (entry.type === "computed" && computedDimensions.some(cd => cd.name === entry.ref)) {
|
|
6143
|
+
dimensionOrder.push({
|
|
6144
|
+
type: "computed",
|
|
6145
|
+
key: entry.ref
|
|
6146
|
+
});
|
|
6147
|
+
seenComputedKeys.add(entry.ref);
|
|
6148
|
+
}
|
|
6149
|
+
});
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6153
|
+
dimensions.forEach(d => {
|
|
6154
|
+
if (!seenDimensionKeys.has(d.fullPath)) {
|
|
6155
|
+
dimensionOrder.push({
|
|
6156
|
+
type: "dimension",
|
|
6157
|
+
key: d.fullPath
|
|
6158
|
+
});
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
computedDimensions.forEach(cd => {
|
|
6162
|
+
if (!seenComputedKeys.has(cd.name)) {
|
|
6163
|
+
dimensionOrder.push({
|
|
6164
|
+
type: "computed",
|
|
6165
|
+
key: cd.name
|
|
6166
|
+
});
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6169
|
+
return dimensionOrder;
|
|
6170
|
+
};
|
|
6123
6171
|
const loadReportDefinition = async id => {
|
|
6124
6172
|
try {
|
|
6125
6173
|
console.log("Loading report definition:", id);
|
|
@@ -6215,13 +6263,15 @@ const ReportBuilder = ({
|
|
|
6215
6263
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6216
6264
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6217
6265
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6266
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6218
6267
|
|
|
6219
6268
|
// Set the report state
|
|
6220
6269
|
setReport({
|
|
6221
6270
|
dimensions: reconstructedDimensions,
|
|
6222
6271
|
metrics: reconstructedMetrics,
|
|
6223
6272
|
filters: loadedFilters,
|
|
6224
|
-
computedDimensions
|
|
6273
|
+
computedDimensions,
|
|
6274
|
+
dimensionOrder
|
|
6225
6275
|
});
|
|
6226
6276
|
|
|
6227
6277
|
// Set title overrides
|
|
@@ -6330,13 +6380,15 @@ const ReportBuilder = ({
|
|
|
6330
6380
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6331
6381
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6332
6382
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6383
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6333
6384
|
|
|
6334
6385
|
// Set the report state
|
|
6335
6386
|
setReport({
|
|
6336
6387
|
dimensions: reconstructedDimensions,
|
|
6337
6388
|
metrics: reconstructedMetrics,
|
|
6338
6389
|
filters: loadedFilters,
|
|
6339
|
-
computedDimensions
|
|
6390
|
+
computedDimensions,
|
|
6391
|
+
dimensionOrder
|
|
6340
6392
|
});
|
|
6341
6393
|
|
|
6342
6394
|
// Set title overrides
|
|
@@ -6369,7 +6421,8 @@ const ReportBuilder = ({
|
|
|
6369
6421
|
dimensions: [],
|
|
6370
6422
|
metrics: [],
|
|
6371
6423
|
filters: {},
|
|
6372
|
-
computedDimensions: []
|
|
6424
|
+
computedDimensions: [],
|
|
6425
|
+
dimensionOrder: []
|
|
6373
6426
|
});
|
|
6374
6427
|
// Reset title overrides
|
|
6375
6428
|
setTitleOverrides({
|
|
@@ -6446,51 +6499,68 @@ const ReportBuilder = ({
|
|
|
6446
6499
|
setReport(prev => {
|
|
6447
6500
|
const newReport = {
|
|
6448
6501
|
...prev,
|
|
6449
|
-
dimensions: [...prev.dimensions, dimensionData]
|
|
6502
|
+
dimensions: [...prev.dimensions, dimensionData],
|
|
6503
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6504
|
+
type: "dimension",
|
|
6505
|
+
key: dimensionData.fullPath
|
|
6506
|
+
}]
|
|
6450
6507
|
};
|
|
6451
6508
|
console.log("Dimension saved:", dimensionData);
|
|
6452
6509
|
console.log("Complete report:", newReport);
|
|
6453
6510
|
return newReport;
|
|
6454
6511
|
});
|
|
6455
6512
|
};
|
|
6456
|
-
const handleRemoveDimension =
|
|
6513
|
+
const handleRemoveDimension = fullPath => {
|
|
6457
6514
|
setReport(prev => ({
|
|
6458
6515
|
...prev,
|
|
6459
|
-
dimensions: prev.dimensions.filter(
|
|
6516
|
+
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6517
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6460
6518
|
}));
|
|
6461
6519
|
};
|
|
6462
|
-
const
|
|
6520
|
+
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
6463
6521
|
setReport(prev => ({
|
|
6464
6522
|
...prev,
|
|
6465
|
-
dimensions:
|
|
6523
|
+
dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
|
|
6524
|
+
...d,
|
|
6525
|
+
sortOrder
|
|
6526
|
+
} : d)
|
|
6466
6527
|
}));
|
|
6467
6528
|
};
|
|
6468
|
-
|
|
6529
|
+
|
|
6530
|
+
// Reorders the single unified dimensions + computed-dimensions list;
|
|
6531
|
+
// `dimensions`/`computedDimensions` arrays themselves are unordered from
|
|
6532
|
+
// this perspective, so only `dimensionOrder` needs to change.
|
|
6533
|
+
const handleReorderDimensionItems = newDimensionOrder => {
|
|
6469
6534
|
setReport(prev => ({
|
|
6470
6535
|
...prev,
|
|
6471
|
-
|
|
6536
|
+
dimensionOrder: newDimensionOrder
|
|
6472
6537
|
}));
|
|
6473
6538
|
};
|
|
6474
|
-
const
|
|
6475
|
-
setReport(prev => {
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
}
|
|
6482
|
-
});
|
|
6539
|
+
const handleSaveComputedDimension = computedDimensionData => {
|
|
6540
|
+
setReport(prev => ({
|
|
6541
|
+
...prev,
|
|
6542
|
+
computedDimensions: [...prev.computedDimensions, computedDimensionData],
|
|
6543
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6544
|
+
type: "computed",
|
|
6545
|
+
key: computedDimensionData.name
|
|
6546
|
+
}]
|
|
6547
|
+
}));
|
|
6483
6548
|
};
|
|
6484
|
-
const
|
|
6549
|
+
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
6485
6550
|
setReport(prev => ({
|
|
6486
6551
|
...prev,
|
|
6487
|
-
computedDimensions: prev.computedDimensions.
|
|
6552
|
+
computedDimensions: prev.computedDimensions.map(cd => cd.name === name ? computedDimensionData : cd),
|
|
6553
|
+
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6554
|
+
...entry,
|
|
6555
|
+
key: computedDimensionData.name
|
|
6556
|
+
} : entry)
|
|
6488
6557
|
}));
|
|
6489
6558
|
};
|
|
6490
|
-
const
|
|
6559
|
+
const handleRemoveComputedDimension = name => {
|
|
6491
6560
|
setReport(prev => ({
|
|
6492
6561
|
...prev,
|
|
6493
|
-
computedDimensions:
|
|
6562
|
+
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6563
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6494
6564
|
}));
|
|
6495
6565
|
};
|
|
6496
6566
|
const handleSaveMetric = metricData => {
|
|
@@ -6546,10 +6616,20 @@ const ReportBuilder = ({
|
|
|
6546
6616
|
});
|
|
6547
6617
|
};
|
|
6548
6618
|
|
|
6549
|
-
//
|
|
6550
|
-
|
|
6619
|
+
// Builds the query object shared between running (paginated) and saving
|
|
6620
|
+
// (unpaginated) a report. Dimension and computed-dimension order both
|
|
6621
|
+
// derive from the single unified `report.dimensionOrder` list, so the
|
|
6622
|
+
// saved `dimensions`/`computed_dimensions` arrays, `order_by`, and the
|
|
6623
|
+
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6624
|
+
// order the user arranged on screen.
|
|
6625
|
+
const buildQueryObject = () => {
|
|
6626
|
+
const dimensionByPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6627
|
+
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6628
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionByPath[entry.key]).filter(Boolean);
|
|
6629
|
+
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6630
|
+
|
|
6551
6631
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6552
|
-
const orderBy =
|
|
6632
|
+
const orderBy = orderedDimensionsOnly.map(dim => {
|
|
6553
6633
|
const orderItem = {
|
|
6554
6634
|
name: dim.fullPath
|
|
6555
6635
|
};
|
|
@@ -6605,21 +6685,29 @@ const ReportBuilder = ({
|
|
|
6605
6685
|
titles.filters = titleOverrides.filters;
|
|
6606
6686
|
}
|
|
6607
6687
|
const queryObj = {
|
|
6608
|
-
dimensions:
|
|
6688
|
+
dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
|
|
6609
6689
|
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6610
|
-
order_by: orderBy
|
|
6611
|
-
limit: size,
|
|
6612
|
-
offset: page * size
|
|
6690
|
+
order_by: orderBy
|
|
6613
6691
|
};
|
|
6614
6692
|
|
|
6615
6693
|
// Only add computed_dimensions if there are any
|
|
6616
|
-
if (
|
|
6617
|
-
queryObj.computed_dimensions =
|
|
6694
|
+
if (orderedComputedOnly.length > 0) {
|
|
6695
|
+
queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
|
|
6618
6696
|
name: cd.name,
|
|
6619
6697
|
value: cd.value
|
|
6620
6698
|
}));
|
|
6621
6699
|
}
|
|
6622
6700
|
|
|
6701
|
+
// Persist the unified dimension + computed-dimension order so it can be
|
|
6702
|
+
// reconstructed on reload and so executed reports render columns in the
|
|
6703
|
+
// same order they were arranged in the builder.
|
|
6704
|
+
if (report.dimensionOrder.length > 0) {
|
|
6705
|
+
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
6706
|
+
type: entry.type,
|
|
6707
|
+
ref: entry.key
|
|
6708
|
+
}));
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6623
6711
|
// Only add titles if there are any overrides
|
|
6624
6712
|
if (Object.keys(titles).length > 0) {
|
|
6625
6713
|
queryObj.titles = titles;
|
|
@@ -6629,6 +6717,14 @@ const ReportBuilder = ({
|
|
|
6629
6717
|
if (filter) {
|
|
6630
6718
|
queryObj.filter = filter;
|
|
6631
6719
|
}
|
|
6720
|
+
return queryObj;
|
|
6721
|
+
};
|
|
6722
|
+
|
|
6723
|
+
// Convert internal report structure to API format
|
|
6724
|
+
const convertReportToApiFormat = (page = 0, size = 50) => {
|
|
6725
|
+
const queryObj = buildQueryObject();
|
|
6726
|
+
queryObj.limit = size;
|
|
6727
|
+
queryObj.offset = page * size;
|
|
6632
6728
|
|
|
6633
6729
|
// Get parameters from context if available, otherwise use default
|
|
6634
6730
|
const parameters = reportingContext?.parameters || {
|
|
@@ -6722,77 +6818,7 @@ const ReportBuilder = ({
|
|
|
6722
6818
|
}
|
|
6723
6819
|
try {
|
|
6724
6820
|
setSaving(true);
|
|
6725
|
-
|
|
6726
|
-
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6727
|
-
const orderBy = report.dimensions.map(dim => {
|
|
6728
|
-
const orderItem = {
|
|
6729
|
-
name: dim.fullPath
|
|
6730
|
-
};
|
|
6731
|
-
if (dim.sortOrder === "desc") {
|
|
6732
|
-
orderItem.desc = true;
|
|
6733
|
-
}
|
|
6734
|
-
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
6735
|
-
return orderItem;
|
|
6736
|
-
});
|
|
6737
|
-
|
|
6738
|
-
// Build titles object - only include overridden titles
|
|
6739
|
-
const titles = {};
|
|
6740
|
-
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
6741
|
-
titles.dimensions = titleOverrides.dimensions;
|
|
6742
|
-
}
|
|
6743
|
-
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
6744
|
-
titles.metrics = titleOverrides.metrics;
|
|
6745
|
-
}
|
|
6746
|
-
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6747
|
-
titles.filters = titleOverrides.filters;
|
|
6748
|
-
}
|
|
6749
|
-
const queryObj = {
|
|
6750
|
-
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
6751
|
-
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6752
|
-
order_by: orderBy
|
|
6753
|
-
};
|
|
6754
|
-
|
|
6755
|
-
// Only add computed_dimensions if there are any
|
|
6756
|
-
if (report.computedDimensions && report.computedDimensions.length > 0) {
|
|
6757
|
-
queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
|
|
6758
|
-
name: cd.name,
|
|
6759
|
-
value: cd.value
|
|
6760
|
-
}));
|
|
6761
|
-
}
|
|
6762
|
-
|
|
6763
|
-
// Only add titles if there are any overrides
|
|
6764
|
-
if (Object.keys(titles).length > 0) {
|
|
6765
|
-
queryObj.titles = titles;
|
|
6766
|
-
}
|
|
6767
|
-
|
|
6768
|
-
// Add filter if there are any - NEW API format with top-level 'and' operator
|
|
6769
|
-
// NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
6770
|
-
if (Object.keys(report.filters).length > 0) {
|
|
6771
|
-
const conditions = [];
|
|
6772
|
-
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
6773
|
-
if (filterData.values) {
|
|
6774
|
-
// Check if values is an object (date range) or array (regular filter)
|
|
6775
|
-
if (Array.isArray(filterData.values)) {
|
|
6776
|
-
// Regular filter - only add if array has values
|
|
6777
|
-
if (filterData.values.length > 0) {
|
|
6778
|
-
conditions.push({
|
|
6779
|
-
[fullPath]: filterData.values
|
|
6780
|
-
});
|
|
6781
|
-
}
|
|
6782
|
-
} else if (typeof filterData.values === "object") {
|
|
6783
|
-
// Date range filter - add the object as-is
|
|
6784
|
-
conditions.push({
|
|
6785
|
-
[fullPath]: filterData.values
|
|
6786
|
-
});
|
|
6787
|
-
}
|
|
6788
|
-
}
|
|
6789
|
-
});
|
|
6790
|
-
if (conditions.length > 0) {
|
|
6791
|
-
queryObj.filter = {
|
|
6792
|
-
and: conditions
|
|
6793
|
-
};
|
|
6794
|
-
}
|
|
6795
|
-
}
|
|
6821
|
+
const queryObj = buildQueryObject();
|
|
6796
6822
|
|
|
6797
6823
|
// Build the doc object
|
|
6798
6824
|
const docObj = {
|
|
@@ -6837,6 +6863,18 @@ const ReportBuilder = ({
|
|
|
6837
6863
|
}
|
|
6838
6864
|
};
|
|
6839
6865
|
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
|
|
6866
|
+
|
|
6867
|
+
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
6868
|
+
// objects so the results grid can render columns in that same order.
|
|
6869
|
+
const dimensionByFullPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6870
|
+
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6871
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
6872
|
+
kind: "dimension",
|
|
6873
|
+
data: dimensionByFullPath[entry.key]
|
|
6874
|
+
} : {
|
|
6875
|
+
kind: "computed",
|
|
6876
|
+
data: computedDimensionByName[entry.key]
|
|
6877
|
+
}).filter(item => item.data);
|
|
6840
6878
|
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6841
6879
|
sx: {
|
|
6842
6880
|
p: 3,
|
|
@@ -7115,7 +7153,7 @@ const ReportBuilder = ({
|
|
|
7115
7153
|
savedDimensions: report.dimensions,
|
|
7116
7154
|
onSaveDimension: handleSaveDimension,
|
|
7117
7155
|
onRemoveDimension: handleRemoveDimension,
|
|
7118
|
-
|
|
7156
|
+
onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
|
|
7119
7157
|
titleOverrides: titleOverrides.dimensions,
|
|
7120
7158
|
onUpdateTitle: handleUpdateDimensionTitle,
|
|
7121
7159
|
onResetTitle: handleResetDimensionTitle,
|
|
@@ -7125,7 +7163,8 @@ const ReportBuilder = ({
|
|
|
7125
7163
|
onSaveComputedDimension: handleSaveComputedDimension,
|
|
7126
7164
|
onUpdateComputedDimension: handleUpdateComputedDimension,
|
|
7127
7165
|
onRemoveComputedDimension: handleRemoveComputedDimension,
|
|
7128
|
-
|
|
7166
|
+
dimensionOrder: report.dimensionOrder,
|
|
7167
|
+
onReorderDimensionItems: handleReorderDimensionItems
|
|
7129
7168
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7130
7169
|
value: activeTab,
|
|
7131
7170
|
index: 1
|
|
@@ -7161,9 +7200,8 @@ const ReportBuilder = ({
|
|
|
7161
7200
|
index: 3
|
|
7162
7201
|
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
7163
7202
|
reportData: reportData,
|
|
7164
|
-
|
|
7203
|
+
orderedDimensionItems: orderedDimensionItems,
|
|
7165
7204
|
metrics: report.metrics,
|
|
7166
|
-
computedDimensions: report.computedDimensions,
|
|
7167
7205
|
loading: loading,
|
|
7168
7206
|
onPageChange: handlePageChange,
|
|
7169
7207
|
totalRows: totalRows,
|
|
@@ -7226,6 +7264,44 @@ const ReportDefinitionsManager = () => {
|
|
|
7226
7264
|
});
|
|
7227
7265
|
};
|
|
7228
7266
|
|
|
7267
|
+
// TODO: replace with currencies fetched from the API once that's available.
|
|
7268
|
+
const CURRENCY_OPTIONS = [{
|
|
7269
|
+
key: "USD",
|
|
7270
|
+
value: "USD"
|
|
7271
|
+
}, {
|
|
7272
|
+
key: "EUR",
|
|
7273
|
+
value: "EUR"
|
|
7274
|
+
}, {
|
|
7275
|
+
key: "SEK",
|
|
7276
|
+
value: "SEK"
|
|
7277
|
+
}, {
|
|
7278
|
+
key: "GBP",
|
|
7279
|
+
value: "GBP"
|
|
7280
|
+
}, {
|
|
7281
|
+
key: "DKK",
|
|
7282
|
+
value: "DKK"
|
|
7283
|
+
}, {
|
|
7284
|
+
key: "NOK",
|
|
7285
|
+
value: "NOK"
|
|
7286
|
+
}];
|
|
7287
|
+
const CurrencySelector = () => {
|
|
7288
|
+
const {
|
|
7289
|
+
parameters,
|
|
7290
|
+
setParameters
|
|
7291
|
+
} = useReportingContext();
|
|
7292
|
+
return /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
7293
|
+
items: CURRENCY_OPTIONS,
|
|
7294
|
+
value: parameters.base_currency,
|
|
7295
|
+
label: "Currency",
|
|
7296
|
+
onChange: e => setParameters({
|
|
7297
|
+
...parameters,
|
|
7298
|
+
base_currency: e.target.value
|
|
7299
|
+
}),
|
|
7300
|
+
sx: {
|
|
7301
|
+
width: 110
|
|
7302
|
+
}
|
|
7303
|
+
});
|
|
7304
|
+
};
|
|
7229
7305
|
const ReportApp = ({
|
|
7230
7306
|
params,
|
|
7231
7307
|
api
|
|
@@ -7292,7 +7368,13 @@ const ReportApp = ({
|
|
|
7292
7368
|
}
|
|
7293
7369
|
}
|
|
7294
7370
|
})
|
|
7295
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
7371
|
+
}, /*#__PURE__*/React__default.createElement(Box, {
|
|
7372
|
+
sx: {
|
|
7373
|
+
display: "flex",
|
|
7374
|
+
justifyContent: "flex-end",
|
|
7375
|
+
p: 1
|
|
7376
|
+
}
|
|
7377
|
+
}, /*#__PURE__*/React__default.createElement(CurrencySelector, null)), /*#__PURE__*/React__default.createElement(ReportDefinitionsManager, null))));
|
|
7296
7378
|
};
|
|
7297
7379
|
|
|
7298
7380
|
var index = {
|