Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -780,4 +780,101 @@ public void Index_FlagOn_DerivedFieldsConsistent()
// Assert.That(row.SumFlexEnd, Is.EqualTo(row.SumFlexEndInSeconds / 3600.0).Within(0.0001));
Assert.Pass("Captured for future fixture work; see XML doc above.");
}

// ------------------------------------------------------------------
// Phase 4 — web admin display + Excel export: HH:mm:ss when flag on
// ------------------------------------------------------------------
//
// The fork lives in a private helper on
// TimePlanningWorkingHoursService.GetShiftTime(plr, shift, actualStamp, useOneMinuteIntervals)
// which is called from FillDataRow when emitting Excel cells. The
// existing 2-arg overload (Options[] lookup) is left untouched; the new
// 4-arg overload formats `actualStamp` as `HH:mm:ss` when both
// `useOneMinuteIntervals` and `actualStamp.HasValue` are true, and falls
// through to the 2-arg overload otherwise.
//
// Because the helper is `private` and the test fixture has no
// `InternalsVisibleTo` attribute, the contract is captured here as the
// documented carve-out (mirroring the Phase 0/1/2 patterns above) so
// that the assertion intent survives any future refactor that exposes
// the helper for direct testing.

/// <summary>
/// Phase 4 contract: when <c>UseOneMinuteIntervals</c> is on AND a
/// precise <c>actualStamp</c> is supplied, the new
/// <c>GetShiftTime(plr, shift, actualStamp, useOneMinuteIntervals)</c>
/// overload returns the stamp formatted as <c>HH:mm:ss</c> rather than
/// the legacy 5-minute <c>plr.Options[shift - 1]</c> lookup.
/// </summary>
[Test]
[Ignore("Phase 4 carve-out: GetShiftTime is private on TimePlanningWorkingHoursService and the fixture has no InternalsVisibleTo; assertion captured for future fixture work.")]
public void GetShiftTime_FlagOnWithActualStamp_ReturnsHHmmss()
{
// Arrange / Act / Assert (intent, to be enabled when the helper is
// exposed via InternalsVisibleTo or extracted to a testable seam):
//
// var plr = new PlanRegistration
// {
// Start1Id = 84, // 5-min idx 84 → "07:00" via Options[83]
// Start1StartedAt = new DateTime(2026, 5, 15, 7, 3, 53),
// };
// var resultFlagOn = service.GetShiftTimeForTest(plr, plr.Start1Id, plr.Start1StartedAt, useOneMinuteIntervals: true);
// var resultFlagOff = service.GetShiftTimeForTest(plr, plr.Start1Id, plr.Start1StartedAt, useOneMinuteIntervals: false);
//
// Assert.That(resultFlagOn, Is.EqualTo("07:03:53"));
// Assert.That(resultFlagOff, Is.EqualTo("07:00")); // Options[83] = legacy snap
Assert.Pass("Captured for future fixture work; see XML doc above.");
}

