Skip to content

Commit ee2aaa2

Browse files
renemadsenclaude
andauthored
fix(timeplanning): admin exact-minute actual start/stop under UseOneMinuteIntervals=true (#1572)
* fix(timeplanning): admin exact-minute actual start/stop under UseOneMinuteIntervals=true (#TBD) - Adds Start{1..5}ExactMinutes / Stop{1..5}ExactMinutes DTO fields mirroring PR #1568's pause shape - Math.round in convertTimeToMinutes to kill the 37.6 float that produced the admin-edit error - ApplyExactMinuteStart/Stop helpers write Start*StartedAt/Stop*StoppedAt under flag-on; cross-midnight aware - DeriveLegacyShiftIdsFromTimestamps keeps legacy *Id columns in sync after timestamp writes (admin + worker paths) - ComputePlanningNettoMinutes routes netto math through timestamps under flag-on (was 5-min-quantized via *Id) - New l1m Playwright shard with 6 round-trip regression tests + 1 flag-off regression test in f shard - CI matrix bumped to include all playwright shards on disk Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(timeplanning): fix new l1m playwright tests after first CI run - Drop cross-midnight 23:55->00:30 test: dialog requires stop>start within a shift, so the case is backend-only (ApplyExactMinuteStop handles it for worker-persisted timestamps but admin can't enter it through the UI) - Bump 5-shifts test timeout to 300s: 12 timepicker fills × ~7s each exceeds default 120s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timeplanning): revert Math.round in convertTimeToMinutes; isolate l1m netto test - f1m midnight tests rely on float-value cancellation: (29.8-1)*5 = 144. Math.round broke this. The float-on-wire concern is moot since ASP.NET silently truncates and the new Start*ExactMinutes path writes the precise timestamp regardless. - l1m netto test was reading leftover pause from prior 5-shift test in same cell; isolated by using a different cell. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timeplanning): restore Math.round at wire + route calculatePlanHours via *ExactMinutes under flag-on - Math.round in convertTimeToMinutes is required: float 37.6 → ASP.NET int? deserializer rejects with 400 → silent save failure (the original user-reported "error") - f1m display tests were collateral damage from the prior Math.round revert: their flex math depends on float cancellation of (stop_float-1)*5=exact_minutes - New approach under flag-on: calculatePlanHours derives actualMinutes from picker strings directly (toRawMinutes) instead of *Id, so display is exact-minute regardless of Math.round at the wire - Flag-off path unchanged: still uses legacy *Id math (picker minutesGap=5 ensures integer values) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(timeplanning): isolate l1m netto test from prior pause-timestamp residue The 5-shift test persists Pause1StartedAt/StoppedAt timestamps on cell0_0 (pause='00:02'); the netto-check test then reused the same cell and the backend's ComputePlanningNettoMinutes still saw those 2 min of leftover pause -> netto = 488 min ('8.13') vs the expected 490 min ('8.17'). A prior fix clicked the dialog's pause-delete icon, which only patches the form pause to null. Under UseOneMinuteIntervals=true that sends pause1ExactMinutes = null, and the backend's `if (minutes.HasValue) ApplyExactMinutePause` guard skips ClearPauseTimestamps entirely - so the timestamps survived. Fix: explicitly pick pause='00:00' via the timepicker (after start/stop so the picker's [max]=getMaxDifference resolves to a positive duration). That sends pause1ExactMinutes = 0, which ApplyExactMinutePause routes through ClearPauseTimestamps to null Pause1StartedAt/StoppedAt on save. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 65f8487 commit ee2aaa2

12 files changed

Lines changed: 4066 additions & 155 deletions

File tree

.github/workflows/dotnet-core-master.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
strategy:
8989
fail-fast: false
9090
matrix:
91-
test: [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,b1m,c1m,d1m,e1m,f1m,h1m,i1m,j1m]
91+
test: [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,b1m,c1m,d1m,e1m,f1m,h1m,i1m,j1m,l1m]
9292
steps:
9393
- uses: actions/checkout@v3
9494
with:

.github/workflows/dotnet-core-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
strategy:
8282
fail-fast: false
8383
matrix:
84-
test: [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,b1m,c1m,d1m,e1m,f1m,h1m,i1m,j1m]
84+
test: [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,b1m,c1m,d1m,e1m,f1m,h1m,i1m,j1m,l1m]
8585
steps:
8686
- uses: actions/checkout@v3
8787
with:

eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Planning/TimePlanningPlanningPrDayModel.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ public class TimePlanningPlanningPrDayModel
107107
public int? Pause4ExactMinutes { get; set; }
108108
public int? Pause5ExactMinutes { get; set; }
109109

110+
// Request-only: exact-minute start/stop times under UseOneMinuteIntervals=true.
111+
// Null means the client did not send the new field; backend falls back to the
112+
// legacy Start*Id/Stop*Id (5-minute slot) write path. When set, backend
113+
// translates the minutes-of-day into Start*StartedAt/Stop*StoppedAt timestamps
114+
// (anchor: planning.Date; cross-midnight on Stop when value <= matching Start).
115+
public int? Start1ExactMinutes { get; set; }
116+
public int? Start2ExactMinutes { get; set; }
117+
public int? Start3ExactMinutes { get; set; }
118+
public int? Start4ExactMinutes { get; set; }
119+
public int? Start5ExactMinutes { get; set; }
120+
public int? Stop1ExactMinutes { get; set; }
121+
public int? Stop2ExactMinutes { get; set; }
122+
public int? Stop3ExactMinutes { get; set; }
123+
public int? Stop4ExactMinutes { get; set; }
124+
public int? Stop5ExactMinutes { get; set; }
125+
110126
public int Break1Shift { get; set; }
111127
public int Break2Shift { get; set; }
112128
public int Break3Shift { get; set; }

eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs

Lines changed: 179 additions & 119 deletions
Large diffs are not rendered by default.

eform-client/playwright/e2e/plugins/time-planning-pn/f/dashboard-edit-a.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,56 @@ test.describe('Dashboard edit values', () => {
166166
await expect(page.locator('#planHours')).toHaveValue('22');
167167
await expect(page.locator('#todaysFlex')).toHaveValue('0.00');
168168
});
169+
170+
/**
171+
* Flag-off regression guard for the admin-edit exact-minute work:
172+
* setting a clean 5-min-aligned actual pair (`03:00 → 11:00`) on a
173+
* flag-off worker must still save and reload correctly. The additive
174+
* `Start{N}ExactMinutes` / `Stop{N}ExactMinutes` DTO fields plus the
175+
* server-side `ApplyExactMinuteStart/Stop` helpers were introduced
176+
* for the flag-on path — this test locks the contract that the
177+
* flag-off path is bit-identical to before (i.e. our `Math.round`
178+
* change and the additive DTO fields did NOT alter flag-off
179+
* behavior). A regression in either direction (e.g. ExactMinutes
180+
* leaking into the flag-off save path or the DTO change altering
181+
* serialization defaults) would surface here as a `toHaveValue`
182+
* mismatch or a save-button-disabled stall.
183+
*
184+
* 03:00 → 11:00 = 480 min ⇒ start1Id = (180/5)+1 = 37; stop1Id =
185+
* (660/5)+1 = 133; (133-37)*5 = 480 min ⇒ actualHours = 8.00.
186+
*/
187+
test('flag-off: clean 5-min actual pair 03:00 -> 11:00 round-trips through save + reopen', async ({ page }) => {
188+
await setTimepickerValue(page, 'plannedStartOfShift1', '03', '00');
189+
await setTimepickerValue(page, 'plannedEndOfShift1', '11', '00');
190+
await setTimepickerValue(page, 'start1StartedAt', '03', '00');
191+
await setTimepickerValue(page, 'stop1StoppedAt', '11', '00');
192+
193+
await expect(page.locator('#saveButton')).toBeEnabled({ timeout: 10000 });
194+
const updatePromise = page.waitForResponse(r =>
195+
r.url().includes('/api/time-planning-pn/plannings/') && r.request().method() === 'PUT');
196+
const reindexPromise = page.waitForResponse(r =>
197+
r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST');
198+
await page.locator('#saveButton').click();
199+
const updateResponse = await updatePromise;
200+
await reindexPromise;
201+
if (await page.locator('.overlay-spinner').count() > 0) {
202+
await page.locator('.overlay-spinner').waitFor({ state: 'hidden', timeout: 30000 });
203+
}
204+
await page.waitForTimeout(500);
205+
expect(updateResponse.status(), 'flag-off PUT must succeed with clean 5-min actuals').toBeLessThan(400);
206+
207+
// Re-open the cell and assert the picker shows the saved clean values.
208+
await page.locator('#cell0_0').scrollIntoViewIfNeeded();
209+
await page.locator('#cell0_0').click();
210+
await expect(page.locator('#planHours')).toBeVisible();
211+
await expect(
212+
page.locator('[data-testid="start1StartedAt"]'),
213+
'flag-off shift1 start must round-trip 03:00 unchanged',
214+
).toHaveValue('03:00');
215+
await expect(
216+
page.locator('[data-testid="stop1StoppedAt"]'),
217+
'flag-off shift1 stop must round-trip 11:00 unchanged',
218+
).toHaveValue('11:00');
219+
await page.locator('#cancelButton').click();
220+
});
169221
});

eform-client/playwright/e2e/plugins/time-planning-pn/l1m/420_SDK.sql

Lines changed: 2778 additions & 0 deletions
Large diffs are not rendered by default.

eform-client/playwright/e2e/plugins/time-planning-pn/l1m/420_eform-angular-time-planning-plugin.sql

Lines changed: 518 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)