diff --git a/cypress/e2e/AutomationCalculator.spec.js b/cypress/e2e/AutomationCalculator.spec.js index 1c8e41995..9d1d8748b 100644 --- a/cypress/e2e/AutomationCalculator.spec.js +++ b/cypress/e2e/AutomationCalculator.spec.js @@ -223,12 +223,13 @@ describe('Automation Calculator page', () => { return; } + // The default is staged locally and only persisted when "Apply default + // to all templates" is confirmed, so typing here must not hit the + // network — no waitToLoad(). cy.get('#default-manual-effort').clear(); - waitToLoad(); cy.get('#default-manual-effort').should('have.value', '0'); cy.get('#default-manual-effort').type('60'); - waitToLoad(); cy.get('#default-manual-effort') .invoke('val') .then((val) => { @@ -431,7 +432,7 @@ describe('Automation Calculator page', () => { waitForStubbedLoad(); cy.getByCy('apply_default_modal').should('not.exist'); - cy.contains('Default manual time applied to 3 templates.').should( + cy.contains('Default manual time applied to all templates.').should( 'exist', ); @@ -445,6 +446,42 @@ describe('Automation Calculator page', () => { }); }); + it('does not update savings when editing the default before applying', () => { + // The default manual time is staged locally, like a per-template manual + // time edit — neither savings figure should move until "Apply default to + // all templates" is confirmed. + visitWithStubbedTemplates(); + + cy.intercept('POST', '/api/tower-analytics/v1/roi_cost_effort_data/').as( + 'roiCostEffortSave', + ); + cy.intercept('POST', '**/roi_templates_apply_default/').as('applyDefault'); + + cy.getByCy('total_savings') + .find('h3') + .invoke('text') + .then((originalTotalSavings) => { + cy.getByCy('current_page_savings') + .find('h3') + .invoke('text') + .then((originalCurrentPageSavings) => { + cy.get('#default-manual-effort').clear(); + cy.get('#default-manual-effort').type('45'); + + cy.getByCy('total_savings') + .find('h3') + .invoke('text') + .should('eq', originalTotalSavings); + cy.getByCy('current_page_savings') + .find('h3') + .invoke('text') + .should('eq', originalCurrentPageSavings); + cy.get('@roiCostEffortSave.all').should('have.length', 0); + cy.get('@applyDefault.all').should('have.length', 0); + }); + }); + }); + it('cancels apply default without calling the endpoint', () => { cy.intercept('POST', '**/roi_templates_apply_default/', { updated_count: 3, @@ -514,7 +551,7 @@ describe('Automation Calculator page', () => { cy.getByCy('apply_default_button').should('not.be.disabled'); }); - it('falls back to 0 when the apply response has no updated_count', () => { + it('shows success message when the apply response has no body', () => { cy.intercept('POST', '**/roi_templates_apply_default/', {}).as( 'applyDefault', ); @@ -526,7 +563,9 @@ describe('Automation Calculator page', () => { cy.wait('@applyDefault'); waitForStubbedLoad(); - cy.contains('Default manual time applied to 0 templates.').should('exist'); + cy.contains('Default manual time applied to all templates.').should( + 'exist', + ); }); it('disables cancel/close while a request is in flight', () => { diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx index 68ef38ecc..d53a33c57 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/AutomationCalculator.tsx @@ -204,6 +204,7 @@ const AutomationCalculator: FC = ({ res.successful_hosts_saved_hours_current_page; api.result.successful_hosts_saved_hours_other_pages = res.successful_hosts_saved_hours_other_pages; + api.result.cost = res.cost; setValue( mapApi( res.meta as any, @@ -215,16 +216,21 @@ const AutomationCalculator: FC = ({ const updateCalculationValues = async (varName: string, value: number) => { if (isNaN(value)) return; + if (varName === 'default_manual_effort_minutes') { + // Staged only: the default is committed via the explicit "Apply + // default to all templates" action (applyDefaultToAll), not on every + // keystroke. Mirrors per-template manual time, which also only + // commits on its own action (blur). + setDefaultManualEffort(value); + return; + } const hourly_automation_cost = varName === 'automation_cost' ? value : costAutomation; const hourly_manual_labor_cost = varName === 'manual_cost' ? value : costManual; - const manual_effort = - varName === 'default_manual_effort_minutes' ? value : defaultManualEffort; const humanVarNames: Record = { automation_cost: 'Automation cost', manual_cost: 'Manual cost', - default_manual_effort_minutes: 'Default manual time', }; const humanVarName = humanVarNames[varName] ?? varName; try { @@ -233,7 +239,7 @@ const AutomationCalculator: FC = ({ api.result.items, hourly_manual_labor_cost, hourly_automation_cost, - manual_effort, + defaultManualEffort, ) as any, ); } catch { @@ -250,8 +256,6 @@ const AutomationCalculator: FC = ({ setCostManual(value); } else if (varName === 'automation_cost') { setCostAutomation(value); - } else if (varName === 'default_manual_effort_minutes') { - setDefaultManualEffort(value); } }; @@ -312,13 +316,14 @@ const AutomationCalculator: FC = ({ }; const applyDefaultToAll = async () => { - let count = 0; try { + // The default is staged locally (see updateCalculationValues) and only + // committed here, so persist it before the apply endpoint reads it — + // that endpoint POSTs no body and relies on the server-persisted + // default_manual_effort_minutes. + await saveROI(getROISaveData(api.result.items) as any); // backend applies to all templates tenant-wide; no params needed - const res = (await applyDefaultROITemplates()) as { - updated_count?: number; - }; - count = typeof res.updated_count === 'number' ? res.updated_count : 0; + await applyDefaultROITemplates(); } catch { addNotification({ title: 'Unable to apply default manual time', @@ -331,9 +336,10 @@ const AutomationCalculator: FC = ({ } addNotification({ - title: `Default manual time applied to ${count} template${ - count === 1 ? '' : 's' - }.`, + // updated_count from the backend only reflects templates with a prior + // manual-effort row, not the true total the default was applied to — + // so the message stays count-free. + title: 'Default manual time applied to all templates.', variant: 'success', dismissable: true, }); diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx index 8a943ded6..ab31b1082 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/CalculationCost.tsx @@ -14,6 +14,12 @@ const WInputGroup = styled(InputGroup)` width: 170px; `; +const Divider = styled.hr` + margin: 16px 0 12px 0; + border: none; + border-top: 1px solid var(--pf-t--global--border--color--200); +`; + const validFloat = (value: number): number => +value && +value < 0 ? NaN : value; @@ -97,6 +103,7 @@ const CalculationCost: FunctionComponent = ({ /> /hr +

Default manual time per template (minutes)

diff --git a/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx b/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx index e9e764359..1f59853e4 100644 --- a/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx +++ b/src/Containers/Reports/Layouts/AutomationCalculator/TemplatesTable/Row.tsx @@ -117,6 +117,7 @@ const Row: FunctionComponent = ({