+ @if (isLoading()) {
+
+ }
@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 @@
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);
}
}