Skip to content

Commit 2ddefc8

Browse files
renemadsenclaude
andcommitted
feat: wire PayRuleSet into AssignedSite and Excel export
- Add PayRuleSetId to AssignedSite API model and conversion operator - Save PayRuleSetId in UpdateAssignedSite service method - Add PayRuleSet dropdown (mtx-select) to AssignedSite dialog - Integrate PayLineGenerator into Excel export with dynamic pay code columns - Add PayRuleSetsService to module providers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a2f697 commit 2ddefc8

7 files changed

Lines changed: 156 additions & 12 deletions

File tree

eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Models/Settings/AssignedSite.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public class AssignedSite
168168
public bool GpsEnabled { get; set; }
169169
public bool SnapshotEnabled { get; set; }
170170
public bool IsManager { get; set; }
171+
public int? PayRuleSetId { get; set; }
171172
public List<int> ManagingTagIds { get; set; } = new List<int>();
172173
public AutoBreakSettings AutoBreakSettings { get; set; }
173174

@@ -327,6 +328,7 @@ public static implicit operator AssignedSite(
327328
GpsEnabled = model.GpsEnabled,
328329
SnapshotEnabled = model.SnapshotEnabled,
329330
IsManager = model.IsManager,
331+
PayRuleSetId = model.PayRuleSetId,
330332
ManagingTagIds = new List<int>(), // Loaded separately in GetAssignedSite
331333
};
332334
}

eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningSettingService/TimeSettingService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@ public async Task<OperationResult> UpdateAssignedSite(Infrastructure.Models.Sett
648648
dbAssignedSite.UsePunchClock = site.UsePunchClock;
649649
dbAssignedSite.UseDetailedPauseEditing = site.UseDetailedPauseEditing;
650650
dbAssignedSite.AutoBreakCalculationActive = site.AutoBreakCalculationActive;
651+
dbAssignedSite.PayRuleSetId = site.PayRuleSetId;
651652

652653
dbAssignedSite.StartMonday = site.StartMonday;
653654
dbAssignedSite.StartTuesday = site.StartTuesday;

eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningWorkingHoursService/TimePlanningWorkingHoursService.cs

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3636
using Sentry;
3737
using TimePlanning.Pn.Infrastructure.Helpers;
3838
using TimePlanning.Pn.Infrastructure.Data.Seed.Data;
39+
using Microting.TimePlanningBase.Infrastructure.Helpers;
3940
using TimePlanning.Pn.Infrastructure.Models.WorkingHours.UpdateCreate;
4041

4142
namespace TimePlanning.Pn.Services.TimePlanningWorkingHoursService;
@@ -2051,13 +2052,69 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
20512052

20522053
var isFifthShiftEnabled = assignedSite.FifthShiftActive;
20532054

2055+
// Load PayRuleSet with day rules and tiers for pay code generation
2056+
PayRuleSet payRuleSet = null;
2057+
if (assignedSite.PayRuleSetId.HasValue)
2058+
{
2059+
payRuleSet = await dbContext.PayRuleSets
2060+
.Include(p => p.DayRules)
2061+
.ThenInclude(d => d.Tiers)
2062+
.FirstOrDefaultAsync(p => p.Id == assignedSite.PayRuleSetId.Value);
2063+
}
2064+
20542065
Thread.CurrentThread.CurrentUICulture = new CultureInfo(language.LanguageCode);
20552066
var culture = new CultureInfo(language.LanguageCode);
20562067
Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "results"));
20572068

20582069
var timeStamp = $"{DateTime.UtcNow:yyyyMMdd_HHmmss}";
20592070
var filePath = Path.Combine(Path.GetTempPath(), "results", $"{timeStamp}_.xlsx");
20602071

