feat(planning): follow the indicative wait in calculation progress
The overlay percentage now rises linearly to 95% at the expected wait captured at Calculate, holds there, and shows 100% only when the planner completes. Beneath the status, one line names the current planning stage (with the rule name for rule stages) and another shows the approximate remaining time, or "Taking longer than expected" once the wait is exceeded, announced once through the live status. Cancel freezes the percentage and hides both lines. PlanningCalculationService reports the stage and submitted request instead of fixed per-stage percentages; PlanningDurationService owns the wait progress on a 250 ms tick. Closes #300 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35a63d24c4
commit
58be749949
14 files changed
+529
-45
No files matched your search
@@ -8,6 +8,7 @@ import type {
|
||||
PlanningExplanation,
|
||||
PlanningOutcome,
|
||||
PlanningReason,
|
||||
PlanningRequest,
|
||||
PlanningSnapshot,
|
||||
} from './leave-planner.service';
|
||||
import { RuleService } from './rule-service';
|
||||
@@ -16,10 +17,14 @@ import { UserInputService } from './user-input-service';
|
||||
|
||||
type Horizon = { start: string; end: string };
|
||||
|
||||
interface PendingCalculation {
|
||||
export interface PendingCalculation {
|
||||
id: number;
|
||||
horizon: Horizon;
|
||||
progress: number;
|
||||
/** The configuration submitted at Calculate, unaffected by later form edits. */
|
||||
request: PlanningRequest;
|
||||
requestedAt: number;
|
||||
/** The latest planning stage reported by the planner, or 'complete'. */
|
||||
stage: string;
|
||||
cancelling: boolean;
|
||||
}
|
||||
|
||||
@@ -34,22 +39,6 @@ export interface AcceptedCalculation {
|
||||
successful: boolean;
|
||||
}
|
||||
|
||||
function stageProgress(stage: string, current: number): number {
|
||||
if (stage === 'complete') return 100;
|
||||
if (stage === 'validation') return 5;
|
||||
if (stage === 'obligations') return 20;
|
||||
if (stage === 'at-risk') return 35;
|
||||
if (stage.startsWith('preference:') || stage.startsWith('optimization:')) return 50;
|
||||
if (stage === 'holiday-week-value') return 58;
|
||||
if (stage === 'non-working-day-gain') return 65;
|
||||
if (stage === 'longest-work-sequence') return 72;
|
||||
if (stage === 'short-sequence-deficit') return 79;
|
||||
if (stage === 'supporting-placements') return 86;
|
||||
if (stage === 'expiry') return 93;
|
||||
if (stage === 'dates') return 98;
|
||||
return current;
|
||||
}
|
||||
|
||||
function plannerError(error: unknown): PlanningReason {
|
||||
return {
|
||||
code: 'planner-error',
|
||||
@@ -64,7 +53,7 @@ function explanationFailure(error: unknown): PlanningReason {
|
||||
};
|
||||
}
|
||||
|
||||
/** Single owner of planning calculations: submission, progress, cancellation and the accepted result. */
|
||||
/** Single owner of planning calculations: submission, planning stages, cancellation and the accepted result. */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PlanningCalculationService {
|
||||
private readonly settings = inject(CalendarSettingsService);
|
||||
@@ -107,7 +96,7 @@ export class PlanningCalculationService {
|
||||
readonly pendingId = computed(() => this.pendingState()?.id ?? null);
|
||||
readonly isCalculating = computed(() => this.pendingState() !== null);
|
||||
readonly isCancelling = computed(() => this.pendingState()?.cancelling ?? false);
|
||||
readonly progress = computed(() => this.pendingState()?.progress ?? 0);
|
||||
readonly pending = this.pendingState.asReadonly();
|
||||
readonly outcome = computed(() => this.acceptedState()?.outcome ?? null);
|
||||
readonly snapshot = computed(() => this.acceptedState()?.snapshot ?? null);
|
||||
readonly displayedPlan = computed(() => {
|
||||
@@ -131,7 +120,9 @@ export class PlanningCalculationService {
|
||||
this.pendingState.set({
|
||||
id: version,
|
||||
horizon: request.horizon,
|
||||
progress: 0,
|
||||
request,
|
||||
requestedAt,
|
||||
stage: 'preparation',
|
||||
cancelling: false,
|
||||
});
|
||||
let calculation: PlanningCalculationContext;
|
||||
@@ -147,12 +138,8 @@ export class PlanningCalculationService {
|
||||
),
|
||||
progress: (stage) => {
|
||||
if (version === this.version)
|
||||
this.pendingState.update(
|
||||
(pending) =>
|
||||
pending && {
|
||||
...pending,
|
||||
progress: stageProgress(stage, pending.progress),
|
||||
},
|
||||
this.pendingState.update((pending) =>
|
||||
pending && !pending.cancelling ? { ...pending, stage } : pending,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user