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 @@ -95,6 +95,18 @@ public class TimePlanningPlanningPrDayModel
public int? Stop5Id { get; set; }

public int? Pause5Id { get; set; }

// Request-only: exact-minute pause durations under UseOneMinuteIntervals=true.
// Null means the client did not send the new field; backend falls back to the
// legacy Pause*Id (5-minute slot) write path. When set, backend translates the
// duration into Pause*StartedAt/Pause*StoppedAt timestamp pairs (anchor: existing
// Pause*StartedAt if present, else shift midpoint).
public int? Pause1ExactMinutes { get; set; }
public int? Pause2ExactMinutes { get; set; }
public int? Pause3ExactMinutes { get; set; }
public int? Pause4ExactMinutes { get; set; }
public int? Pause5ExactMinutes { get; set; }

public int Break1Shift { get; set; }
public int Break2Shift { get; set; }
public int Break3Shift { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,25 @@ public async Task<OperationResult> Update(int id, TimePlanningPlanningPrDayModel
planning.Start1StartedAt = null;
}

if (assignedSite.UseOneMinuteIntervals)
{
var exactPauses = new[]
{
(1, model.Pause1ExactMinutes),
(2, model.Pause2ExactMinutes),
(3, model.Pause3ExactMinutes),
(4, model.Pause4ExactMinutes),
(5, model.Pause5ExactMinutes),
};
foreach (var (shift, minutes) in exactPauses)
{
if (minutes.HasValue)
{
ApplyExactMinutePause(planning, shift, minutes.Value);
}
Comment on lines +971 to +982
}
}
Comment on lines +967 to +984

if (!assignedSite.UseOnlyPlanHours)
{
double minutesPlanned = 0;
Expand Down Expand Up @@ -1876,6 +1895,137 @@ private void EnsureTimestampsFromIds(PlanRegistration planning)
}
}

/// <summary>
/// Translates an admin-edited exact-minute pause duration for the given shift
/// into Pause*StartedAt/Pause*StoppedAt timestamps on the entity. Anchors to
/// the existing Pause*StartedAt when present (preserves the worker's actual
/// pause start) and falls back to the shift midpoint when no anchor exists.
/// Sub-slot pauses (pause10..pause19, pause100..pause102 for shift 1;
/// pause20..pause29, pause200..pause202 for shift 2) are cleared so
/// AggregatePauseMinutes does not double-count.
/// </summary>
private void ApplyExactMinutePause(PlanRegistration planning, int shift, int exactMinutes)
{
if (exactMinutes == 0)
{
ClearPauseTimestamps(planning, shift);
return;
}

Comment on lines +1907 to +1914
DateTime? existingStart = shift switch
{
1 => planning.Pause1StartedAt,
2 => planning.Pause2StartedAt,
3 => planning.Pause3StartedAt,
4 => planning.Pause4StartedAt,
5 => planning.Pause5StartedAt,
_ => null,
};

DateTime startedAt;
if (existingStart.HasValue)
{
startedAt = existingStart.Value;
}
else
{
var (shiftStart, shiftStop) = GetShiftBounds(planning, shift);
if (!shiftStart.HasValue || !shiftStop.HasValue)
{
// No anchor available — skip the write rather than fabricate one.
return;
}
startedAt = shiftStart.Value.AddMinutes(
(shiftStop.Value - shiftStart.Value).TotalMinutes / 2);
}

var stoppedAt = startedAt.AddMinutes(exactMinutes);

ClearPauseTimestamps(planning, shift);

switch (shift)
{
case 1:
planning.Pause1StartedAt = startedAt;
planning.Pause1StoppedAt = stoppedAt;
break;
case 2:
planning.Pause2StartedAt = startedAt;
planning.Pause2StoppedAt = stoppedAt;
break;
case 3:
planning.Pause3StartedAt = startedAt;
planning.Pause3StoppedAt = stoppedAt;
break;
case 4:
planning.Pause4StartedAt = startedAt;
planning.Pause4StoppedAt = stoppedAt;
break;
case 5:
planning.Pause5StartedAt = startedAt;
planning.Pause5StoppedAt = stoppedAt;
break;
}
}

