From ec70120c1de32a69b668b37a2be9480efbc54afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Sat, 2 May 2026 06:07:28 +0200 Subject: [PATCH] feat(assigned-site-dialog): expose UseOneMinuteIntervals toggle for first-user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AssignedSite.UseOneMinuteIntervals flag was wired through the entity, server DTO, write mapping, and angular model — but never exposed in the dialog UI. Adds a new "Advanced settings" section at the bottom of the Generelt tab, gated to the first-user via selectCurrentUserIsFirstUser$ (mirroring the "Use only plan hours" pattern at the top of the same dialog). Persistence path is unchanged — TimeSettingService.UpdateAssignedSite already maps the field. Adds a playwright e2e test that toggles the value, saves, re-opens the dialog, and asserts the toggle remains set. Adds 3 unit tests covering form-control wiring; the no-DOM pattern matches the convention adopted in PR #1538 due to the NG01203 value-accessor issue with template-driven controls. The flag has no business-logic consumers yet (dormant); enabling it persists the choice but has no visible runtime effect until downstream services adopt it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../b/dashboard-edit-multishift.spec.ts | 62 +++++++++++++++++++ .../assigned-site-dialog.component.html | 15 +++++ .../assigned-site-dialog.component.spec.ts | 58 +++++++++++++++++ .../assigned-site-dialog.component.ts | 1 + .../modules/time-planning-pn/i18n/da.ts | 3 + .../modules/time-planning-pn/i18n/enUS.ts | 3 + 6 files changed, 142 insertions(+) diff --git a/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-edit-multishift.spec.ts b/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-edit-multishift.spec.ts index 3c467e25c..245ec1ee6 100644 --- a/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-edit-multishift.spec.ts +++ b/eform-client/playwright/e2e/plugins/time-planning-pn/b/dashboard-edit-multishift.spec.ts @@ -245,4 +245,66 @@ test.describe('Dashboard — multi-shift (3-5) round-trip regression guard', () await page.locator('#cancelButton').click(); }); + + /** + * Regression guard for the "Use 1-minute intervals" first-user toggle in + * the Advanced settings section. The flag rides on AssignedSite end-to-end + * (entity → DTO → write mapping → angular model) and is persisted by + * TimeSettingService.UpdateAssignedSite. This test only verifies the UI + * plumbing — the toggle is dormant (no business-logic consumers yet). + * + * Gating: !data.resigned && (selectCurrentUserIsFirstUser$ | async). + * The CI seed logs in as admin@admin.com (LoginConstants.username), which + * is the first-user, so the toggle is visible in this test context. + */ + test('first-user can toggle Use 1-minute intervals; persists across save+reopen', async ({ page }) => { + await page.locator('mat-nested-tree-node').filter({ hasText: 'Timeregistrering' }).click(); + const indexPromise = page.waitForResponse(r => + r.url().includes('/api/time-planning-pn/plannings/index') && r.request().method() === 'POST'); + await page.locator('mat-tree-node').filter({ hasText: 'Dashboard' }).click(); + await indexPromise; + await waitForSpinner(page); + + // Open the assigned-site dialog for the third worker — same convention + // as the other tests on this shard so they don't clobber each other. + await page.locator('#firstColumn3').click(); + await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 }); + + // The toggle is gated behind first-user; the CI fixture logs in as + // first-user, so it must be visible. + await expect(page.locator('#useOneMinuteIntervals')).toBeVisible({ timeout: 10000 }); + + const cb = page.locator('#useOneMinuteIntervals input[type="checkbox"]'); + await cb.waitFor({ state: 'attached', timeout: 10000 }); + + // Capture the starting state, flip it, save, re-open, assert it persisted. + const wasChecked = await cb.isChecked(); + await page.locator('#useOneMinuteIntervals').click({ force: true }); + if (wasChecked) { + await expect(cb).not.toBeChecked(); + } else { + await expect(cb).toBeChecked(); + } + + const assignSitePromise = page.waitForResponse( + r => r.url().includes('/api/time-planning-pn/settings/assigned-site') && r.request().method() === 'PUT'); + await page.locator('#saveButton').click({ force: true }); + await assignSitePromise; + await waitForSpinner(page); + await expect(page.locator('mat-dialog-container')).toHaveCount(0, { timeout: 10000 }); + + // Re-open and assert the new value round-tripped. + await page.locator('#firstColumn3').click(); + await expect(page.locator('mat-dialog-container')).toBeVisible({ timeout: 10000 }); + + const cbReopened = page.locator('#useOneMinuteIntervals input[type="checkbox"]'); + await cbReopened.waitFor({ state: 'attached', timeout: 10000 }); + if (wasChecked) { + await expect(cbReopened).not.toBeChecked(); + } else { + await expect(cbReopened).toBeChecked(); + } + + await page.locator('#cancelButton').click(); + }); }); diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.html b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.html index 42705c957..a5861ec5f 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.html +++ b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.html @@ -224,6 +224,21 @@

{{ 'Employee status' | translate }}

+ + +

{{ 'Advanced settings' | translate }}

