feat(planning): make optimization rules composable

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
This commit is contained in:
nessar committed 2026-10-10 16:07:34 +02:00
1 parent 209bf05f36
commit 6811142299
45 files changed
+8795 -3075

No files matched your search

@@ -183,6 +183,8 @@
</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>
@@ -239,6 +241,9 @@
</div>
}
@if (validationError()) {
<p role="alert">{{ validationError() }}</p>
}
<div class="editor-actions">
<button mat-button (click)="cancelEdit()">
{{ tr().rules.cancel }}
@@ -338,6 +343,8 @@
</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>
@@ -394,6 +401,9 @@
</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()">
@@ -411,6 +421,89 @@
<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>