Evaluate explicit, ordered optimization configurations through one shared planner model. Add six editable presets, durable one-time legacy migration, and consistent completed and cancelled plan provenance. Include functional regressions and the agreed trace-removal and benchmark follow-up specs. Performance acceptance remains in #78 under unchanged gates. Refs: #70, #71, #72, #73, #74, #75, #76, #77, #78
510 lines
16 KiB
HTML
510 lines
16 KiB
HTML
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
|
|
<mat-expansion-panel-header>
|
|
<mat-panel-title>
|
|
<mat-icon aria-hidden="true">rule</mat-icon>
|
|
<span>{{ tr().rules.title }}</span>
|
|
<span class="rule-count">{{ activeRuleCount }} {{ tr().rules.active }}</span>
|
|
</mat-panel-title>
|
|
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
|
|
</mat-expansion-panel-header>
|
|
|
|
<div class="priority-statement">
|
|
<mat-icon aria-hidden="true">info_outline</mat-icon>
|
|
<span>{{ tr().rules.priorityInfo }}</span>
|
|
</div>
|
|
|
|
<div class="rules-list" cdkDropList (cdkDropListDropped)="drop($event)">
|
|
@for (rule of rules; track rule.id; let i = $index) {
|
|
<div
|
|
class="rule-item"
|
|
cdkDrag
|
|
cdkDragLockAxis="y"
|
|
[cdkDragStartDelay]="{ touch: 180, mouse: 0 }"
|
|
[cdkDragDisabled]="editingRuleId() === rule.id"
|
|
[class.editing]="editingRuleId() === rule.id"
|
|
>
|
|
@if (editingRuleId() !== rule.id) {
|
|
<div class="rule-display">
|
|
<div class="rule-header">
|
|
<div
|
|
class="rule-drag-grip"
|
|
cdkDragHandle
|
|
[title]="tr().rules.dragRule"
|
|
aria-hidden="true"
|
|
>
|
|
<mat-icon>drag_indicator</mat-icon>
|
|
</div>
|
|
<mat-slide-toggle
|
|
[checked]="rule.enabled"
|
|
(change)="toggleRule(rule.id)"
|
|
color="primary"
|
|
>
|
|
<span class="rule-name"
|
|
><span class="rule-number">{{ i + 1 }}.</span>
|
|
{{ rule.name }}</span
|
|
>
|
|
</mat-slide-toggle>
|
|
</div>
|
|
<div class="rule-summary">
|
|
<span
|
|
class="rule-force"
|
|
[class.obligation]="rule.force === 'obligation'"
|
|
>
|
|
{{
|
|
rule.force === 'obligation'
|
|
? tr().rules.obligation
|
|
: tr().rules.preference
|
|
}}
|
|
</span>
|
|
<span [innerHTML]="getRuleSummary(rule)"></span>
|
|
</div>
|
|
<div class="rule-actions">
|
|
<button
|
|
mat-icon-button
|
|
[disabled]="i === 0"
|
|
[aria-label]="tr().rules.moveRuleUp"
|
|
(click)="moveRule(i, -1, $event)"
|
|
>
|
|
<mat-icon aria-hidden="true">keyboard_arrow_up</mat-icon>
|
|
</button>
|
|
<button
|
|
mat-icon-button
|
|
[disabled]="i === rules.length - 1"
|
|
[aria-label]="tr().rules.moveRuleDown"
|
|
(click)="moveRule(i, 1, $event)"
|
|
>
|
|
<mat-icon aria-hidden="true">keyboard_arrow_down</mat-icon>
|
|
</button>
|
|
<button
|
|
mat-icon-button
|
|
[aria-label]="tr().rules.editRule"
|
|
(click)="startEdit(rule, $event)"
|
|
>
|
|
<mat-icon>edit</mat-icon>
|
|
</button>
|
|
<button
|
|
mat-icon-button
|
|
color="warn"
|
|
[aria-label]="tr().rules.deleteRule"
|
|
(click)="deleteRule(rule.id, $event)"
|
|
>
|
|
<mat-icon>delete</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<div class="rule-editor">
|
|
<h4>{{ tr().rules.editRule }}</h4>
|
|
|
|
<mat-form-field class="full-width">
|
|
<mat-label>{{ tr().rules.ruleName }}</mat-label>
|
|
<input matInput [(ngModel)]="$any(editBuffer).name" required />
|
|
</mat-form-field>
|
|
<mat-form-field class="full-width">
|
|
<mat-label>{{ tr().rules.force }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).force">
|
|
@for (option of forceOptions; track option.value) {
|
|
<mat-option [value]="option.value">{{
|
|
option.label()
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
@if ($any(editBuffer).type === RuleType.IF_THEN) {
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
|
|
@for (day of daysOfWeek; track day.value) {
|
|
<mat-option [value]="day.value">{{
|
|
day.label
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.place }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).placeDays"
|
|
min="1"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
|
@for (type of placeDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.position }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placement">
|
|
@for (opt of placementOptions; track opt.value) {
|
|
<mat-option [value]="opt.value">{{
|
|
opt.label
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.getBonus }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).rewardDays"
|
|
min="0"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
|
|
@for (type of rewardDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
} @else if ($any(editBuffer).type === RuleType.OPTIMIZATION) {
|
|
<ng-container [ngTemplateOutlet]="optimizationEditor" />
|
|
} @else {
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.startDate }}</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="startPicker"
|
|
[(ngModel)]="$any(editBuffer).startDate"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="startPicker"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #startPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.endDate }}</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="endPicker"
|
|
[(ngModel)]="$any(editBuffer).endDate"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="endPicker"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #endPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.place }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).placeDays"
|
|
min="1"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
|
@for (type of placeDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
}
|
|
|
|
@if (validationError()) {
|
|
<p role="alert">{{ validationError() }}</p>
|
|
}
|
|
<div class="editor-actions">
|
|
<button mat-button (click)="cancelEdit()">
|
|
{{ tr().rules.cancel }}
|
|
</button>
|
|
<button mat-flat-button color="primary" (click)="saveEdit()">
|
|
{{ tr().rules.save }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (isAddingNew()) {
|
|
<div class="rule-item editing">
|
|
<div class="rule-editor">
|
|
<h4>{{ tr().rules.addRule }}</h4>
|
|
|
|
<mat-form-field class="full-width">
|
|
<mat-label>{{ tr().rules.ruleName }}</mat-label>
|
|
<input matInput [(ngModel)]="$any(editBuffer).name" required />
|
|
</mat-form-field>
|
|
<mat-form-field class="full-width">
|
|
<mat-label>{{ tr().rules.force }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).force">
|
|
@for (option of forceOptions; track option.value) {
|
|
<mat-option [value]="option.value">{{ option.label() }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
@if (newRuleType() === RuleType.IF_THEN) {
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
|
|
@for (day of daysOfWeek; track day.value) {
|
|
<mat-option [value]="day.value">{{ day.label }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.place }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).placeDays"
|
|
min="1"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
|
@for (type of placeDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.position }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placement">
|
|
@for (opt of placementOptions; track opt.value) {
|
|
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.getBonus }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).rewardDays"
|
|
min="0"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
|
|
@for (type of rewardDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
} @else if ($any(editBuffer).type === RuleType.OPTIMIZATION) {
|
|
<ng-container [ngTemplateOutlet]="optimizationEditor" />
|
|
} @else {
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.startDate }}</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="startPicker"
|
|
[(ngModel)]="$any(editBuffer).startDate"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="startPicker"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #startPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.endDate }}</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="endPicker"
|
|
[(ngModel)]="$any(editBuffer).endDate"
|
|
/>
|
|
<mat-datepicker-toggle
|
|
matSuffix
|
|
[for]="endPicker"
|
|
></mat-datepicker-toggle>
|
|
<mat-datepicker #endPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<div class="editor-row">
|
|
<span class="action-text">{{ tr().rules.place }}</span>
|
|
<mat-form-field class="small-input">
|
|
<mat-label>{{ tr().rules.days }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
[(ngModel)]="$any(editBuffer).placeDays"
|
|
min="1"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.type }}</mat-label>
|
|
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
|
@for (type of placeDayTypes; track type) {
|
|
<mat-option [value]="type">{{
|
|
getDayTypeLabel(type)
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
}
|
|
|
|
@if (validationError()) {
|
|
<p role="alert">{{ validationError() }}</p>
|
|
}
|
|
<div class="editor-actions">
|
|
<button mat-button (click)="cancelEdit()">{{ tr().rules.cancel }}</button>
|
|
<button mat-flat-button color="primary" (click)="saveEdit()">
|
|
{{ tr().rules.add }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="add-buttons" [class.hidden]="isAddingNew() || editingRuleId()">
|
|
<button mat-stroked-button (click)="startAddNew(RuleType.IF_THEN)">
|
|
<mat-icon>add</mat-icon> {{ tr().rules.newIfThenRule }}
|
|
</button>
|
|
<button mat-stroked-button (click)="startAddNew(RuleType.TIME_WINDOW)">
|
|
<mat-icon>add</mat-icon> {{ tr().rules.newTimeWindowRule }}
|
|
</button>
|
|
<button mat-stroked-button (click)="startAddNew(RuleType.OPTIMIZATION)">
|
|
<mat-icon aria-hidden="true">add</mat-icon> {{ tr().rules.optimization.newRule }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</mat-expansion-panel>
|
|
|
|
<ng-template #optimizationEditor>
|
|
@if (optimizationConfiguration; as configuration) {
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.optimization.elementsLabel }}</mat-label>
|
|
<mat-select
|
|
[ngModel]="configuration.elements"
|
|
(ngModelChange)="changeOptimizationElements($event)"
|
|
>
|
|
@for (elements of optimizationElements; track elements) {
|
|
<mat-option [value]="elements">{{
|
|
tr().rules.optimization.elements[elements]
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.optimization.measureLabel }}</mat-label>
|
|
<mat-select
|
|
[ngModel]="configuration.measure"
|
|
(ngModelChange)="changeOptimizationMeasure($event)"
|
|
>
|
|
@for (row of optimizationMeasures; track row.measure) {
|
|
<mat-option [value]="row.measure">{{
|
|
tr().rules.optimization.measures[row.measure]
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
@if (optimizationConditions.length) {
|
|
<mat-form-field class="full-width">
|
|
<mat-label>{{ tr().rules.optimization.conditionsLabel }}</mat-label>
|
|
<mat-select multiple [(ngModel)]="configuration.conditions">
|
|
@for (condition of optimizationConditions; track condition) {
|
|
<mat-option [value]="condition">{{
|
|
tr().rules.optimization.conditions[condition]
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
}
|
|
<div class="editor-row">
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.optimization.aggregationLabel }}</mat-label>
|
|
<mat-select [(ngModel)]="configuration.aggregation">
|
|
@for (aggregation of optimizationAggregations; track aggregation) {
|
|
<mat-option [value]="aggregation">{{
|
|
tr().rules.optimization.aggregations[aggregation]
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.optimization.directionLabel }}</mat-label>
|
|
<mat-select [(ngModel)]="configuration.direction">
|
|
@for (direction of optimizationDirections; track direction) {
|
|
<mat-option [value]="direction">{{
|
|
tr().rules.optimization.directions[direction]
|
|
}}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
@if (configuration.measure === 'work-sequence-deficit') {
|
|
<mat-form-field>
|
|
<mat-label>{{ tr().rules.optimization.thresholdLabel }}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
min="1"
|
|
step="1"
|
|
required
|
|
[(ngModel)]="configuration.thresholdDays"
|
|
/>
|
|
</mat-form-field>
|
|
}
|
|
</div>
|
|
}
|
|
</ng-template>
|