/// <summary>
/// Phase 4 contract: the Excel dashboard export
/// (<c>GenerateExcelDashboard</c>) emits <c>HH:mm:ss</c> string cells
/// for shift start/stop columns when the row's <c>AssignedSite</c> has
/// <c>UseOneMinuteIntervals</c> on, sourced from the precise
/// <c>Start1StartedAt</c>/<c>Stop1StoppedAt</c> stamps. With the flag
/// off, cell values must remain byte-identical to the legacy 5-minute
/// <c>Options[]</c> lookup.
/// </summary>
[Test]
[Ignore("Phase 4 carve-out: GenerateExcelDashboard requires the full Excel/SDK/DB fixture not wired here; assertion captured for future fixture work.")]
public void GenerateExcelDashboard_FlagOn_FormatsShiftCellsAsHHmmss()
{
// Arrange / Act / Assert (intent, to be enabled when fixture lands):
//
// var assignedSite = new AssignedSite { SiteId = 950, UseOneMinuteIntervals = true };
// var planning = new PlanRegistration
// {
// SdkSitId = 950,
// Date = new DateTime(2026, 5, 15, 0, 0, 0),
// Start1Id = 84,
// Stop1Id = 187,
// Start1StartedAt = new DateTime(2026, 5, 15, 7, 3, 53),
// Stop1StoppedAt = new DateTime(2026, 5, 15, 15, 30, 11),
// PlanHours = 8.0,
// };
// await assignedSite.Create(TimePlanningPnDbContext);
// await planning.Create(TimePlanningPnDbContext);
//
// var stream = await service.GenerateExcelDashboard(new TimePlanningWorkingHoursRequestModel
// {
// SiteId = 950,
// DateFrom = new DateTime(2026, 5, 15, 0, 0, 0),
// DateTo = new DateTime(2026, 5, 15, 0, 0, 0),
// });
//
// using var pkg = new ExcelPackage(stream);
// var sheet = pkg.Workbook.Worksheets.First();
// // Shift1Start column is the 8th cell on data row 2 (1-indexed).
// Assert.That(sheet.Cells[2, 8].Text, Is.EqualTo("07:03:53"));
// Assert.That(sheet.Cells[2, 9].Text, Is.EqualTo("15:30:11"));
//
// // Flip the flag back off → cells must show the legacy 5-min Options[] strings.
// assignedSite.UseOneMinuteIntervals = false;
// await assignedSite.Update(TimePlanningPnDbContext);
// var stream2 = await service.GenerateExcelDashboard(...);
// using var pkg2 = new ExcelPackage(stream2);
// Assert.That(pkg2.Workbook.Worksheets.First().Cells[2, 8].Text, Is.EqualTo("07:00"));
// Assert.That(pkg2.Workbook.Worksheets.First().Cells[2, 9].Text, Is.EqualTo("15:30"));
Assert.Pass("Captured for future fixture work; see XML doc above.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,13 @@ public class TimePlanningPlanningModel
public string DeviceModel { get; set; }
public string DeviceManufacturer { get; set; }
public bool SoftwareVersionIsValid { get; set; }
/// <summary>
/// Phase 4: per-row mirror of <c>AssignedSite.UseOneMinuteIntervals</c>.
/// The web admin plannings table reads this to decide whether to display
/// actual stamps (<c>start1StartedAt</c> etc.) at <c>HH:mm:ss</c> rather
/// than the legacy <c>HH:mm</c>. Default <c>false</c> preserves byte-
/// identical behavior on rows whose site has the flag off.
/// </summary>
public bool UseOneMinuteIntervals { get; set; }
public List<TimePlanningPlanningPrDayModel> PlanningPrDayModels { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ await dbContext.AssignedSites
{
SiteId = dbAssignedSite.SiteId,
SiteName = site.Name,
// Phase 4: per-row mirror of the assigned-site flag drives the
// web admin's HH:mm vs HH:mm:ss display path in the plannings table.
UseOneMinuteIntervals = dbAssignedSite.UseOneMinuteIntervals,
PlanningPrDayModels = new List<TimePlanningPlanningPrDayModel>()
};

Expand Down Expand Up @@ -451,6 +454,9 @@ public async Task<OperationDataResult<TimePlanningPlanningModel>> IndexByCurrent
{
SiteId = (int)site.MicrotingUid!,
SiteName = site.Name,
// Phase 4: per-row mirror of the assigned-site flag drives the
// web admin's HH:mm vs HH:mm:ss display path in the plannings table.
UseOneMinuteIntervals = dbAssignedSite.UseOneMinuteIntervals,
PlanningPrDayModels = new List<TimePlanningPlanningPrDayModel>()
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2532,7 +2532,7 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
foreach (var planning in timePlannings)
{
var dataRow = new Row() { RowIndex = (uint)rowIndex };
FillDataRow(dataRow, worker, site, culture, planning, plr, language, isThirdShiftEnabled, isFourthShiftEnabled, isFifthShiftEnabled);
FillDataRow(dataRow, worker, site, culture, planning, plr, language, isThirdShiftEnabled, isFourthShiftEnabled, isFifthShiftEnabled, assignedSite.UseOneMinuteIntervals);

// Append pay code values for this day
var dayPayLines = payLinesByDate.ContainsKey(planning.Date)
Expand Down Expand Up @@ -2651,7 +2651,7 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
}

private void FillDataRow(Row dataRow, Worker worker, Microting.eForm.Infrastructure.Data.Entities.Site site, CultureInfo culture,
TimePlanningWorkingHoursModel planning, PlanRegistration plr, Language language, bool isThirdShiftEnabled, bool isFourthShiftEnabled, bool isFifthShiftEnabled)
TimePlanningWorkingHoursModel planning, PlanRegistration plr, Language language, bool isThirdShiftEnabled, bool isFourthShiftEnabled, bool isFifthShiftEnabled, bool useOneMinuteIntervals = false)
{
try {
dataRow.Append(CreateCell(worker.EmployeeNo ?? string.Empty));
Expand All @@ -2661,12 +2661,16 @@ private void FillDataRow(Row dataRow, Worker worker, Microting.eForm.Infrastruct
dataRow.Append(CreateWeekNumberCell(planning.Date));
dataRow.Append(CreateCell(planning.PlanText));
dataRow.Append(CreateNumericCell(planning.PlanHours));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Start)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Stop)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Pause)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Start)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Stop)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Pause)));
// Phase 4: when UseOneMinuteIntervals is on, format actual stamps (start/stop)
// from the precise DateTime stamps with second precision; pause columns have
// no single representative stamp in the legacy 5-min Options[] view, so they
// pass actualStamp=null and fall through to the existing 2-arg lookup.
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Start, planning.Start1StartedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Stop, planning.Stop1StoppedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift1Pause, null, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Start, planning.Start2StartedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Stop, planning.Stop2StoppedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift2Pause, null, useOneMinuteIntervals)));
dataRow.Append(CreateNumericCell(planning.NettoHoursOverrideActive ? planning.NettoHoursOverride : planning.NettoHours));
dataRow.Append(CreateNumericCell(planning.FlexHours));
dataRow.Append(CreateNumericCell(planning.SumFlexEnd));
Expand All @@ -2678,21 +2682,21 @@ private void FillDataRow(Row dataRow, Worker worker, Microting.eForm.Infrastruct
dataRow.Append(CreateCell(planning.CommentOffice?.Replace("<br>", "\n")));
if (isThirdShiftEnabled)
{
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Start)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Stop)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Pause)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Start, planning.Start3StartedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Stop, planning.Stop3StoppedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift3Pause, null, useOneMinuteIntervals)));
}
if (isFourthShiftEnabled)
{
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Start)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Stop)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Pause)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Start, planning.Start4StartedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Stop, planning.Stop4StoppedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift4Pause, null, useOneMinuteIntervals)));
}
if (isFifthShiftEnabled)
{
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Start)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Stop)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Pause)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Start, planning.Start5StartedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Stop, planning.Stop5StoppedAt, useOneMinuteIntervals)));
dataRow.Append(CreateCell(GetShiftTime(plr, planning.Shift5Pause, null, useOneMinuteIntervals)));
}
}
catch (Exception ex)
Expand Down Expand Up @@ -2753,6 +2757,24 @@ private string GetShiftTime(PlanRegistration plr, int? shift)
return shift > 0 ? plr.Options[(int)shift - 1] : "";
}

