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 @@ -263,4 +263,48 @@ public async Task GetRegistrationSites_DeviceNull_SoftwareVersionIsValidFalse()

Assert.That(response.SoftwareVersionIsValid, Is.False);
}

// #1742: the tablet worker list shows when each worker clocked in today.
private static List<Infrastructure.Models.Settings.Site> StartedAndNotStartedSites() =>
[
new()
{
SiteId = 1,
ShiftStartedAt = new DateTime(2026, 10, 1, 6, 58, 0),
ResignedAtDate = DateTime.MinValue,
},
new()
{
SiteId = 2,
ResignedAtDate = DateTime.MinValue,
},
];

[Test]
public async Task GetRegistrationSites_MapsShiftStartedAt_AndSendsEmptyWhenNotStarted()
{
_settingService.GetAvailableSites(Arg.Any<string>())
.Returns(new OperationDataResult<List<Infrastructure.Models.Settings.Site>>(
true, "OK", StartedAndNotStartedSites()));

var response = await _grpcService.GetRegistrationSites(
new GetRegistrationSitesRequest { Token = "token" }, TestServerCallContextFactory.Create());

Assert.That(response.Model[0].ShiftStartedAt, Is.EqualTo("2026-10-01T06:58:00"));
Assert.That(response.Model[1].ShiftStartedAt, Is.Empty);
}

