From 8a614a07ba1025d0be546d3112c5e73b11177fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Tue, 14 Jul 2026 09:46:34 +0200 Subject: [PATCH 1/4] feat(planning): redesign version-history modal as activity-log timeline Reformat the Historie modal into a day-grouped Aktivitetslog timeline (time, human-readable change, actor) per the Design Manual mockup, keeping the large dialog size and the GPS/snapshot side panel. Backend: CompareVersions now diffs the individually recorded pause sub-slots (Pause10-19/100-102 for shift 1, Pause20-29/200-202 for shift 2, StartedAt/StoppedAt) and Pause1-5OverrideMinutes, so punch clock pauses appear in the history instead of producing empty versions. Frontend value formatting for human readability: - planned shift start/end/break and pause overrides: minutes -> HH:mm - legacy Start/Stop/Pause 1-5 tick ids: (id-1)*5 minutes -> HH:mm, 0 = unset - punch datetimes -> HH:mm (accepts Danish-culture '.' time separators) - floats rounded to 2 decimals; unset->unset noise rows suppressed Adds 62 i18n keys across all 26 locales, two backend unit tests for the new diff fields, and a Playwright e2e spec asserting the timeline formatting end to end. Co-Authored-By: Claude Fable 5 --- .../PlanRegistrationVersionHistoryTests.cs | 101 ++++++++++ .../TimePlanningPlanningService.cs | 63 +++++++ .../b/dashboard-version-history.spec.ts | 178 ++++++++++++++++++ .../version-history-modal.component.html | 97 +++++----- .../version-history-modal.component.scss | 150 ++++++++++----- .../version-history-modal.component.ts | 174 ++++++++++++----- .../workday-entity-dialog.component.ts | 6 +- .../modules/time-planning-pn/i18n/bgBG.ts | 62 ++++++ .../modules/time-planning-pn/i18n/csCZ.ts | 62 ++++++ .../modules/time-planning-pn/i18n/da.ts | 62 ++++++ .../modules/time-planning-pn/i18n/deDE.ts | 62 ++++++ .../modules/time-planning-pn/i18n/elGR.ts | 62 ++++++ .../modules/time-planning-pn/i18n/enUS.ts | 62 ++++++ .../modules/time-planning-pn/i18n/esES.ts | 62 ++++++ .../modules/time-planning-pn/i18n/etET.ts | 62 ++++++ .../modules/time-planning-pn/i18n/fiFI.ts | 62 ++++++ .../modules/time-planning-pn/i18n/frFR.ts | 62 ++++++ .../modules/time-planning-pn/i18n/hrHR.ts | 62 ++++++ .../modules/time-planning-pn/i18n/huHU.ts | 62 ++++++ .../modules/time-planning-pn/i18n/isIS.ts | 62 ++++++ .../modules/time-planning-pn/i18n/itIT.ts | 62 ++++++ .../modules/time-planning-pn/i18n/ltLT.ts | 62 ++++++ .../modules/time-planning-pn/i18n/lvLV.ts | 62 ++++++ .../modules/time-planning-pn/i18n/nlNL.ts | 62 ++++++ .../modules/time-planning-pn/i18n/noNO.ts | 62 ++++++ .../modules/time-planning-pn/i18n/plPL.ts | 62 ++++++ .../modules/time-planning-pn/i18n/ptBR.ts | 62 ++++++ .../modules/time-planning-pn/i18n/ptPT.ts | 62 ++++++ .../modules/time-planning-pn/i18n/roRO.ts | 62 ++++++ .../modules/time-planning-pn/i18n/skSK.ts | 62 ++++++ .../modules/time-planning-pn/i18n/slSL.ts | 62 ++++++ .../modules/time-planning-pn/i18n/svSE.ts | 62 ++++++ .../modules/time-planning-pn/i18n/ukUA.ts | 62 ++++++ 33 files changed, 2236 insertions(+), 145 deletions(-) create mode 100644 eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-version-history.spec.ts diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationVersionHistoryTests.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationVersionHistoryTests.cs index 7e3dcf51a..c4a1de0d1 100644 --- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationVersionHistoryTests.cs +++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationVersionHistoryTests.cs @@ -290,4 +290,105 @@ public async Task GetVersionHistory_ExcludesGpsCoordinates_WhenGpsDisabled() .Any(v => v.Changes.Any(c => c.FieldType == "gps")); Assert.That(hasGpsChange, Is.False); } + + [Test] + public async Task GetVersionHistory_ReturnsPauseSubSlotChanges_WhenPauseSubSlotFieldsAreUpdated() + { + // Arrange + var assignedSite = new AssignedSiteEntity + { + SiteId = 1, + GpsEnabled = false, + SnapshotEnabled = false, + CreatedByUserId = 1, + UpdatedByUserId = 1 + }; + await assignedSite.Create(TimePlanningPnDbContext); + + var initialPause10StartedAt = new DateTime(2026, 7, 1, 9, 0, 0); + var planRegistration = new PlanRegistration + { + Date = DateTime.Now, + SdkSitId = 1, + Pause10StartedAt = initialPause10StartedAt, + CreatedByUserId = 1, + UpdatedByUserId = 1 + }; + await planRegistration.Create(TimePlanningPnDbContext); + + // Update the pause sub-slot fields + var updatedPause10StartedAt = new DateTime(2026, 7, 1, 9, 15, 0); + var updatedPause11StoppedAt = new DateTime(2026, 7, 1, 12, 30, 0); + planRegistration.Pause10StartedAt = updatedPause10StartedAt; + planRegistration.Pause11StoppedAt = updatedPause11StoppedAt; + planRegistration.UpdatedByUserId = 2; + await planRegistration.Update(TimePlanningPnDbContext); + + // Act + var result = await _planningService.GetVersionHistory(planRegistration.Id); + + // Assert + Assert.That(result.Success, Is.True); + Assert.That(result.Model, Is.Not.Null); + Assert.That(result.Model.Versions.Count, Is.GreaterThan(0)); + + var latestVersion = result.Model.Versions.FirstOrDefault(); + Assert.That(latestVersion, Is.Not.Null); + + var pause10Change = latestVersion.Changes.FirstOrDefault(c => c.FieldName == "Pause10StartedAt"); + Assert.That(pause10Change, Is.Not.Null); + Assert.That(pause10Change.FromValue, Is.EqualTo(initialPause10StartedAt.ToString("yyyy-MM-dd HH:mm:ss.ffffff"))); + Assert.That(pause10Change.ToValue, Is.EqualTo(updatedPause10StartedAt.ToString("yyyy-MM-dd HH:mm:ss.ffffff"))); + + var pause11Change = latestVersion.Changes.FirstOrDefault(c => c.FieldName == "Pause11StoppedAt"); + Assert.That(pause11Change, Is.Not.Null); + Assert.That(pause11Change.FromValue, Is.EqualTo("")); + Assert.That(pause11Change.ToValue, Is.EqualTo(updatedPause11StoppedAt.ToString("yyyy-MM-dd HH:mm:ss.ffffff"))); + } + + [Test] + public async Task GetVersionHistory_ReturnsPauseOverrideMinutesChange_WhenPauseOverrideMinutesIsUpdated() + { + // Arrange + var assignedSite = new AssignedSiteEntity + { + SiteId = 1, + GpsEnabled = false, + SnapshotEnabled = false, + CreatedByUserId = 1, + UpdatedByUserId = 1 + }; + await assignedSite.Create(TimePlanningPnDbContext); + + var planRegistration = new PlanRegistration + { + Date = DateTime.Now, + SdkSitId = 1, + Pause2OverrideMinutes = 15, + CreatedByUserId = 1, + UpdatedByUserId = 1 + }; + await planRegistration.Create(TimePlanningPnDbContext); + + // Update the pause override minutes + planRegistration.Pause2OverrideMinutes = 45; + planRegistration.UpdatedByUserId = 2; + await planRegistration.Update(TimePlanningPnDbContext); + + // Act + var result = await _planningService.GetVersionHistory(planRegistration.Id); + + // Assert + Assert.That(result.Success, Is.True); + Assert.That(result.Model, Is.Not.Null); + Assert.That(result.Model.Versions.Count, Is.GreaterThan(0)); + + var latestVersion = result.Model.Versions.FirstOrDefault(); + Assert.That(latestVersion, Is.Not.Null); + + var overrideChange = latestVersion.Changes.FirstOrDefault(c => c.FieldName == "Pause2OverrideMinutes"); + Assert.That(overrideChange, Is.Not.Null); + Assert.That(overrideChange.FromValue, Is.EqualTo("15")); + Assert.That(overrideChange.ToValue, Is.EqualTo("45")); + } } diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs index db768ccdd..d5ce2e36b 100644 --- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs +++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs @@ -2154,12 +2154,68 @@ private List CompareVersions(PlanRegistrationVersion current, PlanR CompareDateTimeField(changes, "Pause1StartedAt", previous?.Pause1StartedAt, current.Pause1StartedAt); CompareDateTimeField(changes, "Pause1StoppedAt", previous?.Pause1StoppedAt, current.Pause1StoppedAt); + // Shift 1 pause sub-slots + CompareDateTimeField(changes, "Pause10StartedAt", previous?.Pause10StartedAt, current.Pause10StartedAt); + CompareDateTimeField(changes, "Pause10StoppedAt", previous?.Pause10StoppedAt, current.Pause10StoppedAt); + CompareDateTimeField(changes, "Pause11StartedAt", previous?.Pause11StartedAt, current.Pause11StartedAt); + CompareDateTimeField(changes, "Pause11StoppedAt", previous?.Pause11StoppedAt, current.Pause11StoppedAt); + CompareDateTimeField(changes, "Pause12StartedAt", previous?.Pause12StartedAt, current.Pause12StartedAt); + CompareDateTimeField(changes, "Pause12StoppedAt", previous?.Pause12StoppedAt, current.Pause12StoppedAt); + CompareDateTimeField(changes, "Pause13StartedAt", previous?.Pause13StartedAt, current.Pause13StartedAt); + CompareDateTimeField(changes, "Pause13StoppedAt", previous?.Pause13StoppedAt, current.Pause13StoppedAt); + CompareDateTimeField(changes, "Pause14StartedAt", previous?.Pause14StartedAt, current.Pause14StartedAt); + CompareDateTimeField(changes, "Pause14StoppedAt", previous?.Pause14StoppedAt, current.Pause14StoppedAt); + CompareDateTimeField(changes, "Pause15StartedAt", previous?.Pause15StartedAt, current.Pause15StartedAt); + CompareDateTimeField(changes, "Pause15StoppedAt", previous?.Pause15StoppedAt, current.Pause15StoppedAt); + CompareDateTimeField(changes, "Pause16StartedAt", previous?.Pause16StartedAt, current.Pause16StartedAt); + CompareDateTimeField(changes, "Pause16StoppedAt", previous?.Pause16StoppedAt, current.Pause16StoppedAt); + CompareDateTimeField(changes, "Pause17StartedAt", previous?.Pause17StartedAt, current.Pause17StartedAt); + CompareDateTimeField(changes, "Pause17StoppedAt", previous?.Pause17StoppedAt, current.Pause17StoppedAt); + CompareDateTimeField(changes, "Pause18StartedAt", previous?.Pause18StartedAt, current.Pause18StartedAt); + CompareDateTimeField(changes, "Pause18StoppedAt", previous?.Pause18StoppedAt, current.Pause18StoppedAt); + CompareDateTimeField(changes, "Pause19StartedAt", previous?.Pause19StartedAt, current.Pause19StartedAt); + CompareDateTimeField(changes, "Pause19StoppedAt", previous?.Pause19StoppedAt, current.Pause19StoppedAt); + CompareDateTimeField(changes, "Pause100StartedAt", previous?.Pause100StartedAt, current.Pause100StartedAt); + CompareDateTimeField(changes, "Pause100StoppedAt", previous?.Pause100StoppedAt, current.Pause100StoppedAt); + CompareDateTimeField(changes, "Pause101StartedAt", previous?.Pause101StartedAt, current.Pause101StartedAt); + CompareDateTimeField(changes, "Pause101StoppedAt", previous?.Pause101StoppedAt, current.Pause101StoppedAt); + CompareDateTimeField(changes, "Pause102StartedAt", previous?.Pause102StartedAt, current.Pause102StartedAt); + CompareDateTimeField(changes, "Pause102StoppedAt", previous?.Pause102StoppedAt, current.Pause102StoppedAt); + // Shift 2 CompareDateTimeField(changes, "Start2StartedAt", previous?.Start2StartedAt, current.Start2StartedAt); CompareDateTimeField(changes, "Stop2StoppedAt", previous?.Stop2StoppedAt, current.Stop2StoppedAt); CompareDateTimeField(changes, "Pause2StartedAt", previous?.Pause2StartedAt, current.Pause2StartedAt); CompareDateTimeField(changes, "Pause2StoppedAt", previous?.Pause2StoppedAt, current.Pause2StoppedAt); + // Shift 2 pause sub-slots + CompareDateTimeField(changes, "Pause20StartedAt", previous?.Pause20StartedAt, current.Pause20StartedAt); + CompareDateTimeField(changes, "Pause20StoppedAt", previous?.Pause20StoppedAt, current.Pause20StoppedAt); + CompareDateTimeField(changes, "Pause21StartedAt", previous?.Pause21StartedAt, current.Pause21StartedAt); + CompareDateTimeField(changes, "Pause21StoppedAt", previous?.Pause21StoppedAt, current.Pause21StoppedAt); + CompareDateTimeField(changes, "Pause22StartedAt", previous?.Pause22StartedAt, current.Pause22StartedAt); + CompareDateTimeField(changes, "Pause22StoppedAt", previous?.Pause22StoppedAt, current.Pause22StoppedAt); + CompareDateTimeField(changes, "Pause23StartedAt", previous?.Pause23StartedAt, current.Pause23StartedAt); + CompareDateTimeField(changes, "Pause23StoppedAt", previous?.Pause23StoppedAt, current.Pause23StoppedAt); + CompareDateTimeField(changes, "Pause24StartedAt", previous?.Pause24StartedAt, current.Pause24StartedAt); + CompareDateTimeField(changes, "Pause24StoppedAt", previous?.Pause24StoppedAt, current.Pause24StoppedAt); + CompareDateTimeField(changes, "Pause25StartedAt", previous?.Pause25StartedAt, current.Pause25StartedAt); + CompareDateTimeField(changes, "Pause25StoppedAt", previous?.Pause25StoppedAt, current.Pause25StoppedAt); + CompareDateTimeField(changes, "Pause26StartedAt", previous?.Pause26StartedAt, current.Pause26StartedAt); + CompareDateTimeField(changes, "Pause26StoppedAt", previous?.Pause26StoppedAt, current.Pause26StoppedAt); + CompareDateTimeField(changes, "Pause27StartedAt", previous?.Pause27StartedAt, current.Pause27StartedAt); + CompareDateTimeField(changes, "Pause27StoppedAt", previous?.Pause27StoppedAt, current.Pause27StoppedAt); + CompareDateTimeField(changes, "Pause28StartedAt", previous?.Pause28StartedAt, current.Pause28StartedAt); + CompareDateTimeField(changes, "Pause28StoppedAt", previous?.Pause28StoppedAt, current.Pause28StoppedAt); + CompareDateTimeField(changes, "Pause29StartedAt", previous?.Pause29StartedAt, current.Pause29StartedAt); + CompareDateTimeField(changes, "Pause29StoppedAt", previous?.Pause29StoppedAt, current.Pause29StoppedAt); + CompareDateTimeField(changes, "Pause200StartedAt", previous?.Pause200StartedAt, current.Pause200StartedAt); + CompareDateTimeField(changes, "Pause200StoppedAt", previous?.Pause200StoppedAt, current.Pause200StoppedAt); + CompareDateTimeField(changes, "Pause201StartedAt", previous?.Pause201StartedAt, current.Pause201StartedAt); + CompareDateTimeField(changes, "Pause201StoppedAt", previous?.Pause201StoppedAt, current.Pause201StoppedAt); + CompareDateTimeField(changes, "Pause202StartedAt", previous?.Pause202StartedAt, current.Pause202StartedAt); + CompareDateTimeField(changes, "Pause202StoppedAt", previous?.Pause202StoppedAt, current.Pause202StoppedAt); + // Shift 3 CompareDateTimeField(changes, "Start3StartedAt", previous?.Start3StartedAt, current.Start3StartedAt); CompareDateTimeField(changes, "Stop3StoppedAt", previous?.Stop3StoppedAt, current.Stop3StoppedAt); @@ -2178,6 +2234,13 @@ private List CompareVersions(PlanRegistrationVersion current, PlanR CompareDateTimeField(changes, "Pause5StartedAt", previous?.Pause5StartedAt, current.Pause5StartedAt); CompareDateTimeField(changes, "Pause5StoppedAt", previous?.Pause5StoppedAt, current.Pause5StoppedAt); + // Pause override minutes (admin override of a shift's pause total) + CompareField(changes, "Pause1OverrideMinutes", previous?.Pause1OverrideMinutes.ToString(), current.Pause1OverrideMinutes.ToString()); + CompareField(changes, "Pause2OverrideMinutes", previous?.Pause2OverrideMinutes.ToString(), current.Pause2OverrideMinutes.ToString()); + CompareField(changes, "Pause3OverrideMinutes", previous?.Pause3OverrideMinutes.ToString(), current.Pause3OverrideMinutes.ToString()); + CompareField(changes, "Pause4OverrideMinutes", previous?.Pause4OverrideMinutes.ToString(), current.Pause4OverrideMinutes.ToString()); + CompareField(changes, "Pause5OverrideMinutes", previous?.Pause5OverrideMinutes.ToString(), current.Pause5OverrideMinutes.ToString()); + return changes; } diff --git a/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-version-history.spec.ts b/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-version-history.spec.ts new file mode 100644 index 000000000..f145cf900 --- /dev/null +++ b/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-version-history.spec.ts @@ -0,0 +1,178 @@ +import { test, expect, Page } from '@playwright/test'; +import { LoginPage } from '../../../Page objects/Login.page'; + +/** + * b shard: version-history ("Aktivitetslog") modal + human-readable value + * formatting. + * + * The workday edit dialog exposes an admin-only history icon-button which + * opens VersionHistoryModalComponent — a day-grouped timeline where the + * backend diffs PlanRegistrationVersion rows into FieldChange lists and the + * frontend renders values human-readably: + * - Planned(Start|End|Break)OfShift1-5 / Pause1-5OverrideMinutes are stored + * as integer minutes and must render HH:mm (420 -> "07:00", 900 -> "15:00", + * 60 -> "01:00") — never the raw integer. + * - Each event line reads "