From 7e40ce0f6d23d9195e48c67a2bf8f64f98f2c548 Mon Sep 17 00:00:00 2001 From: TO Date: Wed, 29 Oct 2025 00:21:17 +0100 Subject: [PATCH] V1 done still need to work on SSR --- .../calendar-month/calendar-month.scss | 8 +- .../calendar-year/calendar-year.html | 8 ++ .../calendar-year/calendar-year.scss | 48 ++++++++ .../components/calendar-year/calendar-year.ts | 25 ++-- src/app/components/user-input/user-input.html | 3 +- src/app/components/user-input/user-input.scss | 2 +- src/app/components/user-input/user-input.ts | 55 +++++++-- src/app/services/calendar-service.ts | 114 +++++++++++++++--- src/app/services/user-input-service.ts | 19 ++- 9 files changed, 237 insertions(+), 45 deletions(-) diff --git a/src/app/components/calendar-month/calendar-month.scss b/src/app/components/calendar-month/calendar-month.scss index b7c6b83..a51dec0 100644 --- a/src/app/components/calendar-month/calendar-month.scss +++ b/src/app/components/calendar-month/calendar-month.scss @@ -32,8 +32,10 @@ align-items: center; justify-content: center; width: 30px; - height: 30px; + height: 26px; margin: 0; + margin-top: 2px; + margin-bottom: 2px; border-radius: 40%; &.empty { @@ -70,7 +72,7 @@ border-color: #1976d2; border-width: 2px; width: 26px !important; - height: 26px !important; + height: 22px !important; border-style: dashed !important; } @@ -85,7 +87,7 @@ .closed-day { border: 2px solid #0ee3ff; width: 26px !important; - height: 26px !important; + height: 22px !important; } .rtt { diff --git a/src/app/components/calendar-year/calendar-year.html b/src/app/components/calendar-year/calendar-year.html index cfbc864..53fa006 100644 --- a/src/app/components/calendar-year/calendar-year.html +++ b/src/app/components/calendar-year/calendar-year.html @@ -1,4 +1,12 @@
+ @if (isLoading()) { +
+
+
+

Calcul en cours...

+
+
+ } @for (month of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; track $index) { diff --git a/src/app/components/calendar-year/calendar-year.scss b/src/app/components/calendar-year/calendar-year.scss index fb91eb0..17a4274 100644 --- a/src/app/components/calendar-year/calendar-year.scss +++ b/src/app/components/calendar-year/calendar-year.scss @@ -5,4 +5,52 @@ justify-content: center; gap: 16px; max-width: 60vw; + position: relative; +} + +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.9); + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + z-index: 1000; +} + +.loading-spinner { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + + p { + font-size: 16px; + font-weight: 500; + color: #333; + margin: 0; + } +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts index 340de61..b7c697a 100644 --- a/src/app/components/calendar-year/calendar-year.ts +++ b/src/app/components/calendar-year/calendar-year.ts @@ -1,18 +1,32 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, effect, signal } from '@angular/core'; import { UserInputService } from '../../services/user-input-service'; -import { DateRange } from '@angular/material/datepicker'; import { MatCard, MatCardContent } from '@angular/material/card'; import { CalendarMonth } from "../calendar-month/calendar-month"; import { CalendarService, SelectedDates, VacationsNumber } from '../../services/calendar-service'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-calendar-year', - imports: [MatCard, CalendarMonth, MatCardContent], + imports: [MatCard, CalendarMonth, MatCardContent, CommonModule], templateUrl: './calendar-year.html', styleUrl: './calendar-year.scss', }) export class CalendarYear implements OnInit { + protected isLoading = signal(true); + constructor(private userInput: UserInputService, private calendarService: CalendarService) { + effect(() => { + this.selectedDates = this.calendarService.getSelectedDatesForYear(this.year); + const vacationData = this.userInput.vacationNumberSignal(); + this.isLoading.set(true); + // Simulate server-side calculation with a small delay + setTimeout(() => { + if (this.selectedDates) { + this.selectedDates.optimizeVacations(vacationData); + } + this.isLoading.set(false); + }, 500); + }); } protected selectedDates: SelectedDates = new SelectedDates(); @@ -21,9 +35,6 @@ export class CalendarYear implements OnInit { ngOnInit(): void { this.selectedDates = this.calendarService.getSelectedDatesForYear(this.year); - const vacationsNumber: VacationsNumber = { - cp: 10, rtt: 5, other: 0 - }; - this.selectedDates.optimizeVacations(vacationsNumber); + this.isLoading.set(false); } } diff --git a/src/app/components/user-input/user-input.html b/src/app/components/user-input/user-input.html index 4fe71b4..7efc1b7 100644 --- a/src/app/components/user-input/user-input.html +++ b/src/app/components/user-input/user-input.html @@ -1,6 +1,6 @@

Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !

-
+ Combien de CP par an ? @@ -13,6 +13,5 @@ Autres jours -
diff --git a/src/app/components/user-input/user-input.scss b/src/app/components/user-input/user-input.scss index 253d7b8..92f577b 100644 --- a/src/app/components/user-input/user-input.scss +++ b/src/app/components/user-input/user-input.scss @@ -15,7 +15,7 @@ } } -.form { +form { display: flex; flex-direction: column; gap: 1rem; diff --git a/src/app/components/user-input/user-input.ts b/src/app/components/user-input/user-input.ts index 590400a..fe85981 100644 --- a/src/app/components/user-input/user-input.ts +++ b/src/app/components/user-input/user-input.ts @@ -1,27 +1,60 @@ -import { Component, Input, input, signal, WritableSignal } from '@angular/core'; +import { Component, OnInit, OnDestroy, effect } from '@angular/core'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { MatCalendar } from "@angular/material/datepicker"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { UserInputService } from '../../services/user-input-service'; -import { MatAnchor, MatButton } from "@angular/material/button"; +import { VacationsNumber } from '../../services/calendar-service'; +import { debounceTime, takeUntil, Subject } from 'rxjs'; @Component({ selector: 'app-user-input', - imports: [MatFormField, MatLabel, MatInputModule, ReactiveFormsModule, MatAnchor, MatButton], + imports: [MatFormField, MatLabel, MatInputModule, ReactiveFormsModule], templateUrl: './user-input.html', styleUrl: './user-input.scss', }) -export class UserInput { - constructor(private userInput: UserInputService) { } +export class UserInput implements OnInit, OnDestroy { + constructor(private userInputService: UserInputService) { + // Watch for signal changes and update form + effect(() => { + const vacationData = this.userInputService.vacationNumberSignal(); + this.userInputForm.patchValue({ + CP: vacationData.cp, + RTT: vacationData.rtt, + Others: vacationData.other, + }, { emitEvent: false }); + }); + } + + private destroy$ = new Subject(); userInputForm: FormGroup = new FormGroup({ - CP: new FormControl(null, Validators.required), - RTT: new FormControl(null, Validators.required), - Others: new FormControl(null, Validators.required), + CP: new FormControl(null), + RTT: new FormControl(null), + Others: new FormControl(null), }); - submitForm() { - console.log("prout"); + ngOnInit() { + this.userInputForm.valueChanges + .pipe( + debounceTime(2500), + takeUntil(this.destroy$) + ) + .subscribe(() => this.submitForm()); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + private submitForm() { + if (this.userInputForm.valid) { + const vacationData: VacationsNumber = { + cp: this.userInputForm.get('CP')?.value || 0, + rtt: this.userInputForm.get('RTT')?.value || 0, + other: this.userInputForm.get('Others')?.value || 0, + }; + this.userInputService.setVacationNumber(vacationData); + } } } diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts index c73ea8e..6229d01 100644 --- a/src/app/services/calendar-service.ts +++ b/src/app/services/calendar-service.ts @@ -339,25 +339,100 @@ export class SelectedDates implements SelectedDateInterface { } computeHeuristics(): number { - // Logic to compute heuristics on selected dates - // use fibonacci sequence as heuristic points - - const fibonacci = [0, 1]; - for (let i = 2; i <= 10; i++) { - fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]; + if (this.datesSelected.length === 0) { + return 0; } - const heuristicPoints = this.datesSelected.reduce((acc, dateSelected) => { - const days = dateSelected.getLengthInDays(); - return acc + (fibonacci[days] || 0); - }, 0); - console.log(`Heuristic Points: ${heuristicPoints}`); - return heuristicPoints; + let totalScore = 0; + + // Sort all dates by start date to find next closest dates + const sorted = [...this.datesSelected].filter(date => date.type !== null).sort((a, b) => { + const aStart = a.range.start?.getTime() ?? 0; + const bStart = b.range.start?.getTime() ?? 0; + return aStart - bStart; + }); + + // PRIMARY: Optimize adjacency - reduce "little holes" between vacation periods + for (let i = 0; i < sorted.length - 1; i++) { + const current = sorted[i]; + const next = sorted[i + 1]; + + // Get dates as UTC strings to remove timezone issues + const currentEndDate = new Date(current.range.end ?? new Date()); + const nextStartDate = new Date(next.range.start ?? new Date()); + + // Convert to date-only strings (YYYY-MM-DD) to compare calendar days + const currentEndString = currentEndDate.toISOString().split('T')[0]; + const nextStartString = nextStartDate.toISOString().split('T')[0]; + + // Parse back to dates at UTC midnight + const currentEndDateNormalized = new Date(currentEndString + 'T00:00:00Z'); + const nextStartDateNormalized = new Date(nextStartString + 'T00:00:00Z'); + + const currentEndTime = currentEndDateNormalized.getTime(); + const nextStartTime = nextStartDateNormalized.getTime(); + + const oneDayInMs = 24 * 60 * 60 * 1000; + + // Check if adjacent (next day) - this is the best case! + if (nextStartTime - currentEndTime === oneDayInMs) { + totalScore += 10; // Big bonus for adjacency + } else if (nextStartTime > currentEndTime) { + // Penalty based on days between the two dates + const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1; + // Ensure daysBetween is non-negative before taking sqrt + totalScore -= (Math.sqrt(Math.max(0, daysBetween))); + } + } + + // SECONDARY: Penalize uneven monthly distribution to encourage spread + const monthCounts = new Map(); // Use year*12+month as key to avoid strings + for (const selectedDate of this.datesSelected) { + if (!selectedDate.range.start || !selectedDate.range.end) continue; + + // Count days in this selected date range + const daysInRange = selectedDate.getLengthInDays(); + + // For each day in the range, add it to the appropriate month + let currentDate = new Date(selectedDate.range.start); + for (let i = 0; i < daysInRange; i++) { + const monthKey = currentDate.getFullYear() * 12 + currentDate.getMonth(); + monthCounts.set(monthKey, (monthCounts.get(monthKey) ?? 0) + 1); + currentDate.setDate(currentDate.getDate() + 1); + } + } + + // Apply small penalty for uneven distribution (secondary priority) + for (const count of monthCounts.values()) { + totalScore -= count; // Smaller penalty than gap filling + } + + return totalScore; } optimizeVacations(vacationsNumber: VacationsNumber): void { this.samediMalin(vacationsNumber); - this.lookForVacationRec(); + while (vacationsNumber.cp > 0) { + const result = this.lookForVacationRec(DayType.CP); + if (result.heuristic === -1) { + break; + } + vacationsNumber.cp -= 1; + } + while (vacationsNumber.rtt > 0) { + const result = this.lookForVacationRec(DayType.RTT); + if (result.heuristic === -1) { + break; + } + vacationsNumber.rtt -= 1; + } + while (vacationsNumber.other > 0) { + const result = this.lookForVacationRec(DayType.OTHER); + if (result.heuristic === -1) { + break; + } + vacationsNumber.other -= 1; + } } samediMalin(vacationsNumber: VacationsNumber): void { @@ -418,7 +493,7 @@ export class SelectedDates implements SelectedDateInterface { apply: () => { this._datesSelected = [...tempDatesBackup]; this.push(new SelectedDate(DayType.CP, new DateRange(twoDaysBefore1, twoDaysBefore2))); - vacationsNumber.cp -= 1; + vacationsNumber.cp -= 2; }, heuristic: this.computeHeuristics() }); @@ -433,7 +508,7 @@ export class SelectedDates implements SelectedDateInterface { apply: () => { this._datesSelected = [...tempDatesBackup]; this.push(new SelectedDate(DayType.CP, new DateRange(twoDaysAfter1, twoDaysAfter2))); - vacationsNumber.cp -= 1; + vacationsNumber.cp -= 2; }, heuristic: this.computeHeuristics() }); @@ -450,7 +525,7 @@ export class SelectedDates implements SelectedDateInterface { this._datesSelected = [...tempDatesBackup]; this.push(new SelectedDate(DayType.CP, new DateRange(oneDayBefore, oneDayBefore))); this.push(new SelectedDate(DayType.CP, new DateRange(oneDayAfter, oneDayAfter))); - vacationsNumber.cp -= 1; + vacationsNumber.cp -= 2; }, heuristic: this.computeHeuristics() }); @@ -472,7 +547,7 @@ export class SelectedDates implements SelectedDateInterface { lookForVacationRec(vacType: DayType): { apply: () => void; heuristic: number } { const daysOfYear = CalendarService.monthsInYear(2026); const tempDatesBackup = [...this._datesSelected]; - let strategies: { apply: () => void; heuristic: number }[] = []; + let strategies: { apply: () => void; heuristic: number; value: Date }[] = []; for (const month of daysOfYear) { for (const week of month) { for (const day of week) { @@ -483,13 +558,14 @@ export class SelectedDates implements SelectedDateInterface { // mark these days as vacation const startDay = new Date(day); const endDay = new Date(day); - this.push(new SelectedDate(vacType, new DateRange(startDay, endDay))); + this.datesSelected.push(new SelectedDate(vacType, new DateRange(startDay, endDay))); strategies.push({ apply: () => { this._datesSelected = [...tempDatesBackup]; this.push(new SelectedDate(vacType, new DateRange(startDay, endDay))); }, - heuristic: this.computeHeuristics() + heuristic: this.computeHeuristics(), + value: startDay }); 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 5cc7abf..53a7527 100644 --- a/src/app/services/user-input-service.ts +++ b/src/app/services/user-input-service.ts @@ -1,5 +1,6 @@ import { Injectable, signal } from '@angular/core'; import Holidays, { HolidaysTypes } from 'date-holidays'; +import { VacationsNumber } from './calendar-service'; @@ -7,7 +8,21 @@ import Holidays, { HolidaysTypes } from 'date-holidays'; providedIn: 'root' }) export class UserInputService { - calculateVacationOptimization(nbCP: number, nbRTT: number, nbOther: number, year: number, closedDays: Date[]) { - // Logic to calculate vacation optimization based on user input + private getRandomNumber(): number { + return Math.floor(Math.random() * 9) + 1; + } + + vacationNumberSignal = signal({ + cp: this.getRandomNumber(), + rtt: this.getRandomNumber(), + other: this.getRandomNumber() + }); + + get vacationNumber() { + return this.vacationNumberSignal(); + } + + setVacationNumber(vacationsNumber: VacationsNumber) { + this.vacationNumberSignal.set(vacationsNumber); } }