2072+
// Fetch data early so we can pre-compute pay lines for header discovery
2073+
var content = await Index(model);
2074+
if (!content.Success) return new OperationDataResult<Stream>(false, content.Message);
2075+
2076+
// remove the first entry from the content.Model
2077+
var timePlannings = content.Model.Skip(1).ToList();
2078+
2079+
// Pre-compute pay lines for each day and collect unique pay codes
2080+
var payLinesByDate = new Dictionary<DateTime, List<PlanRegistrationPayLine>>();
2081+
var allPayCodes = new List<string>();
2082+
2083+
if (payRuleSet != null)
2084+
{
2085+
foreach (var planning in timePlannings)
2086+
{
2087+
var nettoHours = planning.NettoHoursOverrideActive
2088+
? planning.NettoHoursOverride
2089+
: planning.NettoHours;
2090+
var totalSeconds = (int)(nettoHours * 3600);
2091+
2092+
if (totalSeconds <= 0)
2093+
{
2094+
payLinesByDate[planning.Date] = new List<PlanRegistrationPayLine>();
2095+
continue;
2096+
}
2097+
2098+
var dayCode = GetDayCodeForDate(planning.Date);
2099+
var payLines = PayLineGenerator.GeneratePayLines(
2100+
planning.Id ?? 0,
2101+
dayCode,
2102+
totalSeconds,
2103+
payRuleSet,
2104+
DateTime.UtcNow);
2105+
2106+
payLinesByDate[planning.Date] = payLines;
2107+
2108+
foreach (var pl in payLines)
2109+
{
2110+
if (!allPayCodes.Contains(pl.PayCode))
2111+
{
2112+
allPayCodes.Add(pl.PayCode);
2113+
}
2114+
}
2115+
}
2116+
}
2117+
20612118
using (SpreadsheetDocument
20622119
document = SpreadsheetDocument.Create(filePath, SpreadsheetDocumentType.Workbook))
20632120
{
@@ -2132,6 +2189,12 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
21322189
headerStrings.Add(localizationService.GetString(header));
21332190
}
21342191