private static void ClearPauseTimestamps(PlanRegistration planning, int shift)
{
switch (shift)
{
case 1:
planning.Pause1StartedAt = null; planning.Pause1StoppedAt = null;
planning.Pause10StartedAt = null; planning.Pause10StoppedAt = null;
planning.Pause11StartedAt = null; planning.Pause11StoppedAt = null;
planning.Pause12StartedAt = null; planning.Pause12StoppedAt = null;
planning.Pause13StartedAt = null; planning.Pause13StoppedAt = null;
planning.Pause14StartedAt = null; planning.Pause14StoppedAt = null;
planning.Pause15StartedAt = null; planning.Pause15StoppedAt = null;
planning.Pause16StartedAt = null; planning.Pause16StoppedAt = null;
planning.Pause17StartedAt = null; planning.Pause17StoppedAt = null;
planning.Pause18StartedAt = null; planning.Pause18StoppedAt = null;
planning.Pause19StartedAt = null; planning.Pause19StoppedAt = null;
planning.Pause100StartedAt = null; planning.Pause100StoppedAt = null;
planning.Pause101StartedAt = null; planning.Pause101StoppedAt = null;
planning.Pause102StartedAt = null; planning.Pause102StoppedAt = null;
break;
case 2:
planning.Pause2StartedAt = null; planning.Pause2StoppedAt = null;
planning.Pause20StartedAt = null; planning.Pause20StoppedAt = null;
planning.Pause21StartedAt = null; planning.Pause21StoppedAt = null;
planning.Pause22StartedAt = null; planning.Pause22StoppedAt = null;
planning.Pause23StartedAt = null; planning.Pause23StoppedAt = null;
planning.Pause24StartedAt = null; planning.Pause24StoppedAt = null;
planning.Pause25StartedAt = null; planning.Pause25StoppedAt = null;
planning.Pause26StartedAt = null; planning.Pause26StoppedAt = null;
planning.Pause27StartedAt = null; planning.Pause27StoppedAt = null;
planning.Pause28StartedAt = null; planning.Pause28StoppedAt = null;
planning.Pause29StartedAt = null; planning.Pause29StoppedAt = null;
planning.Pause200StartedAt = null; planning.Pause200StoppedAt = null;
planning.Pause201StartedAt = null; planning.Pause201StoppedAt = null;
planning.Pause202StartedAt = null; planning.Pause202StoppedAt = null;
break;
case 3:
planning.Pause3StartedAt = null; planning.Pause3StoppedAt = null;
break;
case 4:
planning.Pause4StartedAt = null; planning.Pause4StoppedAt = null;
break;
case 5:
planning.Pause5StartedAt = null; planning.Pause5StoppedAt = null;
break;
}
}

private static (DateTime?, DateTime?) GetShiftBounds(PlanRegistration p, int shift) => shift switch
{
1 => (p.Start1StartedAt, p.Stop1StoppedAt),
2 => (p.Start2StartedAt, p.Stop2StoppedAt),
3 => (p.Start3StartedAt, p.Stop3StoppedAt),
4 => (p.Start4StartedAt, p.Stop4StoppedAt),
5 => (p.Start5StartedAt, p.Stop5StoppedAt),
_ => (null, null),
};

