From 5ab29ef7700cb45e321469e2b5f69678a5761fff Mon Sep 17 00:00:00 2001
From: TO
Date: Tue, 18 Nov 2025 00:14:01 +0100
Subject: [PATCH 1/6] feat: enhance loading spinner with progress indication
and add warning for unplaced vacation days
---
.../calendar-year/calendar-year.html | 5 +-
.../calendar-year/calendar-year.scss | 45 +++++++------
.../components/calendar-year/calendar-year.ts | 11 +++-
src/app/components/user-input/user-input.html | 11 ++++
src/app/components/user-input/user-input.scss | 10 +++
src/app/components/user-input/user-input.ts | 15 ++++-
src/app/services/calendar-service.ts | 65 ++++++++++++++++++-
src/app/services/user-input-service.ts | 5 ++
8 files changed, 140 insertions(+), 27 deletions(-)
diff --git a/src/app/components/calendar-year/calendar-year.html b/src/app/components/calendar-year/calendar-year.html
index 6d75550..a62a2a6 100644
--- a/src/app/components/calendar-year/calendar-year.html
+++ b/src/app/components/calendar-year/calendar-year.html
@@ -2,7 +2,10 @@
@if (isLoading()) {
-
+
+
+
{{ loadingProgress() }}%
+
Calcul en cours...
diff --git a/src/app/components/calendar-year/calendar-year.scss b/src/app/components/calendar-year/calendar-year.scss
index 2309a59..53a1e65 100644
--- a/src/app/components/calendar-year/calendar-year.scss
+++ b/src/app/components/calendar-year/calendar-year.scss
@@ -51,25 +51,32 @@
}
}
+.spinner-container {
+ position: relative;
+ display: inline-block;
+
+ ::ng-deep .mat-mdc-progress-spinner {
+ // Override Material spinner size
+ width: 100px !important;
+ height: 100px !important;
+ }
+
+ .spinner-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 20px;
+ font-weight: bold;
+ color: #333;
+ pointer-events: none;
+
+ @media (prefers-color-scheme: dark) {
+ color: #ddd;
+ }
+ }
+}
+
.card {
margin: 16px;
}
-
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
-
-:root {
- @include mat.progress-spinner-overrides(
- (
- size: 0.5rem,
- active-indicator-width: 0.25rem,
- )
- );
-}
diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts
index 121b5c4..2923d38 100644
--- a/src/app/components/calendar-year/calendar-year.ts
+++ b/src/app/components/calendar-year/calendar-year.ts
@@ -21,6 +21,7 @@ export class CalendarYear {
private calendarSettingsService = inject(CalendarSettingsService);
protected isLoading = signal(true);
+ protected loadingProgress = signal(0);
// Use DateCacheService instead of creating Date objects
private get currentMonth() {
@@ -39,15 +40,21 @@ export class CalendarYear {
console.log('Samedi Malin setting:', c);
console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true);
+ this.loadingProgress.set(0);
// Simulate server-side calculation with a small delay
- setTimeout(() => {
+ setTimeout(async () => {
if (this.selectedDates) {
// Create a copy to avoid mutating the original signal data
- this.selectedDates.optimizeVacations(
+ const remainingDays = await this.selectedDates.optimizeVacations(
{ ...vacationData },
this.calendarSettingsService,
this.calendarService,
+ (progress: number) => {
+ this.loadingProgress.set(progress);
+ },
);
+ // Update the remaining vacation days signal
+ this.userInput.remainingVacationDaysSignal.set(remainingDays);
}
this.isLoading.set(false);
}, 1);
diff --git a/src/app/components/user-input/user-input.html b/src/app/components/user-input/user-input.html
index 5327327..7a7a50f 100644
--- a/src/app/components/user-input/user-input.html
+++ b/src/app/components/user-input/user-input.html
@@ -14,5 +14,16 @@
+ @if (hasRemainingDays()) {
+
+
+ warning
+
+ {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non
+ placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement.
+
+
+
+ }
diff --git a/src/app/components/user-input/user-input.scss b/src/app/components/user-input/user-input.scss
index c2ac840..739410f 100644
--- a/src/app/components/user-input/user-input.scss
+++ b/src/app/components/user-input/user-input.scss
@@ -13,6 +13,16 @@
font-size: 0.875rem;
margin-top: 0.25rem;
}
+
+ .warning-card {
+ margin-bottom: 1rem;
+ }
+
+ .warning-content {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
}
form {
diff --git a/src/app/components/user-input/user-input.ts b/src/app/components/user-input/user-input.ts
index dd29741..38ca38e 100644
--- a/src/app/components/user-input/user-input.ts
+++ b/src/app/components/user-input/user-input.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, OnDestroy, effect } from '@angular/core';
+import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@@ -6,6 +6,8 @@ import { UserInputService } from '../../services/user-input-service';
import { VacationsNumber } from '../../services/calendar-service';
import { debounceTime, takeUntil, Subject } from 'rxjs';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
+import { MatCard, MatCardContent } from '@angular/material/card';
+import { MatIcon } from '@angular/material/icon';
@Component({
selector: 'app-user-input',
@@ -15,7 +17,9 @@ import { CalendarSettings } from '../calendar-settings/calendar-settings';
MatInputModule,
ReactiveFormsModule,
CalendarSettings,
- CalendarSettings,
+ MatCard,
+ MatCardContent,
+ MatIcon,
],
templateUrl: './user-input.html',
styleUrl: './user-input.scss',
@@ -44,6 +48,13 @@ export class UserInput implements OnInit, OnDestroy {
Others: new FormControl(null),
});
+ protected remainingDays = computed(() => {
+ const remaining = this.userInputService.remainingVacationDaysSignal();
+ return remaining.cp + remaining.rtt + remaining.other;
+ });
+
+ protected hasRemainingDays = computed(() => this.remainingDays() > 0);
+
ngOnInit() {
this.userInputForm.valueChanges
.pipe(debounceTime(1500), takeUntil(this.destroy$))
diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts
index f4a3141..0fc9d53 100644
--- a/src/app/services/calendar-service.ts
+++ b/src/app/services/calendar-service.ts
@@ -298,6 +298,7 @@ export class SelectedDates implements SelectedDateInterface {
// Constant to indicate no valid strategy was found
private static readonly NO_VALID_STRATEGY_HEURISTIC = -1;
+ private static readonly MAX_DAYS_WITH_SAME_HEURISTIC = 9;
update(): void {
this.datesSelected = this._datesSelected.slice();
@@ -501,7 +502,7 @@ export class SelectedDates implements SelectedDateInterface {
// Check if adjacent (next day) - this is the best case!
if (nextStartTime - currentEndTime === oneDayInMs) {
- totalScore += 10; // Big bonus for adjacency
+ totalScore += 15; // Big bonus for adjacency
} else if (nextStartTime > currentEndTime) {
// Penalty based on days between the two dates
const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1;
@@ -535,18 +536,38 @@ export class SelectedDates implements SelectedDateInterface {
return totalScore;
}
- optimizeVacations(
+ async optimizeVacations(
vacationsNumber: VacationsNumber,
calendarSettingsService: CalendarSettingsService,
calendarService: CalendarService,
- ): void {
+ progressCallback?: (progress: number) => void,
+ ): Promise {
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
+
+ const initialVacations = { ...vacationsNumber };
+ const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;
+ let processedDays = 0;
+
+ // Helper function to update progress
+ const updateProgress = async () => {
+ if (progressCallback && totalDays > 0) {
+ const progress = Math.round((processedDays / totalDays) * 100);
+ progressCallback(progress);
+ // Allow UI to update with a microtask
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ };
+
+ await updateProgress();
+
while (vacationsNumber.cp > 0) {
const result = this.lookForVacation(DayType.CP, calendarService);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
break;
}
vacationsNumber.cp -= 1;
+ processedDays++;
+ await updateProgress();
}
while (vacationsNumber.rtt > 0) {
const result = this.lookForVacation(DayType.RTT, calendarService);
@@ -554,6 +575,8 @@ export class SelectedDates implements SelectedDateInterface {
break;
}
vacationsNumber.rtt -= 1;
+ processedDays++;
+ await updateProgress();
}
while (vacationsNumber.other > 0) {
const result = this.lookForVacation(DayType.OTHER, calendarService);
@@ -561,7 +584,25 @@ export class SelectedDates implements SelectedDateInterface {
break;
}
vacationsNumber.other -= 1;
+ processedDays++;
+ await updateProgress();
}
+
+ // Final progress update (100%)
+ if (progressCallback) {
+ progressCallback(100);
+ }
+
+ // Inform user if not all days were placed
+ const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other;
+ if (remainingDays > 0) {
+ console.warn(
+ `Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +
+ `Too many days have equivalent placement quality - please place remaining days manually.`,
+ );
+ }
+
+ return vacationsNumber;
}
samediMalin(vacationsNumber: VacationsNumber): void {
@@ -751,6 +792,7 @@ export class SelectedDates implements SelectedDateInterface {
const tempDatesBackup = [...this._datesSelected];
let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;
let bestHeuristic = -Infinity;
+ let daysWithBestHeuristic = 0;
// Evaluate each candidate day
for (const day of candidateDays) {
@@ -767,6 +809,7 @@ export class SelectedDates implements SelectedDateInterface {
// Track only if better than current best
if (heuristic > bestHeuristic) {
bestHeuristic = heuristic;
+ daysWithBestHeuristic = 1;
const capturedDay = day;
bestStrategy = {
apply: () => {
@@ -781,9 +824,25 @@ export class SelectedDates implements SelectedDateInterface {
heuristic: heuristic,
value: capturedDay,
};
+ } else if (heuristic === bestHeuristic) {
+ daysWithBestHeuristic++;
}
}
+ // Check if too many days have the same best heuristic after evaluating all days
+ if (daysWithBestHeuristic > SelectedDates.MAX_DAYS_WITH_SAME_HEURISTIC) {
+ // Restore backup before stopping
+ this._datesSelected = [...tempDatesBackup];
+ this.datesSelected = this._datesSelected.slice();
+ this.grouping();
+
+ // Stop optimization - let user decide manually
+ return {
+ apply: () => {},
+ heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC,
+ };
+ }
+
// Restore backup before returning
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
diff --git a/src/app/services/user-input-service.ts b/src/app/services/user-input-service.ts
index d9aa2f8..50a8dbb 100644
--- a/src/app/services/user-input-service.ts
+++ b/src/app/services/user-input-service.ts
@@ -18,4 +18,9 @@ export class UserInputService {
}
readonly vacationNumberSignal = signal(this.getInitialValues());
+ readonly remainingVacationDaysSignal = signal({
+ cp: 0,
+ rtt: 0,
+ other: 0,
+ });
}
--
2.54.0
From b97e9486e8d5d266fee6bd7cc908f6189fb6edec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9ophyl=20Hoguet?=
<61246905+THoguet@users.noreply.github.com>
Date: Tue, 18 Nov 2025 00:18:10 +0100
Subject: [PATCH 2/6] Update src/app/services/calendar-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/app/services/calendar-service.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts
index 0fc9d53..f95f40d 100644
--- a/src/app/services/calendar-service.ts
+++ b/src/app/services/calendar-service.ts
@@ -502,7 +502,7 @@ export class SelectedDates implements SelectedDateInterface {
// Check if adjacent (next day) - this is the best case!
if (nextStartTime - currentEndTime === oneDayInMs) {
- totalScore += 15; // Big bonus for adjacency
+ totalScore += 15; // 50% increased bonus for adjacency; adjacency is highly valued in the scoring system
} else if (nextStartTime > currentEndTime) {
// Penalty based on days between the two dates
const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1;
--
2.54.0
From df1b8a509d2461e5086889d58dc8b58202166666 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9ophyl=20Hoguet?=
<61246905+THoguet@users.noreply.github.com>
Date: Tue, 18 Nov 2025 00:18:19 +0100
Subject: [PATCH 3/6] Update
src/app/components/calendar-year/calendar-year.scss
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/app/components/calendar-year/calendar-year.scss | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/app/components/calendar-year/calendar-year.scss b/src/app/components/calendar-year/calendar-year.scss
index 53a1e65..d400415 100644
--- a/src/app/components/calendar-year/calendar-year.scss
+++ b/src/app/components/calendar-year/calendar-year.scss
@@ -57,8 +57,8 @@
::ng-deep .mat-mdc-progress-spinner {
// Override Material spinner size
- width: 100px !important;
- height: 100px !important;
+ width: 100px;
+ height: 100px;
}
.spinner-text {
--
2.54.0
From 3fab4c4922c14fb3b30c29801100cac634008827 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9ophyl=20Hoguet?=
<61246905+THoguet@users.noreply.github.com>
Date: Tue, 18 Nov 2025 00:21:40 +0100
Subject: [PATCH 4/6] Update src/app/components/user-input/user-input.html
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/app/components/user-input/user-input.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/components/user-input/user-input.html b/src/app/components/user-input/user-input.html
index 7a7a50f..d4a2088 100644
--- a/src/app/components/user-input/user-input.html
+++ b/src/app/components/user-input/user-input.html
@@ -17,7 +17,7 @@
@if (hasRemainingDays()) {
- warning
+ warning
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non
placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement.
--
2.54.0
From 1f3789d82b9a23c27a3838dcb6bc5e7de36fb79a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Th=C3=A9ophyl=20Hoguet?=
<61246905+THoguet@users.noreply.github.com>
Date: Tue, 18 Nov 2025 00:23:50 +0100
Subject: [PATCH 5/6] Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/app/services/calendar-service.ts | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts
index f95f40d..019a8a9 100644
--- a/src/app/services/calendar-service.ts
+++ b/src/app/services/calendar-service.ts
@@ -548,13 +548,18 @@ export class SelectedDates implements SelectedDateInterface {
const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;
let processedDays = 0;
- // Helper function to update progress
+ // Helper function to update progress, throttled to every 5%
+ let lastReportedProgress = 0;
+ const PROGRESS_STEP = 5; // percent
const updateProgress = async () => {
if (progressCallback && totalDays > 0) {
const progress = Math.round((processedDays / totalDays) * 100);
- progressCallback(progress);
- // Allow UI to update with a microtask
- await new Promise((resolve) => setTimeout(resolve, 0));
+ if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) {
+ progressCallback(progress);
+ lastReportedProgress = progress;
+ // Allow UI to update with a microtask
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
}
};
--
2.54.0
From 54c2160f0dd293f8c1c78342e155a65d891041fd Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 18 Nov 2025 00:26:18 +0100
Subject: [PATCH 6/6] Fix setTimeout async anti-pattern in calendar-year
component (#50)
* Initial plan
* fix: replace setTimeout async anti-pattern with queueMicrotask and void
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
---
.../components/calendar-year/calendar-year.ts | 36 ++++++++++---------
1 file changed, 19 insertions(+), 17 deletions(-)
diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts
index 2923d38..4f7829c 100644
--- a/src/app/components/calendar-year/calendar-year.ts
+++ b/src/app/components/calendar-year/calendar-year.ts
@@ -41,23 +41,25 @@ export class CalendarYear {
console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true);
this.loadingProgress.set(0);
- // Simulate server-side calculation with a small delay
- setTimeout(async () => {
- if (this.selectedDates) {
- // Create a copy to avoid mutating the original signal data
- const remainingDays = await this.selectedDates.optimizeVacations(
- { ...vacationData },
- this.calendarSettingsService,
- this.calendarService,
- (progress: number) => {
- this.loadingProgress.set(progress);
- },
- );
- // Update the remaining vacation days signal
- this.userInput.remainingVacationDaysSignal.set(remainingDays);
- }
- this.isLoading.set(false);
- }, 1);
+ // Defer execution to allow UI to update
+ queueMicrotask(() => {
+ void (async () => {
+ if (this.selectedDates) {
+ // Create a copy to avoid mutating the original signal data
+ const remainingDays = await this.selectedDates.optimizeVacations(
+ { ...vacationData },
+ this.calendarSettingsService,
+ this.calendarService,
+ (progress: number) => {
+ this.loadingProgress.set(progress);
+ },
+ );
+ // Update the remaining vacation days signal
+ this.userInput.remainingVacationDaysSignal.set(remainingDays);
+ }
+ this.isLoading.set(false);
+ })();
+ });
});
}
--
2.54.0