2192+
// Add pay code columns as dynamic headers
2193+
foreach (var payCode in allPayCodes)
2194+
{
2195+
headerStrings.Add(payCode);
2196+
}
2197+
21352198
Worksheet worksheet1 = new Worksheet()
21362199
{ MCAttributes = new MarkupCompatibilityAttributes() { Ignorable = "x14ac xr xr2 xr3" } };
21372200
worksheet1.AddNamespaceDeclaration("r",
@@ -2173,14 +2236,6 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
21732236

21742237
sheetData1.Append(row1);
21752238

2176-
// Fetch data
2177-
var content = await Index(model);
2178-
if (!content.Success) return new OperationDataResult<Stream>(false, content.Message);
2179-
2180-
// remove the first entry from the content.Model
2181-
var timePlannings = content.Model.Skip(1).ToList();
2182-
2183-
//var timePlannings = content.Model;
21842239
var plr = new PlanRegistration();
21852240

21862241
// Fill data
@@ -2189,11 +2244,26 @@ public async Task<OperationDataResult<Stream>> GenerateExcelDashboard(TimePlanni
21892244
{
21902245
var dataRow = new Row() { RowIndex = (uint)rowIndex };
21912246
FillDataRow(dataRow, worker, site, culture, planning, plr, language, isThirdShiftEnabled, isFourthShiftEnabled, isFifthShiftEnabled);
2247+
2248+
// Append pay code values for this day
2249+
if (allPayCodes.Count > 0)
2250+
{
2251+
var dayPayLines = payLinesByDate.ContainsKey(planning.Date)
2252+
? payLinesByDate[planning.Date]
2253+
: new List<PlanRegistrationPayLine>();
2254+
2255+
foreach (var payCode in allPayCodes)
2256+
{
2257+
var payLine = dayPayLines.FirstOrDefault(pl => pl.PayCode == payCode);
2258+
dataRow.Append(CreateNumericCell(payLine?.Hours ?? 0));
2259+
}
2260+
}
2261+
21922262
sheetData1.Append(dataRow);
21932263
rowIndex++;
21942264
}
21952265

2196-
var columnLetter = GetColumnLetter(headers.Length);
2266+
var columnLetter = GetColumnLetter(headerStrings.Count);
21972267
AutoFilter autoFilter1 = new AutoFilter() { Reference = $"A1:{columnLetter}{rowIndex}" };
21982268
autoFilter1.SetAttribute(new OpenXmlAttribute("xr", "uid",
21992269
"http://schemas.microsoft.com/office/spreadsheetml/2014/revision",
@@ -3133,4 +3203,35 @@ private string GetColumnLetter(int columnIndex)
31333203

31343204
return columnLetter;
31353205
}
3206+
3207+
/// <summary>
3208+
/// Classify the date and return the day code for pay rule matching.
3209+
/// Returns: SUNDAY, SATURDAY, HOLIDAY, GRUNDLOVSDAG, or WEEKDAY
3210+
/// </summary>
3211+
private static string GetDayCodeForDate(DateTime date)
3212+
{
3213+
// Check if it's Grundlovsdag (June 5th) - highest priority
3214+
if (date.Month == 6 && date.Day == 5)
3215+
{
3216+
return "GRUNDLOVSDAG";
3217+
}
3218+
3219+
// Check against holiday configuration for official holidays
3220+
if (PlanRegistrationHelper.IsOfficialHoliday(date))
3221+
{
3222+
return "HOLIDAY";
3223+
}
3224+
3225+
if (date.DayOfWeek == DayOfWeek.Sunday)
3226+
{
3227+
return "SUNDAY";
3228+
}
3229+
3230+
if (date.DayOfWeek == DayOfWeek.Saturday)
3231+
{
3232+
return "SATURDAY";
3233+
}
3234+
3235+
return "WEEKDAY";
3236+
}
31363237
}

eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@
196196
</mat-form-field>
197197
</div>
198198

199+
<!-- Pay Rule Set selector -->
200+
<div class="d-flex flex-row" *ngIf="!data.resigned && selectCurrentUserIsAdmin$ | async">
201+
<mat-form-field class="p-1 w-100">
202+
<mat-label>{{ 'Pay Rule Set' | translate }}</mat-label>
203+
<mtx-select
204+
[items]="availablePayRuleSets"
205+
bindLabel="name"
206+
bindValue="id"
207+
[multiple]="false"
208+
[clearable]="true"
209+
placeholder="{{ 'None' | translate }}"
210+
formControlName="payRuleSetId">
211+
</mtx-select>
212+
<mat-hint>{{ 'Select the pay rule set for this worker' | translate }}</mat-hint>
213+
</mat-form-field>
214+
</div>
215+
199216
<div class="d-flex flex-row">
200217
<mat-checkbox class="p-1"
201218
[id]="'resigned'"

eform-client/src/app/plugins/modules/time-planning-pn/components/plannings/time-planning-actions/assigned-site/assigned-site-dialog.component.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {Component, DoCheck, OnInit,
44
import {
55
MAT_DIALOG_DATA
66
} from '@angular/material/dialog';
7-
import {AssignedSiteModel, CommonTagModel, GlobalAutoBreakSettingsModel} from '../../../../models';
7+
import {AssignedSiteModel, CommonTagModel, GlobalAutoBreakSettingsModel, PayRuleSetSimpleModel} from '../../../../models';
88
import {selectCurrentUserIsAdmin, selectCurrentUserIsFirstUser} from 'src/app/state';
99
import {Store} from '@ngrx/store';
10-
import {TimePlanningPnSettingsService} from 'src/app/plugins/modules/time-planning-pn/services';
10+
import {TimePlanningPnSettingsService, TimePlanningPnPayRuleSetsService} from 'src/app/plugins/modules/time-planning-pn/services';
1111
import {
1212
AbstractControl,
1313
FormBuilder,
@@ -29,6 +29,7 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
2929
private fb = inject(FormBuilder);
3030
public data = inject<AssignedSiteModel>(MAT_DIALOG_DATA);
3131
private timePlanningPnSettingsService = inject(TimePlanningPnSettingsService);
32+
private payRuleSetsService = inject(TimePlanningPnPayRuleSetsService);
3233
private store = inject(Store);
3334

3435
assignedSiteForm!: FormGroup;
@@ -38,6 +39,7 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
3839
private previousData: AssignedSiteModel;
3940
private globalAutoBreakSettings: GlobalAutoBreakSettingsModel;
4041
public availableTags: CommonTagModel[] = [];
42+
public availablePayRuleSets: PayRuleSetSimpleModel[] = [];
4143

4244
ngDoCheck(): void {
4345
if (this.hasDataChanged()) {
@@ -58,6 +60,9 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
5860
// Load available tags from eForm core API via service
5961
this.loadAvailableTags();
6062

63+
// Load available pay rule sets
64+
this.loadPayRuleSets();
65+
6166
if (!this.data.resigned) {
6267
const today = new Date();
6368
today.setHours(0, 0, 0, 0);
@@ -178,6 +183,7 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
178183
),
179184
isManager: new FormControl(this.data.isManager ?? false),
180185
managingTagIds: new FormControl(this.data.managingTagIds ?? []),
186+
payRuleSetId: new FormControl(this.data.payRuleSetId ?? null),
181187
planHours: this.fb.group(planHoursGroup),
182188
autoBreakSettings: this.fb.group(autoBreakGroup),
183189
firstShift: this.fb.group(firstShiftGroup),
@@ -569,6 +575,20 @@ export class AssignedSiteDialogComponent implements DoCheck, OnInit {
569575
return this.assignedSiteForm.get('fifthShift') as FormGroup;
570576
}
571577

578+
loadPayRuleSets(): void {
579+
this.payRuleSetsService.getPayRuleSets({offset: 0, pageSize: 1000}).subscribe({
580+
next: (result) => {
581+
if (result && result.success) {
582+
this.availablePayRuleSets = result.model?.payRuleSets || [];
583+
}
584+
},
585+
error: (error) => {
586+
console.error('Error loading pay rule sets:', error);
587+
this.availablePayRuleSets = [];
588+
}
589+
});
590+
}
591+
572592
loadAvailableTags(): void {
573593
this.timePlanningPnSettingsService.getAvailableTags().subscribe({
574594
next: (result) => {

eform-client/src/app/plugins/modules/time-planning-pn/models/assigned-sites/assigned-site.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,5 @@ export class AssignedSiteModel {
163163
daysBackInTimeAllowedEditing: number;
164164
isManager: boolean;
165165
managingTagIds: number[];
166+
payRuleSetId: number | null;
166167
}

eform-client/src/app/plugins/modules/time-planning-pn/time-planning-pn.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {TimePlanningPnLayoutComponent} from './layouts';
1010
import {
1111
TimePlanningPnPlanningsService, TimePlanningPnRegistrationDevicesService,
1212
TimePlanningPnSettingsService,
13-
TimePlanningPnFlexesService
13+
TimePlanningPnFlexesService,
14+
TimePlanningPnPayRuleSetsService
1415
} from './services';
1516
import {
1617
TimePlanningsContainerComponent, TimePlanningSettingsComponent, TimePlanningsTableComponent,
@@ -105,6 +106,7 @@ import {MtxSelect} from '@ng-matero/extensions/select';
105106
TimePlanningPnPlanningsService,
106107
TimePlanningPnRegistrationDevicesService,
107108
TimePlanningPnFlexesService,
109+
TimePlanningPnPayRuleSetsService,
108110
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { subscriptSizing: 'dynamic' } }
109111
],
110112
})

0 commit comments

Comments
 (0)