public async Task<OperationDataResult<PlanRegistrationVersionHistoryModel>> GetVersionHistory(int planRegistrationId)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,26 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy {
const stop5StoppedAt = this.datePipe.transform(this.data.planningPrDayModels.stop5StoppedAt, 'HH:mm', 'UTC');
const pause5Id = this.convertMinutesToTime(this.data.planningPrDayModels.pause5Id * 5);

// Phase 4: under UseOneMinuteIntervals, derive the pause duration from the
// sum of all Pause*StartedAt/Pause*StoppedAt timestamp pairs in seconds and
// round to the nearest minute. When the flag is off, fall back to the legacy
// 5-minute-slot value so flag-off behavior stays bit-identical.
const pause1Exact = this.useOneMinuteIntervals
? this.convertMinutesToTime(this.computeExactPauseMinutes(1))
: pause1Id;
const pause2Exact = this.useOneMinuteIntervals
? this.convertMinutesToTime(this.computeExactPauseMinutes(2))
: pause2Id;
const pause3Exact = this.useOneMinuteIntervals
? this.convertMinutesToTime(this.computeExactPauseMinutes(3))
: pause3Id;
const pause4Exact = this.useOneMinuteIntervals
? this.convertMinutesToTime(this.computeExactPauseMinutes(4))
: pause4Id;
const pause5Exact = this.useOneMinuteIntervals
? this.convertMinutesToTime(this.computeExactPauseMinutes(5))
: pause5Id;

// Er dato i fremtiden?
this.isInTheFuture = Date.parse(this.data.planningPrDayModels.date) > Date.now();
this.todaysFlex = this.data.planningPrDayModels.actualHours - this.data.planningPrDayModels.planHours;
Expand Down Expand Up @@ -226,34 +246,34 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy {
actual: this.fb.group({
shift1: this.fb.group({
start: new FormControl({value: start1StartedAt, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause1Id, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause1Exact, disabled: this.isInTheFuture}),
stop: new FormControl({value: stop1StoppedAt, disabled: this.isInTheFuture}),
},
{validators: [this.actualShiftDurationValidator.bind(this)]},),
shift2: this.fb.group({
start: new FormControl({value: start2StartedAt, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause2Id, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause2Exact, disabled: this.isInTheFuture}),
stop: new FormControl({value: stop2StoppedAt, disabled: this.isInTheFuture}),
},
{validators: [this.actualShiftDurationValidator.bind(this)]},
),
shift3: this.fb.group({
start: new FormControl({value: start3StartedAt, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause3Id, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause3Exact, disabled: this.isInTheFuture}),
stop: new FormControl({value: stop3StoppedAt, disabled: this.isInTheFuture}),
},
{validators: [this.actualShiftDurationValidator.bind(this)]},
),
shift4: this.fb.group({
start: new FormControl({value: start4StartedAt, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause4Id, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause4Exact, disabled: this.isInTheFuture}),
stop: new FormControl({value: stop4StoppedAt, disabled: this.isInTheFuture}),
},
{validators: [this.actualShiftDurationValidator.bind(this)]},
),
shift5: this.fb.group({
start: new FormControl({value: start5StartedAt, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause5Id, disabled: this.isInTheFuture}),
pause: new FormControl({value: pause5Exact, disabled: this.isInTheFuture}),
stop: new FormControl({value: stop5StoppedAt, disabled: this.isInTheFuture}),
},
{validators: [this.actualShiftDurationValidator.bind(this)]},
Expand Down Expand Up @@ -1026,6 +1046,65 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy {
return hours * 60 + minutes;
}

// Sum every Pause*StartedAt/Pause*StoppedAt pair attached to the given shift in
// seconds, then round to the nearest minute. Sub-slot pauses (pause10..pause19,
// pause100..pause102 for shift 1; pause20..pause29, pause200..pause202 for
// shift 2) accumulate into the same display value so the admin sees the full
// pause duration the worker actually had.
private computeExactPauseMinutes(shift: number): number {
let totalSeconds = 0;
for (const [start, stop] of this.getPauseTimestampPairs(shift)) {
if (start && stop) {
totalSeconds += (new Date(stop).getTime() - new Date(start).getTime()) / 1000;
}
Comment on lines +1054 to +1059
}
return Math.round(totalSeconds / 60);
}

private getPauseTimestampPairs(shift: number): Array<[string | null, string | null]> {
const m = this.data.planningPrDayModels;
if (shift === 1) {
return [
[m.pause1StartedAt, m.pause1StoppedAt],
[m.pause10StartedAt, m.pause10StoppedAt],
[m.pause11StartedAt, m.pause11StoppedAt],
[m.pause12StartedAt, m.pause12StoppedAt],
[m.pause13StartedAt, m.pause13StoppedAt],
[m.pause14StartedAt, m.pause14StoppedAt],
[m.pause15StartedAt, m.pause15StoppedAt],
[m.pause16StartedAt, m.pause16StoppedAt],
[m.pause17StartedAt, m.pause17StoppedAt],
[m.pause18StartedAt, m.pause18StoppedAt],
[m.pause19StartedAt, m.pause19StoppedAt],
[m.pause100StartedAt, m.pause100StoppedAt],
[m.pause101StartedAt, m.pause101StoppedAt],
[m.pause102StartedAt, m.pause102StoppedAt],
];
}
if (shift === 2) {
return [
[m.pause2StartedAt, m.pause2StoppedAt],
[m.pause20StartedAt, m.pause20StoppedAt],
[m.pause21StartedAt, m.pause21StoppedAt],
[m.pause22StartedAt, m.pause22StoppedAt],
[m.pause23StartedAt, m.pause23StoppedAt],
[m.pause24StartedAt, m.pause24StoppedAt],
[m.pause25StartedAt, m.pause25StoppedAt],
[m.pause26StartedAt, m.pause26StoppedAt],
[m.pause27StartedAt, m.pause27StoppedAt],
[m.pause28StartedAt, m.pause28StoppedAt],
[m.pause29StartedAt, m.pause29StoppedAt],
[m.pause200StartedAt, m.pause200StoppedAt],
[m.pause201StartedAt, m.pause201StoppedAt],
[m.pause202StartedAt, m.pause202StoppedAt],
];
}
if (shift === 3) { return [[m.pause3StartedAt, m.pause3StoppedAt]]; }
if (shift === 4) { return [[m.pause4StartedAt, m.pause4StoppedAt]]; }
if (shift === 5) { return [[m.pause5StartedAt, m.pause5StoppedAt]]; }
return [];
}

private toMinutes(hhmm: string | null): number | null {
if (!hhmm) {
return null;
Expand Down Expand Up @@ -1319,34 +1398,49 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy {
this.data.planningPrDayModels.start1StartedAt = this.convertTimeToDateTimeOfToday(a1?.start);
// eslint-disable-next-line max-len
this.data.planningPrDayModels.pause1Id = this.convertTimeToMinutes(a1?.pause, true) === 0 ? null : this.convertTimeToMinutes(a1?.pause, true);
if (this.useOneMinuteIntervals) {
this.data.planningPrDayModels.pause1ExactMinutes = this.convertTimeToMinutes(a1?.pause, false);
}
Comment on lines 1400 to +1403
this.data.planningPrDayModels.stop1Id = this.convertTimeToMinutes(a1?.stop, true, true);
this.data.planningPrDayModels.stop1StoppedAt = this.convertTimeToDateTimeOfToday(a1?.stop === '00:00' ? '24:00' : a1?.stop);

this.data.planningPrDayModels.start2Id = this.convertTimeToMinutes(a2?.start, true);
this.data.planningPrDayModels.start2StartedAt = this.convertTimeToDateTimeOfToday(a2?.start);
// eslint-disable-next-line max-len
this.data.planningPrDayModels.pause2Id = this.convertTimeToMinutes(a2?.pause, true) === 0 ? null : this.convertTimeToMinutes(a2?.pause, true);
if (this.useOneMinuteIntervals) {
this.data.planningPrDayModels.pause2ExactMinutes = this.convertTimeToMinutes(a2?.pause, false);
}
this.data.planningPrDayModels.stop2Id = this.convertTimeToMinutes(a2?.stop, true, true);
this.data.planningPrDayModels.stop2StoppedAt = this.convertTimeToDateTimeOfToday(a2?.stop === '00:00' ? '24:00' : a2?.stop);

this.data.planningPrDayModels.start3Id = this.convertTimeToMinutes(a3?.start, true);
this.data.planningPrDayModels.start3StartedAt = this.convertTimeToDateTimeOfToday(a3?.start);
// eslint-disable-next-line max-len
this.data.planningPrDayModels.pause3Id = this.convertTimeToMinutes(a3?.pause, true) === 0 ? null : this.convertTimeToMinutes(a3?.pause, true);
if (this.useOneMinuteIntervals) {
this.data.planningPrDayModels.pause3ExactMinutes = this.convertTimeToMinutes(a3?.pause, false);
}
Comment on lines 1420 to +1423
this.data.planningPrDayModels.stop3Id = this.convertTimeToMinutes(a3?.stop, true, true);
this.data.planningPrDayModels.stop3StoppedAt = this.convertTimeToDateTimeOfToday(a3?.stop === '00:00' ? '24:00' : a3?.stop);

this.data.planningPrDayModels.start4Id = this.convertTimeToMinutes(a4?.start, true);
this.data.planningPrDayModels.start4StartedAt = this.convertTimeToDateTimeOfToday(a4?.start);
// eslint-disable-next-line max-len
this.data.planningPrDayModels.pause4Id = this.convertTimeToMinutes(a4?.pause, true) === 0 ? null : this.convertTimeToMinutes(a4?.pause, true);
if (this.useOneMinuteIntervals) {
this.data.planningPrDayModels.pause4ExactMinutes = this.convertTimeToMinutes(a4?.pause, false);
}
this.data.planningPrDayModels.stop4Id = this.convertTimeToMinutes(a4?.stop, true, true);
this.data.planningPrDayModels.stop4StoppedAt = this.convertTimeToDateTimeOfToday(a4?.stop === '00:00' ? '24:00' : a4?.stop);

this.data.planningPrDayModels.start5Id = this.convertTimeToMinutes(a5?.start, true);
this.data.planningPrDayModels.start5StartedAt = this.convertTimeToDateTimeOfToday(a5?.start);
// eslint-disable-next-line max-len
this.data.planningPrDayModels.pause5Id = this.convertTimeToMinutes(a5?.pause, true) === 0 ? null : this.convertTimeToMinutes(a5?.pause, true);
if (this.useOneMinuteIntervals) {
this.data.planningPrDayModels.pause5ExactMinutes = this.convertTimeToMinutes(a5?.pause, false);
}
this.data.planningPrDayModels.stop5Id = this.convertTimeToMinutes(a5?.stop, true, true);
this.data.planningPrDayModels.stop5StoppedAt = this.convertTimeToDateTimeOfToday(a5?.stop === '00:00' ? '24:00' : a5?.stop);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ export class PlanningPrDayModel {
start5Id: number;
stop5Id: number;
pause5Id: number;
// Request-only: exact-minute pause durations sent under UseOneMinuteIntervals=true.
// Backend translates these into Pause*StartedAt/Pause*StoppedAt timestamp pairs.
pause1ExactMinutes?: number;
pause2ExactMinutes?: number;
pause3ExactMinutes?: number;
pause4ExactMinutes?: number;
pause5ExactMinutes?: number;
nettoHoursOverride: number;
nettoHoursOverrideActive: boolean;
}
Loading