+ +
+ +
+
{{ 'Use 1-minute intervals' | translate }}
+ {{ 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.' | translate }} +
+
+
diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.spec.ts b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.spec.ts index 18ed986fc..0270d19c5 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.spec.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.spec.ts @@ -565,4 +565,62 @@ describe('AssignedSiteDialogComponent', () => { } }); }); + + describe('Use 1-minute intervals toggle', () => { + // The toggle's *ngIf gates on: + // !data.resigned && (selectCurrentUserIsFirstUser$ | async) + // We follow the same no-`detectChanges` pattern adopted in PR #1538: + // assert against component state and observables, not the rendered DOM. + // Rendering the dialog under NO_ERRORS_SCHEMA hits the + // NG01203 "No value accessor for form control name: 'resigned'" issue + // because Material form-control directives aren't registered in the + // tested module. + + function setupWithData( + overrides: Partial, + ): AssignedSiteDialogComponent { + const data = { ...mockAssignedSiteData, ...overrides }; + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + declarations: [AssignedSiteDialogComponent], + imports: [ReactiveFormsModule, TranslateModule.forRoot(), HttpClientTestingModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + FormBuilder, + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: TimePlanningPnSettingsService, useValue: mockSettingsService }, + { provide: TimePlanningPnPayRuleSetsService, useValue: mockPayRuleSetsService }, + { provide: Store, useValue: mockStore }, + ], + }).compileComponents(); + const newFixture = TestBed.createComponent(AssignedSiteDialogComponent); + const c = newFixture.componentInstance; + c.ngOnInit(); + return c; + } + + it('should expose the useOneMinuteIntervals form control', () => { + const c = setupWithData({}); + expect(c.assignedSiteForm.get('useOneMinuteIntervals')).not.toBeNull(); + }); + + it('should default useOneMinuteIntervals form value to data value (false by default)', () => { + const cFalse = setupWithData({}); + expect(cFalse.assignedSiteForm.get('useOneMinuteIntervals')?.value).toBe(false); + + const cTrue = setupWithData({ useOneMinuteIntervals: true } as any); + expect(cTrue.assignedSiteForm.get('useOneMinuteIntervals')?.value).toBe(true); + }); + + it('should expose first-user state via selectCurrentUserIsFirstUser$', () => { + // The new section is gated by !data.resigned && (selectCurrentUserIsFirstUser$ | async); + // assert the observable wiring instead of rendering the DOM (see top-of-block rationale). + const c = setupWithData({}); + expect(c.selectCurrentUserIsFirstUser$).toBeDefined(); + let isFirstUser = false; + c.selectCurrentUserIsFirstUser$.subscribe((v: boolean) => (isFirstUser = v)); + // mockStore.select returns of(true) for every selector; verifies the gate flows through. + expect(isFirstUser).toBe(true); + }); + }); }); diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.ts b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.ts index 74ce7776d..cf6ada872 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.ts @@ -166,6 +166,7 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit { this.assignedSiteForm = this.fb.group({ useGoogleSheetAsDefault: new FormControl(this.data.useGoogleSheetAsDefault), useOnlyPlanHours: new FormControl(this.data.useOnlyPlanHours), + useOneMinuteIntervals: new FormControl(this.data.useOneMinuteIntervals ?? false), autoBreakCalculationActive: new FormControl(this.data.autoBreakCalculationActive), allowPersonalTimeRegistration: new FormControl(this.data.allowPersonalTimeRegistration), allowEditOfRegistrations: new FormControl(this.data.allowEditOfRegistrations), diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/i18n/da.ts b/eform-client/src/app/plugins/modules/time-planning-pn/i18n/da.ts index d2cba95c9..65556ab5a 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/i18n/da.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/i18n/da.ts @@ -311,6 +311,9 @@ export const da = { 'Mobile registration': 'Mobilregistrering', Shifts: 'Vagter', 'Payroll rules': 'Lønregler', + 'Advanced settings': 'Avancerede indstillinger', + 'Use 1-minute intervals': 'Brug 1-minutters intervaller', + 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.': 'Aktiver 1-minutters granularitet for tidsregistrering. Standardintervaller er 5 eller 15 minutter.', 'How working time is captured': 'Sådan registreres arbejdstiden', 'Manual entry': 'Manuel indtastning', 'Allow entry of forgotten days': 'Tillad indtastning af glemte dage', diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/i18n/enUS.ts b/eform-client/src/app/plugins/modules/time-planning-pn/i18n/enUS.ts index d53cccc23..a22cfe4ab 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/i18n/enUS.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/i18n/enUS.ts @@ -311,6 +311,9 @@ export const enUS = { 'Mobile registration': 'Mobile registration', 'Shifts': 'Shifts', 'Payroll rules': 'Payroll rules', + 'Advanced settings': 'Advanced settings', + 'Use 1-minute intervals': 'Use 1-minute intervals', + 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.': 'Enable 1-minute granularity for time entry. Default increments are 5 or 15 minutes.', 'Hide employee from daily operations. Requires a resignation date. All settings below are hidden.': 'Hide employee from daily operations. Requires a resignation date. All settings below are hidden.', 'Breaks are calculated automatically from the rule set. Manual break entry on shift tabs is disabled.': 'Breaks are calculated automatically from the rule set. Manual break entry on shift tabs is disabled.', 'Main switch. When active, select registration method and editing policy below.': 'Main switch. When active, select registration method and editing policy below.',