Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andTHoguet 95f927386d Add logging service and refactor console.warn to use it
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-17 23:38:23 +00:00
copilot-swe-agent[bot] efe7b3da42 Initial plan 2025-11-17 23:30:03 +00:00
CopilotandTHoguet 54c2160f0d 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>
2025-11-18 00:26:18 +01:00
Théophyl HoguetandCopilot 1f3789d82b Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:23:50 +01:00
Théophyl HoguetandCopilot 3fab4c4922 Update src/app/components/user-input/user-input.html
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:21:40 +01:00
Théophyl HoguetandCopilot df1b8a509d Update src/app/components/calendar-year/calendar-year.scss
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:18:19 +01:00
Théophyl HoguetandCopilot b97e9486e8 Update src/app/services/calendar-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:18:10 +01:00
6 changed files with 139 additions and 27 deletions

No files matched your search

@@ -57,8 +57,8 @@
::ng-deep .mat-mdc-progress-spinner { ::ng-deep .mat-mdc-progress-spinner {
// Override Material spinner size // Override Material spinner size
width: 100px !important; width: 100px;
height: 100px !important; height: 100px;
} }
.spinner-text { .spinner-text {
@@ -7,6 +7,7 @@ import { CommonModule } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service'; import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service'; import { DateCacheService } from '../../services/date-cache.service';
import { LoggerService } from '../../services/logger.service';
@Component({ @Component({
selector: 'app-calendar-year', selector: 'app-calendar-year',
@@ -19,6 +20,7 @@ export class CalendarYear {
private userInput = inject(UserInputService); private userInput = inject(UserInputService);
private calendarService = inject(CalendarService); private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService); private calendarSettingsService = inject(CalendarSettingsService);
private logger = inject(LoggerService);
protected isLoading = signal(true); protected isLoading = signal(true);
protected loadingProgress = signal(0); protected loadingProgress = signal(0);
@@ -41,23 +43,26 @@ export class CalendarYear {
console.log('Optimizing vacations with data:', vacationData); console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true); this.isLoading.set(true);
this.loadingProgress.set(0); this.loadingProgress.set(0);
// Simulate server-side calculation with a small delay // Defer execution to allow UI to update
setTimeout(async () => { queueMicrotask(() => {
if (this.selectedDates) { void (async () => {
// Create a copy to avoid mutating the original signal data if (this.selectedDates) {
const remainingDays = await this.selectedDates.optimizeVacations( // Create a copy to avoid mutating the original signal data
{ ...vacationData }, const remainingDays = await this.selectedDates.optimizeVacations(
this.calendarSettingsService, { ...vacationData },
this.calendarService, this.calendarSettingsService,
(progress: number) => { this.calendarService,
this.loadingProgress.set(progress); (progress: number) => {
}, this.loadingProgress.set(progress);
); },
// Update the remaining vacation days signal this.logger,
this.userInput.remainingVacationDaysSignal.set(remainingDays); );
} // Update the remaining vacation days signal
this.isLoading.set(false); this.userInput.remainingVacationDaysSignal.set(remainingDays);
}, 1); }
this.isLoading.set(false);
})();
});
}); });
} }
@@ -17,7 +17,7 @@
@if (hasRemainingDays()) { @if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card"> <mat-card class="mat-elevation-z2 warning-card">
<mat-card-content class="warning-content"> <mat-card-content class="warning-content">
<mat-icon color="warn">warning</mat-icon> <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
<span> <span>
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non
placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement. placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement.
+15 -7
View File
@@ -6,6 +6,7 @@ import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek'; import isoWeek from 'dayjs/plugin/isoWeek';
import { CalendarSettingsService } from './calendar-settings-service'; import { CalendarSettingsService } from './calendar-settings-service';
import { DateCacheService } from './date-cache.service'; import { DateCacheService } from './date-cache.service';
import { LoggerService } from './logger.service';
dayjs.extend(weekOfYear); dayjs.extend(weekOfYear);
dayjs.extend(isoWeek); dayjs.extend(isoWeek);
@@ -21,6 +22,7 @@ const allTypesOptions: HolidaysTypes.Options = {
}) })
export class CalendarService { export class CalendarService {
private dateCache = inject(DateCacheService); private dateCache = inject(DateCacheService);
private logger = inject(LoggerService);
// Cache for holiday timestamps // Cache for holiday timestamps
private holidayTimestampsCache: Set<number> | null = null; private holidayTimestampsCache: Set<number> | null = null;
@@ -502,7 +504,7 @@ export class SelectedDates implements SelectedDateInterface {
// Check if adjacent (next day) - this is the best case! // Check if adjacent (next day) - this is the best case!
if (nextStartTime - currentEndTime === oneDayInMs) { 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) { } else if (nextStartTime > currentEndTime) {
// Penalty based on days between the two dates // Penalty based on days between the two dates
const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1; const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1;
@@ -541,6 +543,7 @@ export class SelectedDates implements SelectedDateInterface {
calendarSettingsService: CalendarSettingsService, calendarSettingsService: CalendarSettingsService,
calendarService: CalendarService, calendarService: CalendarService,
progressCallback?: (progress: number) => void, progressCallback?: (progress: number) => void,
logger?: LoggerService,
): Promise<VacationsNumber> { ): Promise<VacationsNumber> {
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber); if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
@@ -548,13 +551,18 @@ export class SelectedDates implements SelectedDateInterface {
const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other; const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;
let processedDays = 0; 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 () => { const updateProgress = async () => {
if (progressCallback && totalDays > 0) { if (progressCallback && totalDays > 0) {
const progress = Math.round((processedDays / totalDays) * 100); const progress = Math.round((processedDays / totalDays) * 100);
progressCallback(progress); if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) {
// Allow UI to update with a microtask progressCallback(progress);
await new Promise((resolve) => setTimeout(resolve, 0)); lastReportedProgress = progress;
// Allow UI to update with a microtask
await new Promise((resolve) => setTimeout(resolve, 0));
}
} }
}; };
@@ -595,8 +603,8 @@ export class SelectedDates implements SelectedDateInterface {
// Inform user if not all days were placed // Inform user if not all days were placed
const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other; const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other;
if (remainingDays > 0) { if (remainingDays > 0 && logger) {
console.warn( logger.warn(
`Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` + `Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +
`Too many days have equivalent placement quality - please place remaining days manually.`, `Too many days have equivalent placement quality - please place remaining days manually.`,
); );
+52
View File
@@ -0,0 +1,52 @@
import { TestBed } from '@angular/core/testing';
import { LoggerService } from './logger.service';
describe('LoggerService', () => {
let service: LoggerService;
let consoleLogSpy: jasmine.Spy;
let consoleWarnSpy: jasmine.Spy;
let consoleErrorSpy: jasmine.Spy;
let consoleDebugSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LoggerService);
// Spy on console methods
consoleLogSpy = spyOn(console, 'log');
consoleWarnSpy = spyOn(console, 'warn');
consoleErrorSpy = spyOn(console, 'error');
consoleDebugSpy = spyOn(console, 'debug');
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should log messages', () => {
service.log('test message');
expect(consoleLogSpy).toHaveBeenCalledWith('test message');
});
it('should log warnings', () => {
service.warn('test warning');
expect(consoleWarnSpy).toHaveBeenCalledWith('test warning');
});
it('should log errors', () => {
service.error('test error');
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
});
it('should log debug messages', () => {
service.debug('test debug');
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug');
});
it('should pass additional arguments to console methods', () => {
const arg1 = { key: 'value' };
const arg2 = [1, 2, 3];
service.log('test', arg1, arg2);
expect(consoleLogSpy).toHaveBeenCalledWith('test', arg1, arg2);
});
});
+47
View File
@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
/**
* Logger service that wraps console methods.
* Can be configured to disable logging in production environments.
*/
@Injectable({
providedIn: 'root',
})
export class LoggerService {
private isProduction = false; // Can be configured based on environment
/**
* Log an informational message
*/
log(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.log(message, ...args);
}
}
/**
* Log a warning message
*/
warn(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.warn(message, ...args);
}
}
/**
* Log an error message
*/
error(message: string, ...args: unknown[]): void {
// Always log errors, even in production
console.error(message, ...args);
}
/**
* Log a debug message
*/
debug(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.debug(message, ...args);
}
}
}