/// <summary>
/// Phase 4 second-precision overload: when <paramref name="useOneMinuteIntervals"/>
/// is on AND a precise <paramref name="actualStamp"/> is available, format the
/// stamp as <c>HH:mm:ss</c> directly (sourcing the value from
/// <c>PlanRegistration.Start1StartedAt</c> / <c>Stop1StoppedAt</c> / etc.
/// instead of the legacy 5-minute <c>plr.Options</c> lookup). For every
/// other case (flag off OR no actual stamp) this delegates to the existing
/// 2-arg method so the byte-identical 5-minute path is preserved.
/// </summary>
private string GetShiftTime(PlanRegistration plr, int? shift, DateTime? actualStamp, bool useOneMinuteIntervals)
{
if (useOneMinuteIntervals && actualStamp.HasValue)
{
return actualStamp.Value.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
}
return GetShiftTime(plr, shift);
}

private string GetMessageText(int? messageId, Language language)
{
if (messageId == null) return string.Empty;
Expand Down Expand Up @@ -3155,7 +3177,7 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(
var dataRow = new Row() { RowIndex = (uint)rowIndex };
try
{
FillDataRow(dataRow, worker, site, culture, planning, plr, language, isThirdShiftEnabled, isFourthShiftEnabled, isFifthShiftEnabled);
FillDataRow(dataRow, worker, site, culture, planning, plr, language, isThirdShiftEnabled, isFourthShiftEnabled, isFifthShiftEnabled, cache?.AssignedSite?.UseOneMinuteIntervals ?? false);

// Append pay code values for this day
var dayPayLines = (cache != null && cache.PayLinesByDate.ContainsKey(planning.Date))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,45 @@ test.describe('Dashboard — multi-shift (3-5) round-trip regression guard', ()

await page.locator('#cancelButton').click();
});

/**
* Phase 4 — second-precision DISPLAY: when a row's site has
* `useOneMinuteIntervals = true` AND the planning has a precise
* `start1StartedAt` stamp (e.g. 07:03:53), the plannings-table cell must
* render the stamp at HH:mm:ss instead of the legacy HH:mm.
*
* Server-side seeding `AssignedSite.UseOneMinuteIntervals = true` plus
* a planning with `Start1StartedAt = 2026-05-15 07:03:53` requires DB
* fixture work the CI playwright shard doesn't yet wire up (the tests
* here log in as admin and rely on the default seed). Captured here as
* a TODO so the assertion shape survives any future fixture work; the
* Phase 4 jest unit test on `formatStamp(...)` covers the contract for
* the merge-blocking path.
*/
test.skip('plannings-table renders HH:mm:ss for actual stamp when site flag is on', async ({ page }) => {
// TODO(phase 4 fixture): seed AssignedSite.UseOneMinuteIntervals = true
// for the worker referenced by #cell3_0 AND a PlanRegistration row with
// Start1StartedAt = '2026-05-15T07:03:53Z' on a date that lands inside
// the dashboard's default visible range.
//
// Then the assertion shape is:
//
// await page.locator('mat-nested-tree-node').filter({ hasText: 'Timeregistrering' }).click();
// await page.locator('mat-tree-node').filter({ hasText: 'Dashboard' }).click();
// await waitForSpinner(page);
//
// const cellId = '#cell3_0';
// await page.locator(cellId).scrollIntoViewIfNeeded();
//
// // The first-shift actual line is rendered with id firstShiftActual{rowIdx}_{colField}.
// const firstShiftActual = page.locator('[id^="firstShiftActual"]').first();
// await expect(firstShiftActual).toContainText('07:03:53');
// // Negative guard — the legacy 5-min path would render '07:00' / '07:05' instead.
// await expect(firstShiftActual).not.toContainText(/07:0[05]\s/);
//
// Until the fixture lands the unit test
// `formatStamp (Phase 4) — uses HH:mm:ss format when row.useOneMinuteIntervals is true`
// covers the format-helper contract (eform-client/src/app/plugins/modules/time-planning-pn/
// components/plannings/time-plannings-table/time-plannings-table.component.spec.ts).
});
});
Loading
Loading