[Test]
public async Task GetRegistrationSitesByCurrentUser_MapsShiftStartedAt_AndSendsEmptyWhenNotStarted()
{
_settingService.GetAllRegistrationSitesByCurrentUser()
.Returns(new OperationDataResult<List<Infrastructure.Models.Settings.Site>>(
true, "OK", StartedAndNotStartedSites()));

var response = await _grpcService.GetRegistrationSitesByCurrentUser(
new GetRegistrationSitesByCurrentUserRequest(), TestServerCallContextFactory.Create());

Assert.That(response.Model[0].ShiftStartedAt, Is.EqualTo("2026-10-01T06:58:00"));
Assert.That(response.Model[1].ShiftStartedAt, Is.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -975,4 +975,116 @@ public async Task GetAvailableSitesByCurrentUser_ManagerWithMultipleTagsOnOwnSit
Assert.That(result.Model.Count, Is.EqualTo(1));
Assert.That(result.Model.Single().SiteId, Is.EqualTo(14000));
}

// --- ShiftStartedAt (#1742) ---
// The registration-device worker list shows when each worker clocked in
// today: the earliest StartNStartedAt of today's row, null when not started.

private async Task SeedStartedAndNotStartedWorkersAsync(int startedSiteUid, int notStartedSiteUid)
{
var core = await _coreService.GetCore();
var sdkDbContext = core.DbContextHelper.GetDbContext();
var language = await sdkDbContext.Languages.FirstOrDefaultAsync();
if (language == null)
{
language = new Microting.eForm.Infrastructure.Data.Entities.Language
{
LanguageCode = "da",
Name = "Danish"
};
await language.Create(sdkDbContext);
}

foreach (var siteUid in new[] { startedSiteUid, notStartedSiteUid })
{
var site = new Microting.eForm.Infrastructure.Data.Entities.Site
{
Name = $"Kiosk worker {siteUid}",
MicrotingUid = siteUid,
LanguageId = language.Id
};
await site.Create(sdkDbContext);
var worker = new Microting.eForm.Infrastructure.Data.Entities.Worker
{
FirstName = "Jane",
LastName = $"Doe {siteUid}",
Email = $"kiosk{siteUid}@example.com",
MicrotingUid = siteUid + 1000
};
await worker.Create(sdkDbContext);
await new Microting.eForm.Infrastructure.Data.Entities.SiteWorker
{
SiteId = site.Id,
WorkerId = worker.Id,
MicrotingUid = siteUid + 2000
}.Create(sdkDbContext);
await new AssignedSiteEntity
{
SiteId = siteUid,
CreatedByUserId = 1,
UpdatedByUserId = 1
}.Create(TimePlanningPnDbContext);
}

var today = DateTime.UtcNow.Date;
await new PlanRegistration
{
SdkSitId = startedSiteUid,
Date = today,
Start1StartedAt = today.AddHours(6).AddMinutes(58),
Stop1StoppedAt = today.AddHours(11),
Start2StartedAt = today.AddHours(12).AddMinutes(30),
CreatedByUserId = 1,
UpdatedByUserId = 1
}.Create(TimePlanningPnDbContext);
Comment on lines +1029 to +1039
// A plan row without a start: the worker has not clocked in.
await new PlanRegistration
{
SdkSitId = notStartedSiteUid,
Date = today,
PlanHours = 7.5,
CreatedByUserId = 1,
UpdatedByUserId = 1
}.Create(TimePlanningPnDbContext);
}

[Test]
public async Task GetAvailableSites_ReturnsEarliestShiftStartToday_AndNullWhenNotStarted()
{
// Arrange
await new RegistrationDevice
{
Token = "kiosk-token-1742",
CreatedByUserId = 1,
UpdatedByUserId = 1
}.Create(TimePlanningPnDbContext);
await SeedStartedAndNotStartedWorkersAsync(15000, 15001);

// Act
var result = await _settingsService.GetAvailableSites("kiosk-token-1742");

// Assert
Assert.That(result.Success, Is.True, result.Message);
var today = DateTime.UtcNow.Date;
Assert.That(result.Model.Single(x => x.SiteId == 15000).ShiftStartedAt,
Is.EqualTo(today.AddHours(6).AddMinutes(58)));
Comment on lines +1068 to +1070
Assert.That(result.Model.Single(x => x.SiteId == 15001).ShiftStartedAt, Is.Null);
}

[Test]
public async Task GetAllRegistrationSitesByCurrentUser_ReturnsEarliestShiftStartToday_AndNullWhenNotStarted()
{
// Arrange
await SeedStartedAndNotStartedWorkersAsync(15100, 15101);

// Act
var result = await _settingsService.GetAllRegistrationSitesByCurrentUser();

// Assert
Assert.That(result.Success, Is.True, result.Message);
var today = DateTime.UtcNow.Date;
Assert.That(result.Model.Single(x => x.SiteId == 15100).ShiftStartedAt,
Is.EqualTo(today.AddHours(6).AddMinutes(58)));
Assert.That(result.Model.Single(x => x.SiteId == 15101).ShiftStartedAt, Is.Null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public class Site
public string DefaultLanguage { get; set; }
public bool HoursStarted { get; set; }
public bool PauseStarted { get; set; }
/// <summary>
/// Earliest StartNStartedAt today, null when no shift has started (#1742).
/// Naive wall-clock time as stored, not UTC.
/// </summary>
public DateTime? ShiftStartedAt { get; set; }
public bool AutoBreakCalculationActive { get; set; }
public string AvatarUrl { get; set; }
public bool ThirdShiftActive { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ message Site {
string phone_number = 22;
bool use_one_minute_intervals = 23;
bool over_midnight = 24;
// Earliest StartNStartedAt today, in the same format as the other
// *_started_at strings (the worker's naive wall-clock time, no zone);
// empty when no shift has started.
string shift_started_at = 25;
}

// Response wrappers matching C# OperationDataResult<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@
PhoneNumber = site.PhoneNumber ?? "",
UseOneMinuteIntervals = site.UseOneMinuteIntervals,
OverMidnight = site.OverMidnight,
ShiftStartedAt = TimePlanningWorkingHoursGrpcService.FormatDateTime(site.ShiftStartedAt),
};
response.Model.Add(grpcSite);
}
Expand Down Expand Up @@ -339,6 +340,7 @@
PhoneNumber = site.PhoneNumber ?? "",
UseOneMinuteIntervals = site.UseOneMinuteIntervals,
OverMidnight = site.OverMidnight,
ShiftStartedAt = TimePlanningWorkingHoursGrpcService.FormatDateTime(site.ShiftStartedAt),
};
response.Model.Add(grpcSite);
}
Expand All @@ -347,7 +349,7 @@
return response;
}

