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 @@
+
+
+
+
+
+
+
+
{{ '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.',