diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Planning/TimePlanningPlanningPrDayModel.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Planning/TimePlanningPlanningPrDayModel.cs index 1cbb8bfc..acb0c009 100644 --- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Planning/TimePlanningPlanningPrDayModel.cs +++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Planning/TimePlanningPlanningPrDayModel.cs @@ -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; } diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs index 699d2d1e..8d9482fa 100644 --- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs +++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs @@ -964,6 +964,25 @@ public async Task 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); + } + } + } + if (!assignedSite.UseOnlyPlanHours) { double minutesPlanned = 0; @@ -1876,6 +1895,137 @@ private void EnsureTimestampsFromIds(PlanRegistration planning) } } + /// + /// 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. + /// + private void ApplyExactMinutePause(PlanRegistration planning, int shift, int exactMinutes) + { + if (exactMinutes == 0) + { + ClearPauseTimestamps(planning, shift); + return; + } + + 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> GetVersionHistory(int planRegistrationId) { try diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/workday-entity/workday-entity-dialog.component.ts b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/workday-entity/workday-entity-dialog.component.ts index f6e5ebfc..47534895 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/workday-entity/workday-entity-dialog.component.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/workday-entity/workday-entity-dialog.component.ts @@ -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; @@ -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)]}, @@ -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; + } + } + 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; @@ -1319,6 +1398,9 @@ 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); + } this.data.planningPrDayModels.stop1Id = this.convertTimeToMinutes(a1?.stop, true, true); this.data.planningPrDayModels.stop1StoppedAt = this.convertTimeToDateTimeOfToday(a1?.stop === '00:00' ? '24:00' : a1?.stop); @@ -1326,6 +1408,9 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy { 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); @@ -1333,6 +1418,9 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy { 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); + } this.data.planningPrDayModels.stop3Id = this.convertTimeToMinutes(a3?.stop, true, true); this.data.planningPrDayModels.stop3StoppedAt = this.convertTimeToDateTimeOfToday(a3?.stop === '00:00' ? '24:00' : a3?.stop); @@ -1340,6 +1428,9 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy { 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); @@ -1347,6 +1438,9 @@ export class WorkdayEntityDialogComponent implements OnInit, OnDestroy { 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); diff --git a/eform-client/src/app/plugins/modules/time-planning-pn/models/plannings/planning-pr-day.model.ts b/eform-client/src/app/plugins/modules/time-planning-pn/models/plannings/planning-pr-day.model.ts index 633a3938..0ccbfec3 100644 --- a/eform-client/src/app/plugins/modules/time-planning-pn/models/plannings/planning-pr-day.model.ts +++ b/eform-client/src/app/plugins/modules/time-planning-pn/models/plannings/planning-pr-day.model.ts @@ -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; }