private static Grpc.BreakDaySettings MapBreakDay(Infrastructure.Models.Settings.Day? day)

Check warning on line 352 in eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/GrpcServices/TimePlanningSettingsGrpcService.cs

View workflow job for this annotation

GitHub Actions / test-dotnet (a, FullyQualifiedName=TimePlanning.Pn.Test.AbsenceRequestServiceTests|FullyQualified...

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (day == null) return new Grpc.BreakDaySettings();
return new Grpc.BreakDaySettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@
private static long ToWholeSeconds(double hours) =>
(long)Math.Round(hours * 3600, MidpointRounding.AwayFromZero);

private static string FormatDateTime(DateTime? dt) =>
/// <summary>
/// The wire format of every *_started_at / *_stopped_at string: the stored
/// naive wall-clock time, no zone suffix; empty when unset.
/// </summary>
internal static string FormatDateTime(DateTime? dt) =>
dt?.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF") ?? "";
Comment on lines +189 to 190

private static Grpc.WorkingHoursModel MapToGrpc(
Expand Down Expand Up @@ -301,7 +305,7 @@
}

private static TimePlanningWorkingHoursUpdateModel MapFromGrpc(
Grpc.WorkingHoursModel? m, Grpc.DeviceMetadata? device)

Check warning on line 308 in eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/GrpcServices/TimePlanningWorkingHoursGrpcService.cs

View workflow job for this annotation

GitHub Actions / test-dotnet (a, FullyQualifiedName=TimePlanning.Pn.Test.AbsenceRequestServiceTests|FullyQualified...

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (m == null) return new TimePlanningWorkingHoursUpdateModel();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ planRegistrationForToday is
PinCode = worker.PinCode,
DefaultLanguage = language?.LanguageCode ?? "en",
HoursStarted = hoursStarted,
ShiftStartedAt = FirstShiftStartedAt(planRegistrationForToday),
PauseStarted = pauseStarted,
AutoBreakCalculationActive = assignedSite.AutoBreakCalculationActive,
ThirdShiftActive = assignedSite.ThirdShiftActive,
Expand Down Expand Up @@ -627,6 +628,7 @@ planRegistrationForToday is
PinCode = worker.PinCode,
DefaultLanguage = language?.LanguageCode ?? "en",
HoursStarted = hoursStarted,
ShiftStartedAt = FirstShiftStartedAt(planRegistrationForToday),
PauseStarted = pauseStarted,
AutoBreakCalculationActive = assignedSite.AutoBreakCalculationActive,
ThirdShiftActive = assignedSite.ThirdShiftActive,
Expand Down Expand Up @@ -1468,4 +1470,27 @@ await pushNotificationService.SendToSiteAsync(

return new OperationResult(true, localizationService.GetString("AssignedSiteUpdatedSuccessfuly"));
}
}

/// <summary>
/// When the worker first clocked in today: the earliest StartNStartedAt
/// of the day's row, or null when no shift has started (#1742). The
/// registration-device worker list shows it next to each worker.
/// </summary>
private static DateTime? FirstShiftStartedAt(PlanRegistration? planRegistration)
{
if (planRegistration == null)
{
return null;
}

// Min over DateTime? skips nulls, and is null when all are.
return new[]
{
planRegistration.Start1StartedAt,
planRegistration.Start2StartedAt,
planRegistration.Start3StartedAt,
planRegistration.Start4StartedAt,
planRegistration.Start5StartedAt
}.Min();
Comment on lines +1486 to +1494
}
}
Loading