Skip to content

Commit dc9a1c4

Browse files
author
Bob Strahan
committed
Cost Estimator UI Feature for Context Grouping and Subtotals
1 parent 8c6f2eb commit dc9a1c4

File tree

1 file changed

+86
-12
lines changed

1 file changed

+86
-12
lines changed

src/ui/src/components/document-panel/DocumentPanel.jsx

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,27 @@ import useConfiguration from '../../hooks/use-configuration';
2222

2323
const logger = new Logger('DocumentPanel');
2424

25-
// Format the cost cell content based on whether it's a total row
26-
const formatCostCell = (item) => {
27-
if (item.isTotal) {
28-
return <Box fontWeight="bold">{`${item.note}: ${item.cost}`}</Box>;
25+
// Helper function to parse serviceApi key into context and service
26+
const parseServiceApiKey = (serviceApiKey) => {
27+
const parts = serviceApiKey.split('/');
28+
if (parts.length >= 3) {
29+
const context = parts[0];
30+
const serviceApi = parts.slice(1).join('/');
31+
return { context, serviceApi };
2932
}
30-
return item.cost;
33+
// Fallback for keys that don't follow the new format (less than 3 parts) - set context to ''
34+
return { context: '', serviceApi: serviceApiKey };
35+
};
36+
37+
// Helper function to format cost cells
38+
const formatCostCell = (rowItem) => {
39+
if (rowItem.isTotal) {
40+
return <Box fontWeight="bold">{`${rowItem.note}: ${rowItem.cost}`}</Box>;
41+
}
42+
if (rowItem.isSubtotal) {
43+
return <Box fontWeight="bold" color="text-body-secondary">{`${rowItem.note}: ${rowItem.cost}`}</Box>;
44+
}
45+
return rowItem.cost;
3146
};
3247

3348
// Component to display metering information in a table
@@ -65,15 +80,18 @@ const MeteringTable = ({ meteringData, preCalculatedTotals }) => {
6580
return <Box>Loading pricing data...</Box>;
6681
}
6782

68-
// Transform metering data into table rows
69-
const tableItems = [];
83+
// Transform metering data into table rows with context parsing
84+
const rawTableItems = [];
85+
const contextTotals = {};
7086
let totalCost = 0;
7187

72-
Object.entries(meteringData).forEach(([serviceApi, metrics]) => {
88+
Object.entries(meteringData).forEach(([originalServiceApiKey, metrics]) => {
89+
const { context, serviceApi } = parseServiceApiKey(originalServiceApiKey);
90+
7391
Object.entries(metrics).forEach(([unit, value]) => {
7492
const numericValue = Number(value);
7593

76-
// Look up the unit price from the pricing data
94+
// Look up the unit price from the pricing data using the parsed serviceApi
7795
let unitPrice = null;
7896
let unitPriceDisplayValue = 'None';
7997
let cost = 0;
@@ -83,6 +101,13 @@ const MeteringTable = ({ meteringData, preCalculatedTotals }) => {
83101
unitPriceDisplayValue = `$${unitPrice}`;
84102
cost = numericValue * unitPrice;
85103
totalCost += cost;
104+
105+
// Track context totals
106+
if (!contextTotals[context]) {
107+
contextTotals[context] = 0;
108+
}
109+
contextTotals[context] += cost;
110+
86111
logger.debug(`Found price for ${serviceApi}/${unit}: ${unitPriceDisplayValue}`);
87112
} else {
88113
logger.warn(`Invalid price for ${serviceApi}/${unit}, using None`);
@@ -91,34 +116,80 @@ const MeteringTable = ({ meteringData, preCalculatedTotals }) => {
91116
logger.debug(`No price found for ${serviceApi}/${unit}, using None`);
92117
}
93118

94-
tableItems.push({
119+
rawTableItems.push({
120+
context,
95121
serviceApi,
96122
unit,
97123
value: String(numericValue),
98124
unitCost: unitPriceDisplayValue,
99125
cost: unitPrice !== null ? `$${cost.toFixed(4)}` : 'N/A',
126+
costValue: cost,
100127
isTotal: false,
128+
isSubtotal: false,
101129
});
102130
});
103131
});
104132

133+
// Group items by context and add subtotals
134+
const tableItems = [];
135+
const contextGroups = {};
136+
137+
// Group raw items by context
138+
rawTableItems.forEach((item) => {
139+
if (!contextGroups[item.context]) {
140+
contextGroups[item.context] = [];
141+
}
142+
contextGroups[item.context].push(item);
143+
});
144+
145+
// Sort contexts alphabetically and add items with subtotals
146+
const sortedContexts = Object.keys(contextGroups).sort();
147+
148+
sortedContexts.forEach((context) => {
149+
// Add all items for this context
150+
tableItems.push(...contextGroups[context]);
151+
152+
// Add subtotal row for this context
153+
const contextTotal = contextTotals[context] || 0;
154+
tableItems.push({
155+
context: '',
156+
serviceApi: '',
157+
unit: '',
158+
value: '',
159+
unitCost: '',
160+
cost: `$${contextTotal.toFixed(4)}`,
161+
costValue: contextTotal,
162+
isTotal: false,
163+
isSubtotal: true,
164+
note: `${context} Subtotal`,
165+
});
166+
});
167+
105168
// Use preCalculatedTotals if provided, otherwise calculate locally
106169
const finalTotalCost = preCalculatedTotals ? preCalculatedTotals.totalCost : totalCost;
107170

108-
// Add total row
171+
// Add overall total row
109172
tableItems.push({
173+
context: '',
110174
serviceApi: '',
111175
unit: '',
112176
value: '',
113177
unitCost: '',
114178
cost: `$${finalTotalCost.toFixed(4)}`,
179+
costValue: finalTotalCost,
115180
isTotal: true,
181+
isSubtotal: false,
116182
note: 'Total',
117183
});
118184

119185
return (
120186
<Table
121187
columnDefinitions={[
188+
{
189+
id: 'context',
190+
header: 'Context',
191+
cell: (rowItem) => rowItem.context,
192+
},
122193
{
123194
id: 'serviceApi',
124195
header: 'Service/Api',
@@ -169,7 +240,10 @@ const calculateTotalCosts = (meteringData, documentItem, pricingData) => {
169240
let totalCost = 0;
170241

171242
if (pricingData) {
172-
Object.entries(meteringData).forEach(([serviceApi, metrics]) => {
243+
Object.entries(meteringData).forEach(([originalServiceApiKey, metrics]) => {
244+
// Parse the serviceApi key to remove context prefix
245+
const { serviceApi } = parseServiceApiKey(originalServiceApiKey);
246+
173247
Object.entries(metrics).forEach(([unit, value]) => {
174248
const numericValue = Number(value);
175249
if (pricingData[serviceApi] && pricingData[serviceApi][unit] !== undefined) {

0 commit comments

Comments
 (0)