{ "CI_job_history_included": false, "approvals": [], "diff_versions": [ { "commits": [ { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 219, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 0, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 220, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 1, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 221, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 2, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 222, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 3, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 223, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 4, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 224, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 5, "sha": null, "trailers": {} }, { "authored_date": null, "commit_author_id": null, "committed_date": null, "committer_id": null, "merge_request_commits_metadata_id": 209, "merge_request_diff_id": 117, "message": null, "project_id": null, "relative_order": 6, "sha": null, "trailers": {} } ], "files": [ { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -20,7 +20,7 @@ jobs:\n cache: 'npm'\n \n - name: Install dependencies\n- run: npm ci\n+ run: npm ci --legacy-peer-deps\n \n - name: Check code formatting with Prettier\n run: npx prettier --check \"src/**/*.{ts,html,scss,css,json}\"\n@@ -41,7 +41,7 @@ jobs:\n cache: 'npm'\n \n - name: Install dependencies\n- run: npm ci\n+ run: npm ci --legacy-peer-deps\n \n - name: Run TypeScript compiler\n run: npx tsc --noEmit\n@@ -59,7 +59,7 @@ jobs:\n cache: 'npm'\n \n - name: Install dependencies\n- run: npm ci\n+ run: npm ci --legacy-peer-deps\n \n - name: Run unit tests\n run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage\n@@ -85,7 +85,7 @@ jobs:\n cache: 'npm'\n \n - name: Install dependencies\n- run: npm ci\n+ run: npm ci --legacy-peer-deps\n \n - name: Build production bundle\n run: npm run build -- --configuration production\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": ".github/workflows/pr-validation.yml", "project_id": 35, "relative_order": 0, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -3,14 +3,14 @@ import { UserInputService } from '../../services/user-input-service';\n import { MatCard, MatCardContent } from '@angular/material/card';\n import { CalendarMonth } from '../calendar-month/calendar-month';\n import { CalendarService, SelectedDates } from '../../services/calendar-service';\n-import { CommonModule } from '@angular/common';\n+\n import { MatProgressSpinner } from '@angular/material/progress-spinner';\n import { CalendarSettingsService } from '../../services/calendar-settings-service';\n import { DateCacheService } from '../../services/date-cache.service';\n \n @Component({\n \tselector: 'app-calendar-year',\n-\timports: [MatCard, CalendarMonth, MatCardContent, CommonModule, MatProgressSpinner],\n+\timports: [MatCard, CalendarMonth, MatCardContent, MatProgressSpinner],\n \ttemplateUrl: './calendar-year.html',\n \tstyleUrl: './calendar-year.scss',\n })\n@@ -45,9 +45,10 @@ export class CalendarYear {\n \t\t\tqueueMicrotask(() => {\n \t\t\t\tvoid (async () => {\n \t\t\t\t\tif (this.selectedDates) {\n-\t\t\t\t\t\t// Create a copy to avoid mutating the original signal data\n+\t\t\t\t\t\t// Create a deep copy to avoid mutating the original signal data\n+\t\t\t\t\t\tconst vacationDaysCopy = vacationData.map((vd) => ({ ...vd }));\n \t\t\t\t\t\tconst remainingDays = await this.selectedDates.optimizeVacations(\n-\t\t\t\t\t\t\t{ ...vacationData },\n+\t\t\t\t\t\t\tvacationDaysCopy,\n \t\t\t\t\t\t\tthis.calendarSettingsService,\n \t\t\t\t\t\t\tthis.calendarService,\n \t\t\t\t\t\t\t(progress: number) => {\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/components/calendar-year/calendar-year.ts", "project_id": 35, "relative_order": 1, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,29 +1,40 @@\n
\n \t

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

\n-\t
\n-\t\t\n-\t\t\tCombien de CP par an ?\n-\t\t\t\n-\t\t\n-\t\t\n-\t\t\tCombien de jours de RTT par an ?\n-\t\t\t\n-\t\t\n-\t\t\n-\t\t\tAutres jours\n-\t\t\t\n-\t\t\n-\t
\n+\t
\n+\t\t@for (day of daysOff; track day.id) {\n+\t\t\t
\n+\t\t\t\t\n+\t\t\t
\n+\t\t}\n+\t\t\n+\t\t\t\n+\t\t\t

Ajouter un jour de vacances

\n+\t\t
\n+\t
\n+\t
\n+\t\t\n+\t\t\n+\t
\n \t@if (hasRemainingDays()) {\n \t\t\n \t\t\t\n-\t\t\t\twarning\n-\t\t\t\t\n-\t\t\t\t\t{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non\n-\t\t\t\t\tplacé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement.\n-\t\t\t\t\n+\t\t\t\twarning\n+\t\t\t\t
\n+\t\t\t\t\t\n+\t\t\t\t\t\t{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances\n+\t\t\t\t\t\tnon placé{{ remainingDays() > 1 ? 's' : '' }}:\n+\t\t\t\t\t\n+\t\t\t\t\t@for (day of remainingDaysDetails(); track $index) {\n+\t\t\t\t\t\t- {{ day }}\n+\t\t\t\t\t}\n+\t\t\t\t\t Veuillez les placer manuellement. \n+\t\t\t\t
\n \t\t\t
\n \t\t
\n \t}\n-\t\n
\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/components/user-input/user-input.html", "project_id": 35, "relative_order": 2, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -25,7 +25,79 @@\n \t}\n }\n \n+.listOfDays {\n+\tdisplay: flex;\n+\tflex-direction: column;\n+\tgap: 0.5rem;\n+}\n+\n+.day-input-wrapper {\n+\tanimation: slide-fade-in 200ms ease-out;\n+\n+\t@starting-style {\n+\t\topacity: 0;\n+\t\ttransform: translateX(-20px);\n+\t}\n+}\n+\n+.day-enter {\n+\tanimation: slide-fade-in 200ms ease-out;\n+}\n+\n+.day-leave {\n+\topacity: 0;\n+\ttransform: translateX(20px);\n+\ttransition:\n+\t\topacity 200ms ease-in,\n+\t\ttransform 200ms ease-in;\n+}\n+\n+@keyframes slide-fade-in {\n+\tfrom {\n+\t\topacity: 0;\n+\t\ttransform: translateX(-20px);\n+\t}\n+\tto {\n+\t\topacity: 1;\n+\t\ttransform: translateX(0);\n+\t}\n+}\n+\n form {\n \tdisplay: flex;\n \tflex-direction: column;\n }\n+\n+.add-day-card {\n+\tdisplay: flex;\n+\tflex-direction: row;\n+\talign-items: center;\n+\tpadding: 16px;\n+\tgap: 1rem;\n+\tmargin-bottom: 1rem;\n+\tcursor: pointer;\n+}\n+\n+.add-day-card h3 {\n+\tflex-grow: 1;\n+\tmargin: 0;\n+\tfont-weight: normal;\n+}\n+\n+.action-buttons {\n+\tdisplay: flex;\n+\tflex-direction: row;\n+\tjustify-content: space-between;\n+\talign-items: center;\n+\tmargin-bottom: 1rem;\n+\tgap: 1rem;\n+}\n+\n+.warning {\n+\tdisplay: flex;\n+\tflex-direction: column;\n+}\n+\n+:host {\n+\tmax-width: 100%;\n+}\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/components/user-input/user-input.scss", "project_id": 35, "relative_order": 3, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,19 +1,23 @@\n import { ComponentFixture, TestBed } from '@angular/core/testing';\n import { provideZonelessChangeDetection } from '@angular/core';\n import { UserInput } from './user-input';\n+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\n+import { UserInputService } from '../../services/user-input-service';\n \n describe('UserInput', () => {\n \tlet component: UserInput;\n \tlet fixture: ComponentFixture;\n+\tlet userInputService: UserInputService;\n \n \tbeforeEach(async () => {\n \t\tawait TestBed.configureTestingModule({\n \t\t\timports: [UserInput],\n-\t\t\tproviders: [provideZonelessChangeDetection()],\n+\t\t\tproviders: [provideZonelessChangeDetection(), provideAnimationsAsync()],\n \t\t}).compileComponents();\n \n \t\tfixture = TestBed.createComponent(UserInput);\n \t\tcomponent = fixture.componentInstance;\n+\t\tuserInputService = TestBed.inject(UserInputService);\n \t\tfixture.detectChanges();\n \t});\n \n@@ -21,20 +25,22 @@ describe('UserInput', () => {\n \t\texpect(component).toBeTruthy();\n \t});\n \n-\tit('should initialize form with values from service', () => {\n-\t\texpect(component.userInputForm).toBeDefined();\n-\t\texpect(component.userInputForm.get('CP')).toBeDefined();\n-\t\texpect(component.userInputForm.get('RTT')).toBeDefined();\n-\t\texpect(component.userInputForm.get('Others')).toBeDefined();\n+\tit('should initialize with vacation days from service', () => {\n+\t\tconst vacationDays = userInputService.vacationNumberSignal();\n+\t\texpect(vacationDays).toBeDefined();\n+\t\texpect(Array.isArray(vacationDays)).toBe(true);\n+\t\texpect(vacationDays.length).toBeGreaterThan(0);\n \t});\n \n-\tit('should have valid form controls', () => {\n-\t\tconst cpControl = component.userInputForm.get('CP');\n-\t\tconst rttControl = component.userInputForm.get('RTT');\n-\t\tconst othersControl = component.userInputForm.get('Others');\n+\tit('should add a new vacation day', () => {\n+\t\tconst initialLength = userInputService.vacationNumberSignal().length;\n+\t\tcomponent.addDay();\n+\t\texpect(userInputService.vacationNumberSignal().length).toBe(initialLength + 1);\n+\t});\n \n-\t\texpect(cpControl).toBeTruthy();\n-\t\texpect(rttControl).toBeTruthy();\n-\t\texpect(othersControl).toBeTruthy();\n+\tit('should remove a vacation day', () => {\n+\t\tconst initialLength = userInputService.vacationNumberSignal().length;\n+\t\tcomponent.removeFromList(0);\n+\t\texpect(userInputService.vacationNumberSignal().length).toBe(initialLength - 1);\n \t});\n });\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/components/user-input/user-input.spec.ts", "project_id": 35, "relative_order": 4, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,79 +1,78 @@\n-import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core';\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\n+import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';\n import { MatInputModule } from '@angular/material/input';\n-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\n import { UserInputService } from '../../services/user-input-service';\n-import { VacationsNumber } from '../../services/calendar-service';\n-import { debounceTime, takeUntil, Subject } from 'rxjs';\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\n import { MatCard, MatCardContent } from '@angular/material/card';\n import { MatIcon } from '@angular/material/icon';\n+import { DayInput } from '../userInput/day-input/day-input';\n+import { MatButtonModule } from '@angular/material/button';\n+import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-service';\n \n @Component({\n \tselector: 'app-user-input',\n \timports: [\n-\t\tMatFormField,\n-\t\tMatLabel,\n \t\tMatInputModule,\n-\t\tReactiveFormsModule,\n \t\tCalendarSettings,\n \t\tMatCard,\n \t\tMatCardContent,\n \t\tMatIcon,\n+\t\tMatButtonModule,\n+\t\tDayInput,\n \t],\n \ttemplateUrl: './user-input.html',\n \tstyleUrl: './user-input.scss',\n+\tchangeDetection: ChangeDetectionStrategy.OnPush,\n })\n-export class UserInput implements OnInit, OnDestroy {\n-\tconstructor(private userInputService: UserInputService) {\n-\t\t// Watch for signal changes and update form\n-\t\teffect(() => {\n-\t\t\tconst vacationData = this.userInputService.vacationNumberSignal();\n-\t\t\tthis.userInputForm.patchValue(\n-\t\t\t\t{\n-\t\t\t\t\tCP: vacationData.cp,\n-\t\t\t\t\tRTT: vacationData.rtt,\n-\t\t\t\t\tOthers: vacationData.other,\n-\t\t\t\t},\n-\t\t\t\t{ emitEvent: false },\n-\t\t\t);\n-\t\t});\n-\t}\n-\n-\tprivate destroy$ = new Subject();\n+export class UserInput {\n+\tprivate userInputService: UserInputService = inject(UserInputService);\n \n-\tuserInputForm: FormGroup = new FormGroup({\n-\t\tCP: new FormControl(null),\n-\t\tRTT: new FormControl(null),\n-\t\tOthers: new FormControl(null),\n+\tprotected remainingDays = computed(() => {\n+\t\tconst remaining = this.userInputService.remainingVacationDaysSignal();\n+\t\treturn remaining.reduce((acc, day) => acc + day.numberOfDays, 0);\n \t});\n \n-\tprotected remainingDays = computed(() => {\n+\tprotected remainingDaysDetails = computed(() => {\n \t\tconst remaining = this.userInputService.remainingVacationDaysSignal();\n-\t\treturn remaining.cp + remaining.rtt + remaining.other;\n+\t\treturn remaining\n+\t\t\t.filter((day) => day.numberOfDays > 0)\n+\t\t\t.map((day) => {\n+\t\t\t\tconst typeName = tooltipTypeMapping[day.type as unknown as DayType];\n+\t\t\t\tconst expiryDate = day.expiryDate.toLocaleDateString('fr-FR', {\n+\t\t\t\t\tday: 'numeric',\n+\t\t\t\t\tmonth: 'short',\n+\t\t\t\t\tyear: 'numeric',\n+\t\t\t\t});\n+\t\t\t\treturn `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;\n+\t\t\t});\n \t});\n \n \tprotected hasRemainingDays = computed(() => this.remainingDays() > 0);\n \n-\tngOnInit() {\n-\t\tthis.userInputForm.valueChanges\n-\t\t\t.pipe(debounceTime(1500), takeUntil(this.destroy$))\n-\t\t\t.subscribe(() => this.submitForm());\n+\tprotected get daysOff() {\n+\t\treturn this.userInputService.vacationNumberSignal();\n+\t}\n+\n+\tpublic addDay() {\n+\t\tthis.userInputService.vacationNumberSignal.update((days) => [\n+\t\t\t...days,\n+\t\t\tthis.userInputService.createVacationDay(),\n+\t\t]);\n+\t}\n+\n+\tpublic removeFromList(index: number) {\n+\t\tthis.userInputService.vacationNumberSignal.update((days) =>\n+\t\t\tdays.filter((_, i) => i !== index),\n+\t\t);\n \t}\n \n-\tngOnDestroy() {\n-\t\tthis.destroy$.next();\n-\t\tthis.destroy$.complete();\n+\tpublic updateDay(index: number, updatedDay: VacDay) {\n+\t\tthis.userInputService.vacationNumberSignal.update((days) =>\n+\t\t\tdays.map((day, i) => (i === index ? updatedDay : day)),\n+\t\t);\n \t}\n \n-\tprivate submitForm() {\n-\t\tif (this.userInputForm.valid) {\n-\t\t\tconst vacationData: VacationsNumber = {\n-\t\t\t\tcp: this.userInputForm.get('CP')?.value || 0,\n-\t\t\t\trtt: this.userInputForm.get('RTT')?.value || 0,\n-\t\t\t\tother: this.userInputForm.get('Others')?.value || 0,\n-\t\t\t};\n-\t\t\tthis.userInputService.vacationNumberSignal.set(vacationData);\n-\t\t}\n+\tpublic triggerComputation() {\n+\t\t// Trigger signal update by setting a new array reference\n+\t\tthis.userInputService.vacationNumberSignal.update((days) => [...days]);\n \t}\n }\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/components/user-input/user-input.ts", "project_id": 35, "relative_order": 5, "renamed_file": false, "too_large": false }, { "a_mode": "0", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -0,0 +1,36 @@\n+\n+\n+\t\n+\t\t\n+\t\n+\t\n+\t\t\n+\t\t\t\n+\t\t\t@for (dayType of selectableDayTypes; track $index) {\n+\t\t\t\t{{ tooltipTypeMapping[dayType] }}\n+\t\t\t}\n+\t\t\n+\t\n+\t\n+\t\tChoose a date\n+\t\t\n+\t\t\n+\t\t\n+\t\n+\t\n+\n+\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": true, "new_path": null, "old_path": "src/app/components/userInput/day-input/day-input.html", "project_id": 35, "relative_order": 6, "renamed_file": false, "too_large": false }, { "a_mode": "0", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -0,0 +1,21 @@\n+.card {\n+\tdisplay: flex;\n+\tflex-direction: row;\n+\talign-items: center;\n+\tjustify-content: space-between;\n+\tpadding: 16px;\n+\tgap: 16px;\n+\tflex-wrap: wrap;\n+}\n+\n+.dateInput {\n+\tflex-grow: 1;\n+}\n+\n+.numberInput {\n+\twidth: 80px;\n+}\n+\n+::ng-deep .mat-mdc-form-field-subscript-wrapper {\n+\tdisplay: none;\n+}\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": true, "new_path": null, "old_path": "src/app/components/userInput/day-input/day-input.scss", "project_id": 35, "relative_order": 7, "renamed_file": false, "too_large": false }, { "a_mode": "0", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -0,0 +1,33 @@\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\n+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\n+\n+import { DayInput } from './day-input';\n+import { SelectableDayType, VacDay } from '../../../services/calendar-service';\n+\n+describe('DayInput', () => {\n+\tlet component: DayInput;\n+\tlet fixture: ComponentFixture;\n+\n+\tconst mockVacDay: VacDay = {\n+\t\tid: 'test-id',\n+\t\ttype: SelectableDayType.CP,\n+\t\tnumberOfDays: 5,\n+\t\texpiryDate: new Date('2025-12-31'),\n+\t};\n+\n+\tbeforeEach(async () => {\n+\t\tawait TestBed.configureTestingModule({\n+\t\t\timports: [DayInput],\n+\t\t\tproviders: [provideAnimationsAsync()],\n+\t\t}).compileComponents();\n+\n+\t\tfixture = TestBed.createComponent(DayInput);\n+\t\tcomponent = fixture.componentInstance;\n+\t\tfixture.componentRef.setInput('day', mockVacDay);\n+\t\tfixture.detectChanges();\n+\t});\n+\n+\tit('should create', () => {\n+\t\texpect(component).toBeTruthy();\n+\t});\n+});\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": true, "new_path": null, "old_path": "src/app/components/userInput/day-input/day-input.spec.ts", "project_id": 35, "relative_order": 8, "renamed_file": false, "too_large": false }, { "a_mode": "0", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -0,0 +1,103 @@\n+import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';\n+import { SelectableDayType, tooltipTypeMapping, VacDay } from '../../../services/calendar-service';\n+import { MatCard } from '@angular/material/card';\n+import { MatOption, MatSelect } from '@angular/material/select';\n+import { MatInput, MatInputModule, MatLabel } from '@angular/material/input';\n+import {\n+\tMatDatepicker,\n+\tMatDatepickerInput,\n+\tMatDatepickerInputEvent,\n+\tMatDatepickerToggle,\n+} from '@angular/material/datepicker';\n+import {\n+\tDateAdapter,\n+\tMAT_DATE_FORMATS,\n+\tMAT_DATE_LOCALE,\n+\tNativeDateAdapter,\n+} from '@angular/material/core';\n+import { MatIconModule } from '@angular/material/icon';\n+import { MatButtonModule } from '@angular/material/button';\n+\n+class CustomDateAdapter extends NativeDateAdapter {\n+\toverride format(date: Date, displayFormat: unknown): string {\n+\t\tif (displayFormat === 'input') {\n+\t\t\tconst day = date.getDate().toString().padStart(2, '0');\n+\t\t\tconst month = (date.getMonth() + 1).toString().padStart(2, '0');\n+\t\t\tconst year = date.getFullYear();\n+\t\t\treturn `${day}/${month}/${year}`;\n+\t\t}\n+\t\treturn super.format(date, displayFormat as object);\n+\t}\n+\n+\toverride parse(value: string): Date | null {\n+\t\tif (!value) return null;\n+\t\tconst parts = value.split('/');\n+\t\tif (parts.length === 3) {\n+\t\t\tconst day = parseInt(parts[0], 10);\n+\t\t\tconst month = parseInt(parts[1], 10) - 1;\n+\t\t\tconst year = parseInt(parts[2], 10);\n+\t\t\tif (!isNaN(day) && !isNaN(month) && !isNaN(year)) {\n+\t\t\t\treturn new Date(year, month, day);\n+\t\t\t}\n+\t\t}\n+\t\treturn null;\n+\t}\n+}\n+\n+const MY_DATE_FORMATS = {\n+\tparse: {\n+\t\tdateInput: 'input',\n+\t},\n+\tdisplay: {\n+\t\tdateInput: 'input',\n+\t\tmonthYearLabel: { year: 'numeric', month: 'short' },\n+\t\tdateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },\n+\t\tmonthYearA11yLabel: { year: 'numeric', month: 'long' },\n+\t},\n+};\n+\n+@Component({\n+\tselector: 'app-day-input',\n+\timports: [\n+\t\tMatIconModule,\n+\t\tMatCard,\n+\t\tMatSelect,\n+\t\tMatOption,\n+\t\tMatInput,\n+\t\tMatDatepicker,\n+\t\tMatLabel,\n+\t\tMatDatepickerToggle,\n+\t\tMatDatepickerInput,\n+\t\tMatInputModule,\n+\t\tMatButtonModule,\n+\t],\n+\ttemplateUrl: './day-input.html',\n+\tstyleUrl: './day-input.scss',\n+\tchangeDetection: ChangeDetectionStrategy.OnPush,\n+\tproviders: [\n+\t\t{ provide: DateAdapter, useClass: CustomDateAdapter },\n+\t\t{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },\n+\t\t{ provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMATS },\n+\t],\n+})\n+export class DayInput {\n+\tpublic day = input.required();\n+\tpublic dayChange = output();\n+\tpublic deleteEntry = output();\n+\n+\tprotected readonly selectableDayTypes = Object.values(SelectableDayType);\n+\tprotected readonly tooltipTypeMapping = tooltipTypeMapping;\n+\n+\tonInput(event: Event) {\n+\t\tconst value = Number((event.target as HTMLInputElement).value);\n+\t\tthis.dayChange.emit({ ...this.day(), numberOfDays: value });\n+\t}\n+\n+\tonTypeChange(type: SelectableDayType) {\n+\t\tthis.dayChange.emit({ ...this.day(), type });\n+\t}\n+\n+\taddEvent(event: MatDatepickerInputEvent) {\n+\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n+\t}\n+}\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": true, "new_path": null, "old_path": "src/app/components/userInput/day-input/day-input.ts", "project_id": 35, "relative_order": 9, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -184,10 +184,11 @@ export class CalendarService {\n \t}\n }\n \n-export type VacationsNumber = {\n-\trtt: number;\n-\tcp: number;\n-\tother: number;\n+export type VacDay = {\n+\tid: string;\n+\ttype: SelectableDayType;\n+\tnumberOfDays: number;\n+\texpiryDate: Date;\n };\n \n export type Week = (Date | null)[];\n@@ -203,6 +204,12 @@ export enum DayType {\n \tTODAY = 'today',\n }\n \n+export enum SelectableDayType {\n+\tCP = DayType.CP,\n+\tRTT = DayType.RTT,\n+\tOTHER = DayType.OTHER,\n+}\n+\n export const tooltipTypeMapping: { [key in DayType]: string } = {\n \t[DayType.WEEKEND]: 'Weekend',\n \t[DayType.CLOSED_DAY]: 'Jour Férié',\n@@ -537,15 +544,15 @@ export class SelectedDates implements SelectedDateInterface {\n \t}\n \n \tasync optimizeVacations(\n-\t\tvacationsNumber: VacationsNumber,\n+\t\tvacationDays: VacDay[],\n \t\tcalendarSettingsService: CalendarSettingsService,\n \t\tcalendarService: CalendarService,\n \t\tprogressCallback?: (progress: number) => void,\n-\t): Promise {\n-\t\tif (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);\n+\t): Promise {\n+\t\tif (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationDays);\n \n-\t\tconst initialVacations = { ...vacationsNumber };\n-\t\tconst totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;\n+\t\t// Calculate total days to process\n+\t\tconst totalDays = vacationDays.reduce((sum, vd) => sum + vd.numberOfDays, 0);\n \t\tlet processedDays = 0;\n \n \t\t// Helper function to update progress, throttled to every 5%\n@@ -565,30 +572,30 @@ export class SelectedDates implements SelectedDateInterface {\n \n \t\tawait updateProgress();\n \n-\t\twhile (vacationsNumber.cp > 0) {\n-\t\t\tconst result = this.lookForVacation(DayType.CP, calendarService);\n-\t\t\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t\tvacationsNumber.cp -= 1;\n-\t\t\tprocessedDays++;\n-\t\t\tawait updateProgress();\n-\t\t}\n-\t\twhile (vacationsNumber.rtt > 0) {\n-\t\t\tconst result = this.lookForVacation(DayType.RTT, calendarService);\n-\t\t\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\n+\t\t// Track VacDays that can't find valid placements\n+\t\tconst exhaustedVacDayIds = new Set();\n+\n+\t\t// Process vacation days ordered by expiration date (earliest first)\n+\t\twhile (vacationDays.some((vd) => vd.numberOfDays > 0 && !exhaustedVacDayIds.has(vd.id))) {\n+\t\t\t// Sort by expiration date to use days that expire first, excluding exhausted ones\n+\t\t\tconst sortedVacDays = [...vacationDays]\n+\t\t\t\t.filter((vd) => vd.numberOfDays > 0 && !exhaustedVacDayIds.has(vd.id))\n+\t\t\t\t.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());\n+\n+\t\t\tif (sortedVacDays.length === 0) {\n \t\t\t\tbreak;\n \t\t\t}\n-\t\t\tvacationsNumber.rtt -= 1;\n-\t\t\tprocessedDays++;\n-\t\t\tawait updateProgress();\n-\t\t}\n-\t\twhile (vacationsNumber.other > 0) {\n-\t\t\tconst result = this.lookForVacation(DayType.OTHER, calendarService);\n+\n+\t\t\tconst vacDayToUse = sortedVacDays[0];\n+\t\t\tconst result = this.lookForVacation(vacDayToUse, calendarService);\n+\n \t\t\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\n-\t\t\t\tbreak;\n+\t\t\t\t// Mark this VacDay as exhausted and try other vacation days\n+\t\t\t\texhaustedVacDayIds.add(vacDayToUse.id);\n+\t\t\t\tcontinue;\n \t\t\t}\n-\t\t\tvacationsNumber.other -= 1;\n+\n+\t\t\tvacDayToUse.numberOfDays -= 1;\n \t\t\tprocessedDays++;\n \t\t\tawait updateProgress();\n \t\t}\n@@ -599,7 +606,7 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t}\n \n \t\t// Inform user if not all days were placed\n-\t\tconst remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other;\n+\t\tconst remainingDays = vacationDays.reduce((sum, vd) => sum + vd.numberOfDays, 0);\n \t\tif (remainingDays > 0) {\n \t\t\tconsole.warn(\n \t\t\t\t`Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +\n@@ -607,10 +614,10 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\t);\n \t\t}\n \n-\t\treturn vacationsNumber;\n+\t\treturn vacationDays;\n \t}\n \n-\tsamediMalin(vacationsNumber: VacationsNumber): void {\n+\tsamediMalin(vacationDays: VacDay[]): void {\n \t\tconst now = new Date();\n \t\tnow.setHours(0, 0, 0, 0);\n \t\tconst nowTime = now.getTime();\n@@ -619,13 +626,62 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\t(date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6,\n \t\t);\n \n-\t\tfor (const holiday of samedi_ferie) {\n-\t\t\tif (!holiday.range.start) continue;\n+\t\t// Helper function to get available CP days that don't expire before a given date (ordered by expiry date)\n+\t\tconst getAvailableCpDaysForDate = (useByDate: Date): VacDay[] => {\n+\t\t\tconst useByTime = useByDate.getTime();\n+\t\t\treturn vacationDays\n+\t\t\t\t.filter(\n+\t\t\t\t\t(vd) =>\n+\t\t\t\t\t\tvd.type === SelectableDayType.CP &&\n+\t\t\t\t\t\tvd.numberOfDays > 0 &&\n+\t\t\t\t\t\tvd.expiryDate.getTime() >= useByTime,\n+\t\t\t\t)\n+\t\t\t\t.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());\n+\t\t};\n \n-\t\t\t// Skip if no CP days available\n-\t\t\tif (vacationsNumber.cp < 2) {\n-\t\t\t\tcontinue;\n+\t\t// Helper function to get total available CP days that don't expire before a given date\n+\t\tconst getTotalCpDaysForDate = (useByDate: Date): number => {\n+\t\t\tconst useByTime = useByDate.getTime();\n+\t\t\treturn vacationDays\n+\t\t\t\t.filter(\n+\t\t\t\t\t(vd) =>\n+\t\t\t\t\t\tvd.type === SelectableDayType.CP && vd.expiryDate.getTime() >= useByTime,\n+\t\t\t\t)\n+\t\t\t\t.reduce((sum, vd) => sum + vd.numberOfDays, 0);\n+\t\t};\n+\n+\t\t// Helper function to consume 2 CP days from the available pool for a specific date\n+\t\tconst consume2CpDaysForDate = (useByDate: Date): boolean => {\n+\t\t\tlet daysToConsume = 2;\n+\t\t\tconst cpDays = getAvailableCpDaysForDate(useByDate);\n+\t\t\tfor (const cpDay of cpDays) {\n+\t\t\t\tif (daysToConsume === 0) break;\n+\t\t\t\tconst toConsume = Math.min(cpDay.numberOfDays, daysToConsume);\n+\t\t\t\tcpDay.numberOfDays -= toConsume;\n+\t\t\t\tdaysToConsume -= toConsume;\n \t\t\t}\n+\t\t\treturn daysToConsume === 0;\n+\t\t};\n+\n+\t\t// Helper function to add 1 OTHER day as reward\n+\t\tconst addOtherDayReward = (): void => {\n+\t\t\t// Find an existing OTHER VacDay or create a new one\n+\t\t\tconst otherDay = vacationDays.find((vd) => vd.type === SelectableDayType.OTHER);\n+\t\t\tif (otherDay) {\n+\t\t\t\totherDay.numberOfDays += 1;\n+\t\t\t} else {\n+\t\t\t\t// Create a new OTHER VacDay with a far future expiry date\n+\t\t\t\tvacationDays.push({\n+\t\t\t\t\tid: crypto.randomUUID(),\n+\t\t\t\t\ttype: SelectableDayType.OTHER,\n+\t\t\t\t\tnumberOfDays: 1,\n+\t\t\t\t\texpiryDate: new Date(now.getFullYear() + 10, 11, 31),\n+\t\t\t\t});\n+\t\t\t}\n+\t\t};\n+\n+\t\tfor (const holiday of samedi_ferie) {\n+\t\t\tif (!holiday.range.start) continue;\n \n \t\t\t// Get the Saturday date (holiday) and the following Sunday\n \t\t\tconst saturdayDate = new Date(holiday.range.start);\n@@ -651,21 +707,24 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\tconst oneDayAfter = new Date(sundayDate);\n \t\t\toneDayAfter.setDate(sundayDate.getDate() + 1);\n \n-\t\t\t// Check if we can apply each strategy (and not in the past)\n+\t\t\t// Check if we can apply each strategy (not in the past, not already selected, and have non-expired CP days)\n \t\t\tconst canApplyTwoBeforeStrategy =\n \t\t\t\ttwoDaysBefore1.getTime() >= nowTime &&\n \t\t\t\t!this.isSelected(twoDaysBefore1) &&\n-\t\t\t\t!this.isSelected(twoDaysBefore2);\n+\t\t\t\t!this.isSelected(twoDaysBefore2) &&\n+\t\t\t\tgetTotalCpDaysForDate(twoDaysBefore2) >= 2; // Check expiry against the latest date used\n \n \t\t\tconst canApplyTwoAfterStrategy =\n \t\t\t\ttwoDaysAfter1.getTime() >= nowTime &&\n \t\t\t\t!this.isSelected(twoDaysAfter1) &&\n-\t\t\t\t!this.isSelected(twoDaysAfter2);\n+\t\t\t\t!this.isSelected(twoDaysAfter2) &&\n+\t\t\t\tgetTotalCpDaysForDate(twoDaysAfter2) >= 2; // Check expiry against the latest date used\n \n \t\t\tconst canApplyMixedStrategy =\n \t\t\t\toneDayBefore.getTime() >= nowTime &&\n \t\t\t\t!this.isSelected(oneDayBefore) &&\n-\t\t\t\t!this.isSelected(oneDayAfter);\n+\t\t\t\t!this.isSelected(oneDayAfter) &&\n+\t\t\t\tgetTotalCpDaysForDate(oneDayAfter) >= 2; // Check expiry against the latest date used\n \n \t\t\t// Test all applicable strategies and choose the one with the best heuristic score\n \t\t\tconst strategies: { apply: () => void; heuristic: number }[] = [];\n@@ -688,8 +747,8 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\t\t\t\t\t\tnew DateRange(twoDaysBefore1, twoDaysBefore2),\n \t\t\t\t\t\t\t),\n \t\t\t\t\t\t);\n-\t\t\t\t\t\tvacationsNumber.cp -= 2;\n-\t\t\t\t\t\tvacationsNumber.other += 1; // Reward: gain 1 other day\n+\t\t\t\t\t\tconsume2CpDaysForDate(twoDaysBefore2);\n+\t\t\t\t\t\taddOtherDayReward();\n \t\t\t\t\t},\n \t\t\t\t\theuristic: heuristic,\n \t\t\t\t});\n@@ -712,8 +771,8 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\t\t\t\t\t\tnew DateRange(twoDaysAfter1, twoDaysAfter2),\n \t\t\t\t\t\t\t),\n \t\t\t\t\t\t);\n-\t\t\t\t\t\tvacationsNumber.cp -= 2;\n-\t\t\t\t\t\tvacationsNumber.other += 1; // Reward: gain 1 other day\n+\t\t\t\t\t\tconsume2CpDaysForDate(twoDaysAfter2);\n+\t\t\t\t\t\taddOtherDayReward();\n \t\t\t\t\t},\n \t\t\t\t\theuristic: heuristic,\n \t\t\t\t});\n@@ -745,8 +804,8 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\t\t\t\t\t\tnew DateRange(oneDayAfter, oneDayAfter),\n \t\t\t\t\t\t\t),\n \t\t\t\t\t\t);\n-\t\t\t\t\t\tvacationsNumber.cp -= 2;\n-\t\t\t\t\t\tvacationsNumber.other += 1; // Reward: gain 1 other day\n+\t\t\t\t\t\tconsume2CpDaysForDate(oneDayAfter);\n+\t\t\t\t\t\taddOtherDayReward();\n \t\t\t\t\t},\n \t\t\t\t\theuristic: heuristic,\n \t\t\t\t});\n@@ -766,22 +825,24 @@ export class SelectedDates implements SelectedDateInterface {\n \t}\n \n \tlookForVacation(\n-\t\tvacType: DayType,\n+\t\tvacDay: VacDay,\n \t\tcalendarService: CalendarService,\n \t): { apply: () => void; heuristic: number } {\n \t\tconst daysOfYear = calendarService.monthsInAYearFromNow();\n \t\tconst now = new Date();\n \t\tnow.setHours(0, 0, 0, 0);\n \t\tconst nowTime = now.getTime();\n+\t\tconst expiryTime = vacDay.expiryDate.getTime();\n \n-\t\t// Preprocess: filter out past days and already selected days in one pass\n+\t\t// Preprocess: filter out past days, already selected days, and days after expiry date\n \t\tconst candidateDays: Date[] = [];\n \t\tfor (const month of daysOfYear) {\n \t\t\tfor (const week of month) {\n \t\t\t\tfor (const day of week) {\n \t\t\t\t\tif (day) {\n \t\t\t\t\t\tday.setHours(0, 0, 0, 0);\n-\t\t\t\t\t\tif (day.getTime() >= nowTime && !this.isSelected(day)) {\n+\t\t\t\t\t\tconst dayTime = day.getTime();\n+\t\t\t\t\t\tif (dayTime >= nowTime && dayTime <= expiryTime && !this.isSelected(day)) {\n \t\t\t\t\t\t\tcandidateDays.push(day);\n \t\t\t\t\t\t}\n \t\t\t\t\t}\n@@ -794,6 +855,7 @@ export class SelectedDates implements SelectedDateInterface {\n \t\t\treturn { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };\n \t\t}\n \n+\t\tconst vacType = vacDay.type as unknown as DayType;\n \t\tconst tempDatesBackup = [...this._datesSelected];\n \t\tlet bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;\n \t\tlet bestHeuristic = -Infinity;\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/services/calendar-service.ts", "project_id": 35, "relative_order": 10, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,8 +1,9 @@\n import { TestBed } from '@angular/core/testing';\n import { provideZonelessChangeDetection } from '@angular/core';\n import { UserInputService } from './user-input-service';\n+import { SelectableDayType, VacDay } from './calendar-service';\n \n-describe('UserInput', () => {\n+describe('UserInputService', () => {\n \tlet service: UserInputService;\n \n \tbeforeEach(() => {\n@@ -16,19 +17,41 @@ describe('UserInput', () => {\n \t\texpect(service).toBeTruthy();\n \t});\n \n-\tit('should initialize with random vacation numbers', () => {\n+\tit('should initialize with vacation days array', () => {\n \t\tconst vacations = service.vacationNumberSignal();\n-\t\texpect(vacations.cp).toBeGreaterThanOrEqual(1);\n-\t\texpect(vacations.cp).toBeLessThanOrEqual(9);\n-\t\texpect(vacations.rtt).toBeGreaterThanOrEqual(1);\n-\t\texpect(vacations.rtt).toBeLessThanOrEqual(9);\n-\t\texpect(vacations.other).toBeGreaterThanOrEqual(1);\n-\t\texpect(vacations.other).toBeLessThanOrEqual(9);\n+\t\texpect(Array.isArray(vacations)).toBe(true);\n+\t\texpect(vacations.length).toBeGreaterThan(0);\n+\t});\n+\n+\tit('should have vacation days with valid properties', () => {\n+\t\tconst vacations = service.vacationNumberSignal();\n+\t\tvacations.forEach((day) => {\n+\t\t\texpect(day.id).toBeDefined();\n+\t\t\texpect(day.type).toBeDefined();\n+\t\t\texpect(day.numberOfDays).toBeGreaterThanOrEqual(1);\n+\t\t\texpect(day.numberOfDays).toBeLessThanOrEqual(9);\n+\t\t\texpect(day.expiryDate).toBeInstanceOf(Date);\n+\t\t});\n \t});\n \n \tit('should update vacation number signal', () => {\n-\t\tconst newVacations = { cp: 5, rtt: 3, other: 2 };\n+\t\tconst newVacations: VacDay[] = [\n+\t\t\t{\n+\t\t\t\tid: 'test-id',\n+\t\t\t\ttype: SelectableDayType.CP,\n+\t\t\t\tnumberOfDays: 5,\n+\t\t\t\texpiryDate: new Date(),\n+\t\t\t},\n+\t\t];\n \t\tservice.vacationNumberSignal.set(newVacations);\n \t\texpect(service.vacationNumberSignal()).toEqual(newVacations);\n \t});\n+\n+\tit('should create a new vacation day', () => {\n+\t\tconst newDay = service.createVacationDay();\n+\t\texpect(newDay.id).toBeDefined();\n+\t\texpect(newDay.type).toBe(SelectableDayType.CP);\n+\t\texpect(newDay.numberOfDays).toBe(1);\n+\t\texpect(newDay.expiryDate).toBeInstanceOf(Date);\n+\t});\n });\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/services/user-input-service.spec.ts", "project_id": 35, "relative_order": 11, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,5 +1,5 @@\n import { Injectable, signal } from '@angular/core';\n-import { VacationsNumber } from './calendar-service';\n+import { SelectableDayType, VacDay } from './calendar-service';\n \n @Injectable({\n \tprovidedIn: 'root',\n@@ -9,18 +9,39 @@ export class UserInputService {\n \t\treturn Math.floor(Math.random() * 9) + 1;\n \t}\n \n-\tprivate getInitialValues(): VacationsNumber {\n+\tprivate getInitialValues(): VacDay[] {\n+\t\treturn [\n+\t\t\t{\n+\t\t\t\tid: crypto.randomUUID(),\n+\t\t\t\ttype: SelectableDayType.CP,\n+\t\t\t\tnumberOfDays: this.getRandomNumber(),\n+\t\t\t\t// today + 365 days\n+\t\t\t\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\n+\t\t\t},\n+\t\t\t{\n+\t\t\t\tid: crypto.randomUUID(),\n+\t\t\t\ttype: SelectableDayType.RTT,\n+\t\t\t\tnumberOfDays: this.getRandomNumber(),\n+\t\t\t\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\n+\t\t\t},\n+\t\t\t{\n+\t\t\t\tid: crypto.randomUUID(),\n+\t\t\t\ttype: SelectableDayType.OTHER,\n+\t\t\t\tnumberOfDays: this.getRandomNumber(),\n+\t\t\t\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\n+\t\t\t},\n+\t\t];\n+\t}\n+\n+\tpublic createVacationDay(): VacDay {\n \t\treturn {\n-\t\t\tcp: this.getRandomNumber(),\n-\t\t\trtt: this.getRandomNumber(),\n-\t\t\tother: this.getRandomNumber(),\n+\t\t\tid: crypto.randomUUID(),\n+\t\t\ttype: SelectableDayType.CP,\n+\t\t\tnumberOfDays: 1,\n+\t\t\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\n \t\t};\n \t}\n \n-\treadonly vacationNumberSignal = signal(this.getInitialValues());\n-\treadonly remainingVacationDaysSignal = signal({\n-\t\tcp: 0,\n-\t\trtt: 0,\n-\t\tother: 0,\n-\t});\n+\treadonly vacationNumberSignal = signal(this.getInitialValues());\n+\treadonly remainingVacationDaysSignal = signal([]);\n }\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/services/user-input-service.ts", "project_id": 35, "relative_order": 12, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -11,6 +11,7 @@ import {\n \twithEventReplay,\n \twithIncrementalHydration,\n } from '@angular/platform-browser';\n+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\n \n export const appConfig: ApplicationConfig = {\n \tproviders: [\n@@ -18,5 +19,6 @@ export const appConfig: ApplicationConfig = {\n \t\tprovideZonelessChangeDetection(),\n \t\tprovideRouter(routes),\n \t\tprovideClientHydration(withEventReplay(), withIncrementalHydration()),\n+\t\tprovideAnimationsAsync(),\n \t],\n };\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "src/app/app.config.ts", "project_id": 35, "relative_order": 13, "renamed_file": false, "too_large": false }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": true, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "package-lock.json", "project_id": 35, "relative_order": 14, "renamed_file": false, "too_large": true }, { "a_mode": "100644", "b_mode": "100644", "binary": false, "deleted_file": false, "diff": "@@ -1,70 +1,71 @@\n {\n-\t\"name\": \"vac-optimizer\",\n-\t\"version\": \"1.5.0\",\n-\t\"scripts\": {\n-\t\t\"ng\": \"ng\",\n-\t\t\"start\": \"ng serve\",\n-\t\t\"build\": \"ng build\",\n-\t\t\"watch\": \"ng build --watch --configuration development\",\n-\t\t\"test\": \"ng test\",\n-\t\t\"serve:ssr:vac-optimizer\": \"node dist/vac-optimizer/server/server.mjs\",\n-\t\t\"lint\": \"eslint \\\"src/**/*.ts\\\"\",\n-\t\t\"lint:fix\": \"eslint \\\"src/**/*.ts\\\" --fix\",\n-\t\t\"format\": \"prettier --write \\\"src/**/*.{ts,html,scss,css,json}\\\"\",\n-\t\t\"format:check\": \"prettier --check \\\"src/**/*.{ts,html,scss,css,json}\\\"\"\n-\t},\n-\t\"prettier\": {\n-\t\t\"printWidth\": 100,\n-\t\t\"singleQuote\": true,\n-\t\t\"overrides\": [\n-\t\t\t{\n-\t\t\t\t\"files\": \"*.html\",\n-\t\t\t\t\"options\": {\n-\t\t\t\t\t\"parser\": \"angular\"\n-\t\t\t\t}\n-\t\t\t}\n-\t\t]\n-\t},\n-\t\"private\": true,\n-\t\"dependencies\": {\n-\t\t\"@angular/cdk\": \"^20.2.12\",\n-\t\t\"@angular/common\": \"^20.3.10\",\n-\t\t\"@angular/compiler\": \"^20.3.10\",\n-\t\t\"@angular/core\": \"^20.3.10\",\n-\t\t\"@angular/forms\": \"^20.3.10\",\n-\t\t\"@angular/material\": \"^20.2.12\",\n-\t\t\"@angular/platform-browser\": \"^20.3.10\",\n-\t\t\"@angular/platform-server\": \"^20.3.10\",\n-\t\t\"@angular/router\": \"^20.3.10\",\n-\t\t\"@angular/ssr\": \"^20.3.10\",\n-\t\t\"date-holidays\": \"^3.26.2\",\n-\t\t\"dayjs\": \"^1.11.19\",\n-\t\t\"express\": \"^5.1.0\",\n-\t\t\"rxjs\": \"~7.8.0\",\n-\t\t\"tslib\": \"^2.3.0\"\n-\t},\n-\t\"devDependencies\": {\n-\t\t\"@angular/build\": \"^20.3.10\",\n-\t\t\"@angular/cli\": \"^20.3.10\",\n-\t\t\"@angular/compiler-cli\": \"^20.3.10\",\n-\t\t\"@eslint/js\": \"^9.39.1\",\n-\t\t\"@types/express\": \"^5.0.1\",\n-\t\t\"@types/jasmine\": \"~5.1.0\",\n-\t\t\"@types/jest\": \"^30.0.0\",\n-\t\t\"@types/node\": \"^24.10.0\",\n-\t\t\"eslint\": \"^9.39.1\",\n-\t\t\"eslint-config-prettier\": \"^10.1.8\",\n-\t\t\"eslint-plugin-prettier\": \"^5.5.4\",\n-\t\t\"jasmine-core\": \"~5.12.1\",\n-\t\t\"jest\": \"^30.2.0\",\n-\t\t\"jest-preset-angular\": \"^15.0.3\",\n-\t\t\"karma\": \"~6.4.0\",\n-\t\t\"karma-chrome-launcher\": \"~3.2.0\",\n-\t\t\"karma-coverage\": \"~2.2.0\",\n-\t\t\"karma-jasmine\": \"~5.1.0\",\n-\t\t\"karma-jasmine-html-reporter\": \"~2.1.0\",\n-\t\t\"prettier\": \"^3.6.2\",\n-\t\t\"typescript\": \"~5.9.2\",\n-\t\t\"typescript-eslint\": \"^8.46.4\"\n-\t}\n+ \"name\": \"vac-optimizer\",\n+ \"version\": \"1.5.0\",\n+ \"scripts\": {\n+ \"ng\": \"ng\",\n+ \"start\": \"ng serve\",\n+ \"build\": \"ng build\",\n+ \"watch\": \"ng build --watch --configuration development\",\n+ \"test\": \"ng test\",\n+ \"serve:ssr:vac-optimizer\": \"node dist/vac-optimizer/server/server.mjs\",\n+ \"lint\": \"eslint \\\"src/**/*.ts\\\"\",\n+ \"lint:fix\": \"eslint \\\"src/**/*.ts\\\" --fix\",\n+ \"format\": \"prettier --write \\\"src/**/*.{ts,html,scss,css,json}\\\"\",\n+ \"format:check\": \"prettier --check \\\"src/**/*.{ts,html,scss,css,json}\\\"\"\n+ },\n+ \"prettier\": {\n+ \"printWidth\": 100,\n+ \"singleQuote\": true,\n+ \"overrides\": [\n+ {\n+ \"files\": \"*.html\",\n+ \"options\": {\n+ \"parser\": \"angular\"\n+ }\n+ }\n+ ]\n+ },\n+ \"private\": true,\n+ \"dependencies\": {\n+ \"@angular/animations\": \"^21.0.1\",\n+ \"@angular/cdk\": \"^21.0.1\",\n+ \"@angular/common\": \"^21.0.1\",\n+ \"@angular/compiler\": \"^21.0.1\",\n+ \"@angular/core\": \"^21.0.1\",\n+ \"@angular/forms\": \"^21.0.1\",\n+ \"@angular/material\": \"^21.0.1\",\n+ \"@angular/platform-browser\": \"^21.0.1\",\n+ \"@angular/platform-server\": \"^21.0.1\",\n+ \"@angular/router\": \"^21.0.1\",\n+ \"@angular/ssr\": \"^21.0.1\",\n+ \"date-holidays\": \"^3.26.2\",\n+ \"dayjs\": \"^1.11.19\",\n+ \"express\": \"^5.1.0\",\n+ \"rxjs\": \"~7.8.0\",\n+ \"tslib\": \"^2.3.0\"\n+ },\n+ \"devDependencies\": {\n+ \"@angular/build\": \"^21.0.1\",\n+ \"@angular/cli\": \"^21.0.1\",\n+ \"@angular/compiler-cli\": \"^21.0.1\",\n+ \"@eslint/js\": \"^9.39.1\",\n+ \"@types/express\": \"^5.0.1\",\n+ \"@types/jasmine\": \"~5.1.0\",\n+ \"@types/jest\": \"^30.0.0\",\n+ \"@types/node\": \"^24.10.0\",\n+ \"eslint\": \"^9.39.1\",\n+ \"eslint-config-prettier\": \"^10.1.8\",\n+ \"eslint-plugin-prettier\": \"^5.5.4\",\n+ \"jasmine-core\": \"~5.12.1\",\n+ \"jest\": \"^30.2.0\",\n+ \"jest-preset-angular\": \"^15.0.3\",\n+ \"karma\": \"~6.4.0\",\n+ \"karma-chrome-launcher\": \"~3.2.0\",\n+ \"karma-coverage\": \"~2.2.0\",\n+ \"karma-jasmine\": \"~5.1.0\",\n+ \"karma-jasmine-html-reporter\": \"~2.1.0\",\n+ \"prettier\": \"^3.6.2\",\n+ \"typescript\": \"~5.9.2\",\n+ \"typescript-eslint\": \"^8.46.4\"\n+ }\n }\n", "encoded_file_path": false, "external_diff_offset": null, "external_diff_size": null, "generated": false, "merge_request_diff_id": 117, "new_file": false, "new_path": null, "old_path": "package.json", "project_id": 35, "relative_order": 15, "renamed_file": false, "too_large": false } ], "metadata": { "base_commit_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "commits_count": 7, "created_at": "2026-05-03T16:00:00.880965", "diff_type": 1, "files_count": 16, "head_commit_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "id": 117, "merge_request_id": 117, "project_id": 35, "start_commit_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "state": "collected", "stored_externally": null, "updated_at": "2026-05-03T16:00:01.50577" } } ], "discussions": [ { "id": "5ba409d22fa8b52a5ecf8e4a9a6dfe39c4bf8331", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "requested review from `@Copilot`", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:11:41.000Z", "id": 486, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2025-11-29T18:11:41.000Z" } ], "resolvable": false }, { "id": "3caed3d5f92329d595fdcc3a5c42dc315e8e1a2f", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The mat-select event handler is incorrect. The `valueChange` event emits the new value directly, not an event object. Change `$event.value` to just `$event`, or use the proper approach by updating the day object and emitting it. The current code will emit `undefined`.\n```suggestion:-0+0\n\t\t\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:14.000Z", "id": 285, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "50a41af89022b0623ab74427bfce75a0a82c3d28", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of an input signal's value is not allowed and violates Angular's signal immutability principle. Instead, create a new object with the updated property. Replace lines 93-94 with: `this.dayChange.emit({ ...this.day(), numberOfDays: value });`\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), numberOfDays: value });\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:15.000Z", "id": 286, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "dff0aad8188fcd88a1d0c2ef4076c3d1299ea43d", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of an input signal's value is not allowed and violates Angular's signal immutability principle. Instead, create a new object with the updated property. Replace lines 98-99 with: `this.dayChange.emit({ ...this.day(), expiryDate: event.value! });`\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n\t}\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:15.000Z", "id": 287, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "0c69003a6c9493b69e39ba99d31da76fe30300c4", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Directly mutating the array returned from a signal violates signal immutability. Use the signal's `update` method instead: `this.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);`\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:15.000Z", "id": 288, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "f0b103b136fdcf50706e94dab767a1aa16658698", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Using `any` type defeats TypeScript's type safety. The parameter type should match the actual data structure being used. Based on the code, this should be `VacationsNumber` (with properties cp, rtt, other), not the new `VacDay[]` type.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:16.000Z", "id": 283, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "04b1c5e0799a7d2164512b3360e2a4df08b68136", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Using `any` type defeats TypeScript's type safety. The parameter type should be `VacationsNumber` (with properties cp, rtt, other) to match how it's used throughout the method.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:16.000Z", "id": 284, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "7e3b58c13aafbd18f5c54ed61a40729693d42eb5", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Directly mutating the array returned from a signal violates signal immutability. Use the signal's `update` method instead: `this.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));`\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:16.000Z", "id": 290, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "5c39376514e132d47e3031b1154e1f4bb3d55590", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Type mismatch: `vacationData` is `VacDay[]` from the signal, but `optimizeVacations` expects an object with `{cp: number, rtt: number, other: number}` properties. The spread operator `{ ...vacationData }` will create an object with numeric keys, not the expected structure. You need to transform the `VacDay[]` into the expected format before calling `optimizeVacations`, or update `optimizeVacations` to accept the new type.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:17.000Z", "id": 292, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "a364b66c53b342a6d7f478f3b50c24f16539aea1", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "This test will fail because the component has a required input `day` that is not provided. You need to either provide the required input using `fixture.componentRef.setInput('day', mockVacDay)` before `detectChanges()`, or add `provideAnimationsAsync()` to the providers array since the component uses Material components that require animations.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:17.000Z", "id": 293, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "93543d316cffffe9f66f4b8687093cdb3da0efcb", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Empty lifecycle hook. Since `ngOnInit` has no implementation, it should be removed along with the `OnInit` interface from the class declaration (line 35).", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:17.000Z", "id": 294, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "04a0ad2d79301a9c55533101d6980a74364a6402", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Unused import. `ReactiveFormsModule` is no longer used after removing the form-based implementation. This import should be removed from both the import statement and the component's imports array (line 23).", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:17.000Z", "id": 295, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "2ba11518378e5ea27e281332f48ac420d8c691fa", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Unused import and variable. The `destroy$` Subject (line 38) is created and completed in `ngOnDestroy`, but it's never actually used (no `.pipe(takeUntil(this.destroy$))` calls remain after form removal). Both the import and the `destroy$` property can be removed, along with the `OnDestroy` interface and `ngOnDestroy` method.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:17.000Z", "id": 296, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19.000Z" } ], "resolvable": false }, { "id": "3bcebd4ea06434845dcb16fc2e72e4ae9d5046df", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "**Review:** Commented\n\n## Pull request overview\n\nThis PR refactors the vacation management system from a simple count-based approach to a more structured day-tracking system with expiry dates, and upgrades Angular from v20 to v21. Key changes include:\n- Migration from `VacationsNumber` (simple object with cp/rtt/other counts) to `VacDay[]` (array of vacation day objects with IDs, types, counts, and expiry dates)\n- Addition of async progress tracking for vacation optimization\n- New `DayInput` component for managing individual vacation day entries\n- Angular version upgrade to v21.0.1 with animations support\n\n### Reviewed changes\n\nCopilot reviewed 16 out of 17 changed files in this pull request and generated 12 comments.\n\n
\nShow a summary per file\n\n| File | Description |\r\n| ---- | ----------- |\r\n| src/main.server.ts | Adds zone change detection configuration for server-side rendering |\r\n| src/app/services/user-input-service.ts | Refactors service to use VacDay[] structure instead of VacationsNumber |\r\n| src/app/services/calendar-service.ts | Updates types, adds async optimization with progress tracking, and implements heuristic limit |\r\n| src/app/components/userInput/day-input/* | New component for managing individual vacation day inputs |\r\n| src/app/components/user-input/* | Refactored to use signal-based state with new VacDay structure |\r\n| src/app/components/calendar-year/* | Updated to handle async optimization and remaining days tracking |\r\n| package.json | Upgrades Angular from v20 to v21 and adds animations package |\n
\n\n\n\n
\nComments suppressed due to low confidence (1)\n\n**src/app/components/user-input/user-input.spec.ts:40**\n* These tests reference `component.userInputForm` which no longer exists after the refactoring. The component now uses `daysOff` signal instead of a form. These tests should be updated to test the new implementation: verify `daysOff` is defined, test `addDay()` and `removeFromList()` methods.\n```\n\tit('should initialize form with values from service', () => {\n\t\texpect(component.userInputForm).toBeDefined();\n\t\texpect(component.userInputForm.get('CP')).toBeDefined();\n\t\texpect(component.userInputForm.get('RTT')).toBeDefined();\n\t\texpect(component.userInputForm.get('Others')).toBeDefined();\n\t});\n\n\tit('should have valid form controls', () => {\n\t\tconst cpControl = component.userInputForm.get('CP');\n\t\tconst rttControl = component.userInputForm.get('RTT');\n\t\tconst othersControl = component.userInputForm.get('Others');\n\n\t\texpect(cpControl).toBeTruthy();\n\t\texpect(rttControl).toBeTruthy();\n\t\texpect(othersControl).toBeTruthy();\n\t});\n```\n
\n\n", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T18:15:18.000Z", "id": 488, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-29T18:15:18.000Z" } ], "resolvable": false }, { "id": "43a86e9824ea9eb4fc0c4dce15eb48107e59cbac", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "requested review from `@Copilot`", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:09:52.000Z", "id": 487, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2025-11-29T20:09:52.000Z" } ], "resolvable": false }, { "id": "5083bd8d88fc93d304e91aff980758f7c301d9c3", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@copilot` open a new pull request to apply changes based on the comments in [this thread](https://gitlab.nessar.fr/Nessar/vac-optimizer/-/merge_requests/61#pullrequestreview-3520781340)", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:15:39.000Z", "id": 489, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-29T20:15:39.000Z" } ], "resolvable": false }, { "id": "7656397e772292d02fec6bd4f16d7cbe46a516eb", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "mentioned in merge request !62", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:15:46.000Z", "id": 493, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2026-05-03T16:02:20.581Z" } ], "resolvable": false }, { "id": "8b8c9e6a729ae8f6e2820693acf31b37666daac8", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@THoguet` I've opened a new pull request, #62, to work on those changes. Once the pull request is ready, I'll request review from you.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:15:47.000Z", "id": 490, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-29T20:15:47.000Z" } ], "resolvable": false }, { "id": "2e713bc8f05358557662c9d60e9988b52528f691", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Input type should be \"text\" or \"date\", not \"datetime\". The \"datetime\" type is not a valid HTML input type. For date-only pickers (which this appears to be), use:\n```html\n\n```\n```suggestion:-0+0\n\t\t\ttype=\"text\"\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:33.000Z", "id": 302, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 24, "new_path": "src/app/components/userInput/day-input/day-input.html", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.html", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\t\ttype=\"datetime\"\n", "from_line": 24, "id": 11, "to_content": "\t\t\ttype=\"text\"\n", "to_line": 24 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": true, "resolved": false }, { "id": "4063b429da7483714f9dc1018dc9846a4a2d99ab", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Missing `dayChange` output binding from `app-day-input`. The `DayInput` component emits changes through the `dayChange` output, but this parent component doesn't listen to it. Without handling this event, changes made in the child component won't be properly synchronized. Add the binding:\n```html\n\n\n```\n\nHowever, note that with the current direct mutation pattern in the component, you may not need this if you address the signal mutation issues identified in other comments.\n```suggestion:-0+0\n\t\t\t\t\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:34.000Z", "id": 299, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "7418d7f17ef93b7e54070a85a7b3848ce11f927a", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of input signal object. Input signals return readonly values and should not be mutated directly. Instead, emit a new object with the updated value through `dayChange` output.\n\nConsider this approach:\n```typescript\nonInput(event: Event) {\n const value = Number((event.target as HTMLInputElement).value);\n this.dayChange.emit({ ...this.day(), numberOfDays: value });\n}\n```\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), numberOfDays: value });\n\t}\n\n\taddEvent(event: MatDatepickerInputEvent) {\n\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:34.000Z", "id": 300, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "ada2ad565ab2bac3476f115a72724036782e4531", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "[nitpick] Commented-out code should be removed. If the `mat-list-item` wrapper was removed intentionally, the closing comment should be deleted as well.\n```suggestion:-0+0\n\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:34.000Z", "id": 301, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 36, "new_path": "src/app/components/userInput/day-input/day-input.html", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.html", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\n", "from_line": 36, "id": 12, "to_content": "", "to_line": 36 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": true, "resolved": false }, { "id": "2f12fa6725549528aa23827635b025862a1b7c3c", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Empty `mat-option` without value serves no clear purpose. If this is meant to be a \"clear selection\" option, it should have a descriptive label. If not needed, remove it:\n```html\n\n @for (dayType of selectableDayTypes; track $index) {\n {{ tooltipTypeMapping[dayType] }}\n }\n\n```\n```suggestion:-0+0\n\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:34.000Z", "id": 304, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 14, "new_path": "src/app/components/userInput/day-input/day-input.html", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.html", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\t\t\n", "from_line": 14, "id": 9, "to_content": "", "to_line": 14 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": true, "resolved": false }, { "id": "164c3928a28335974b84b8538e96890fc63cac07", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of signal array. The code mutates `this.daysOff` which is the result of calling a signal (`this.userInputService.vacationNumberSignal()`), but this doesn't trigger signal updates. The mutation affects the underlying array but Angular's change detection won't be notified.\n\nThis issue is partially acknowledged in `triggerComputation()` (line 80), but the pattern of mutating and then manually triggering updates is error-prone. Consider using `update()` instead:\n```typescript\npublic addDay() {\n this.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);\n}\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:35.000Z", "id": 297, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "a60f375f2780b6240ec875c04233941e03ea0c72", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Empty `ngOnInit()` method serves no purpose and should be removed. If the `OnInit` lifecycle hook isn't needed, also remove `OnInit` from the implements clause and the import.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:35.000Z", "id": 298, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "a11d8d687ee876976b270e025d127ea9d90c49cd", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "[nitpick] Commented-out code should be removed. If the `mat-list-item` wrapper was removed intentionally, the comment should be deleted as well.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:35.000Z", "id": 303, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "a88decb911911d2a482f1c621eaa0bea3fb3a099", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Type casting `day.type as unknown as DayType` indicates a design issue. Since `SelectableDayType` is a subset of `DayType` (with values CP, RTT, OTHER), the type system should handle this naturally. Consider redesigning the types to avoid unsafe casting, or if the relationship is valid, use a direct cast: `day.type as DayType`.\n```suggestion:-0+0\n\t\t\t\tconst typeName = tooltipTypeMapping[day.type as DayType];\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:35.000Z", "id": 308, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 39, "new_path": "src/app/components/user-input/user-input.ts", "old_line": null, "old_path": "src/app/components/user-input/user-input.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\t\t\tconst typeName = tooltipTypeMapping[day.type as unknown as DayType];\n", "from_line": 39, "id": 10, "to_content": "\t\t\t\tconst typeName = tooltipTypeMapping[day.type as DayType];\n", "to_line": 39 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": true, "resolved": false }, { "id": "c4ec72019396a5fa87181e1707194a8810b7796d", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The `destroy$` Subject is defined but never used since `ngOnInit()` no longer subscribes to observables. If not needed for cleanup, remove both `destroy$` and the `OnDestroy` implementation.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:36.000Z", "id": 305, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "6b815b32e7febd4a318fa04fccee2bf7f55ee5af", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Select dropdown for day type is missing an accessible label. Screen readers won't be able to describe this field to users. Add a ``:\n```html\n\n Type of leave\n \n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:36.000Z", "id": 306, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "660d3baf903280b280fa39b6b6fd44327e58d7cb", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`::ng-deep` is deprecated and should be avoided. It pierces component encapsulation and can cause unexpected styling issues. Consider using Angular's host context or moving this style to a global stylesheet if it needs to affect Material components. Alternatively, use ViewEncapsulation.None if you need to style child components, though this is also not recommended.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:36.000Z", "id": 307, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "db7f9a124de73adf638ed2274c8c9d1614dfe02e", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Unused import in component's imports array. `ReactiveFormsModule` should be removed from the imports array since reactive forms are no longer used in this component.\n```suggestion:-0+0\n\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:36.000Z", "id": 309, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "5253b331dbe788afc4f323ce0aa899d39385178b", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of input signal object and non-null assertion without validation. Input signals should not be mutated directly, and `event.value` may be null for invalid dates.\n\nConsider this approach:\n```typescript\naddEvent(event: MatDatepickerInputEvent) {\n if (event.value) {\n this.dayChange.emit({ ...this.day(), expiryDate: event.value });\n }\n}\n```\n```suggestion:-0+0\n\t\tif (event.value) {\n\t\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value });\n\t\t}\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:36.000Z", "id": 310, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "e2d12b4c80dfea6e4158940b4e2d4b768510b41e", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Direct mutation of signal array. Similar to `addDay()`, this mutates the signal's underlying array without triggering updates. Use `update()` instead:\n```typescript\npublic removeFromList(index: number) {\n this.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n}\n```\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:37.000Z", "id": 311, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "94a2efbe55b78a4cf1befeaea906f092142341c8", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The test will fail because `DayInput` has a required input `day` that is not provided. The component requires a `VacDay` object to be passed as input.\n\nAdd the required input:\n```typescript\nbeforeEach(async () => {\n await TestBed.configureTestingModule({\n imports: [DayInput],\n }).compileComponents();\n\n fixture = TestBed.createComponent(DayInput);\n fixture.componentRef.setInput('day', {\n id: 'test-id',\n type: SelectableDayType.CP,\n numberOfDays: 5,\n expiryDate: new Date()\n });\n component = fixture.componentInstance;\n fixture.detectChanges();\n});\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:37.000Z", "id": 312, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "74a3920641cca225eac5668a4df4fa05b356064a", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Missing test provider for animations. The `DayInput` component uses Material components that depend on animations. Add `provideAnimationsAsync()` to the test configuration to avoid console warnings and potential test failures:\n```typescript\nimport { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\n\nawait TestBed.configureTestingModule({\n imports: [DayInput],\n providers: [provideAnimationsAsync()],\n}).compileComponents();\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:37.000Z", "id": 313, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": false }, { "id": "6304be20f39ab024eb6a253dd8eb1d9a5b06de66", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Shallow copy doesn't deep copy the `expiryDate` Date objects. Since `optimizeVacations()` modifies `numberOfDays` on the vacation days objects, and those objects contain mutable `Date` objects, a shallow spread may not be sufficient if dates are mutated. While the current code appears to only mutate `numberOfDays`, the comment claims \"deep copy\" but provides shallow copy.\n\nConsider using a proper deep copy or clarify the comment:\n```typescript\nconst vacationDaysCopy = vacationData.map((vd) => ({ \n ...vd, \n expiryDate: new Date(vd.expiryDate) \n}));\n```\n```suggestion:-0+0\n\t\t\t\t\t\tconst vacationDaysCopy = vacationData.map((vd) => ({\n\t\t\t\t\t\t\t...vd,\n\t\t\t\t\t\t\texpiryDate: vd.expiryDate ? new Date(vd.expiryDate) : vd.expiryDate,\n\t\t\t\t\t\t}));\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:37.000Z", "id": 317, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 49, "new_path": "src/app/components/calendar-year/calendar-year.ts", "old_line": null, "old_path": "src/app/components/calendar-year/calendar-year.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\t\t\t\t\tconst vacationDaysCopy = vacationData.map((vd) => ({ ...vd }));\n", "from_line": 49, "id": 13, "to_content": "\t\t\t\t\t\tconst vacationDaysCopy = vacationData.map((vd) => ({\n\t\t\t\t\t\t\t...vd,\n\t\t\t\t\t\t\texpiryDate: vd.expiryDate ? new Date(vd.expiryDate) : vd.expiryDate,\n\t\t\t\t\t\t}));\n", "to_line": 49 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41.000Z" } ], "resolvable": true, "resolved": false }, { "id": "204454ab035f599535b8efe5701de45e33346b6b", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Missing tests for the component's key behaviors. The test suite only verifies component creation but doesn't test the `onInput()` and `addEvent()` methods, or the output events (`dayChange`, `deleteEntry`). Given the repository has comprehensive test coverage for other components and services, consider adding tests for:\n- Input changes emitting `dayChange` event\n- Date changes emitting `dayChange` event \n- Delete button emitting `deleteEntry` event", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:38.000Z", "id": 314, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:42.000Z" } ], "resolvable": false }, { "id": "a7e5abdbeec0eb6faaf6e943874893a064b31116", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The `valueChange` event emits only the selected value, not an object with a `value` property. Accessing `$event.value` will result in `undefined`. Also, emitting just the type value instead of updating the entire `VacDay` object doesn't align with how other changes are handled.\n\nUse:\n```html\n\n```\n\nOr better, follow the pattern of not mutating inputs:\n```html\n\n```\n```suggestion:-0+0\n\t\t\n```", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:38.000Z", "id": 316, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:42.000Z" } ], "resolvable": false }, { "id": "3efd62c46ec1a8f490cf2e2edfdcf1841a45725a", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "No validation for negative or invalid number inputs. The HTML has `min=\"0\"`, but the `Number()` conversion can still produce negative values or `NaN` if the user enters invalid text. Consider adding validation:\n```typescript\nonInput(event: Event) {\n const value = Number((event.target as HTMLInputElement).value);\n if (isNaN(value) || value < 0) return;\n this.dayChange.emit({ ...this.day(), numberOfDays: value });\n}\n```\n```suggestion:-0+0\n\t\tconst value = Number((event.target as HTMLInputElement).value);\n\t\tif (isNaN(value) || value < 0) return;\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:38.000Z", "id": 321, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 92, "new_path": "src/app/components/userInput/day-input/day-input.ts", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\tconst value = Number((event.target as HTMLInputElement).value);\n", "from_line": 92, "id": 17, "to_content": "\t\tconst value = Number((event.target as HTMLInputElement).value);\n\t\tif (isNaN(value) || value < 0) return;\n", "to_line": 92 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:42.000Z" } ], "resolvable": true, "resolved": false }, { "id": "b19a66e4b0892155f4dc5f49b37469d42d772b35", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Type casting `vacDay.type as unknown as DayType` indicates a design issue. Since `SelectableDayType` enum values are defined as equal to `DayType` values (lines 207-211), a direct cast should work: `vacDay.type as DayType`. The double cast through `unknown` suggests the types aren't properly aligned.\n```suggestion:-0+0\n\t\tconst vacType = vacDay.type as DayType;\n```", "commands_changes": {}, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": false, "created_at": "2025-11-29T20:16:38.000Z", "id": 323, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 858, "new_path": "src/app/services/calendar-service.ts", "old_line": null, "old_path": "src/app/services/calendar-service.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\tconst vacType = vacDay.type as unknown as DayType;\n", "from_line": 858, "id": 16, "to_content": "\t\tconst vacType = vacDay.type as DayType;\n", "to_line": 858 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:42.000Z" } ], "resolvable": true, "resolved": false }, { "id": "1d9ac7e37b50d40dfadc013c291c83da010c5cb4", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "**Review:** Commented\n\n## Pull request overview\n\nCopilot reviewed 13 out of 14 changed files in this pull request and generated 22 comments.\n\n\n\n", "commands_changes": {}, "confidential": false, "created_at": "2025-11-29T20:16:39.000Z", "id": 492, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-29T20:16:39.000Z" } ], "resolvable": false }, { "id": "455602e37aceae09683cf3eed40c50960f6c1784", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@copilot` fix pipeline\r\n", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T01:47:17.000Z", "id": 491, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-30T01:47:17.000Z" } ], "resolvable": false }, { "id": "a0e32bb1ffefb56c45be223f1753bc05656bf28c", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "mentioned in merge request !63", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T01:47:25.000Z", "id": 497, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2026-05-03T16:02:20.950Z" } ], "resolvable": false }, { "id": "f9b7ab79e0ad436a3773e140cac27afaa267fe49", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@THoguet` I've opened a new pull request, #63, to work on those changes. Once the pull request is ready, I'll request review from you.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T01:47:27.000Z", "id": 494, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-30T01:47:27.000Z" } ], "resolvable": false }, { "id": "19584670a79e0d0b4c442c9d099405b0fdcb99d3", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "requested review from `@Copilot`", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T01:59:05.000Z", "id": 496, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2025-11-30T01:59:05.000Z" } ], "resolvable": false }, { "id": "2ba7594cc2f70d3a0fac2ebd65cc9bd6968d5d72", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@copilot` fix unit test and linter", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:06:25.000Z", "id": 495, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-30T02:06:25.000Z" } ], "resolvable": false }, { "id": "cb1d519561ee336fe12ac9006b045c8293006e90", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "mentioned in merge request !64", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:06:34.000Z", "id": 500, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": true, "type": null, "updated_at": "2026-05-03T16:02:21.534Z" } ], "resolvable": false }, { "id": "759fe2d8ffb50e2c039ac076aab31903760f3214", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "`@THoguet` I've opened a new pull request, #64, to work on those changes. Once the pull request is ready, I'll request review from you.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:06:35.000Z", "id": 498, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-30T02:06:35.000Z" } ], "resolvable": false }, { "id": "faccdc68c3122f98db4829852b47706fa6c0948d", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The `daysOff` getter directly returns a signal instead of its value. This is inconsistent with Angular best practices for signals. Consider either making this a computed signal or calling the signal to return its value, e.g., `return this.userInputService.vacationNumberSignal();`", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:11.000Z", "id": 315, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "76afbb7677f332efabeed44167085ab82a209516", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The clickable card uses `(click)=\"addDay()\"` on a `mat-card` element instead of the button inside it. This creates a redundant click handler since the button inside already has keyboard accessibility. The card itself isn't keyboard-accessible unless additional attributes like `tabindex=\"0\"` and `(keydown.enter)` or `(keydown.space)` are added. Consider moving the click handler to just the button, or add proper keyboard event handlers to the card.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:12.000Z", "id": 318, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "8845366b3cc593bf7017a0afd2564884f59b17ed", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The number input field lacks a label, making it inaccessible to screen reader users. Consider adding a `mat-label` inside the `mat-form-field` to describe what this input represents (e.g., \"Number of days\").", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:12.000Z", "id": 319, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "da71a155ac1c9d7c97b2d6b8fa2577b9a246b27b", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The non-null assertion operator `!` is used on `event.value`, but there's no guarantee that `event.value` is non-null. If the user clears the date field, `event.value` could be `null`, causing a runtime error. Consider handling the null case explicitly, e.g., by not emitting if the value is null, or using a default date.\n```suggestion:-0+0\n\t\tif (event.value !== null) {\n\t\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value });\n\t\t}\n```", "commands_changes": {}, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": false, "created_at": "2025-11-30T02:07:12.000Z", "id": 324, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 101, "new_path": "src/app/components/userInput/day-input/day-input.ts", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n", "from_line": 101, "id": 18, "to_content": "\t\tif (event.value !== null) {\n\t\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value });\n\t\t}\n", "to_line": 101 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": true, "resolved": false }, { "id": "e57d09ac1c9a403401a14e733a29d7e740226762", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "[nitpick] The explicit type annotation `: UserInputService` is redundant when using `inject()`, as TypeScript can infer the type. Consider simplifying to: `private userInputService = inject(UserInputService);`\n```suggestion:-0+0\n\tprivate userInputService = inject(UserInputService);\n```", "commands_changes": {}, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": false, "created_at": "2025-11-30T02:07:12.000Z", "id": 326, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 27, "new_path": "src/app/components/user-input/user-input.ts", "old_line": null, "old_path": "src/app/components/user-input/user-input.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\tprivate userInputService: UserInputService = inject(UserInputService);\n", "from_line": 27, "id": 19, "to_content": "\tprivate userInputService = inject(UserInputService);\n", "to_line": 27 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": true, "resolved": false }, { "id": "6ad84f6ed6c0369d36c14d0c57ab5be1059f9f4c", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "This line has the same date calculation issue. The duplicated logic appears 4 times in this file (lines 19, 25, 31, and 41). Extract this into a helper method like `private getExpiryDateOneYearFromNow(): Date` to eliminate duplication.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:13.000Z", "id": 320, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "8b41e355df4ce527a6a48f22e74408318dd3a4e7", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Commented-out code should be removed rather than left in the codebase. If this `mat-list-item` wrapper is no longer needed, delete it. If it might be needed in the future, rely on version control to retrieve it.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:13.000Z", "id": 322, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "d3ad97fb1423601f830fccb9e78f254d3eab6cbc", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Commented-out code should be removed. See comment on line 1.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:13.000Z", "id": 325, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "4d1e419e0f5ebb3f3834621229c4b19a3b6aa6ab", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The type cast `day.type as unknown as DayType` is a code smell. Since `SelectableDayType` values are derived from `DayType`, this cast should be unnecessary. Consider refactoring the type relationship or using a type-safe mapping function.\n```suggestion:-0+0\n\t\t\t\tconst typeName = isDayType(day.type)\n\t\t\t\t\t? tooltipTypeMapping[day.type]\n\t\t\t\t\t: 'Unknown';\n```", "commands_changes": {}, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": false, "created_at": "2025-11-30T02:07:13.000Z", "id": 328, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 39, "new_path": "src/app/components/user-input/user-input.ts", "old_line": null, "old_path": "src/app/components/user-input/user-input.ts", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "\t\t\t\tconst typeName = tooltipTypeMapping[day.type as unknown as DayType];\n", "from_line": 39, "id": 15, "to_content": "\t\t\t\tconst typeName = isDayType(day.type)\n\t\t\t\t\t? tooltipTypeMapping[day.type]\n\t\t\t\t\t: 'Unknown';\n", "to_line": 39 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": true, "resolved": false }, { "id": "02f44efb1258265012bd52c9213200ecdcc3b429", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The component has been significantly refactored with new methods (`addDay`, `removeFromList`, `updateDay`, `triggerComputation`) and computed signals, but the test file only adds `provideAnimationsAsync()`. Consider adding tests to verify the new functionality works correctly.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:14.000Z", "id": 327, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "fb8c9462b424cb123af8f99cb74024b6ee4eeac2", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Using `::ng-deep` is deprecated and discouraged in Angular. It's a workaround that pierces view encapsulation and can cause global style pollution. Consider using Angular Material's theming system or custom form field appearance options instead to hide the subscript wrapper.\n```suggestion:-0+0\n/* To hide the subscript wrapper, add a custom class to your mat-form-field in the template, e.g. */\n/* Then use the following selector: */\n.no-subscript .mat-mdc-form-field-subscript-wrapper {\n```", "commands_changes": {}, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": false, "created_at": "2025-11-30T02:07:14.000Z", "id": 329, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "position": { "base_sha": "3272622abfbe608161aa51332e21b1cb23e3af93", "head_sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "line_range": null, "new_line": 19, "new_path": "src/app/components/userInput/day-input/day-input.scss", "old_line": null, "old_path": "src/app/components/userInput/day-input/day-input.scss", "position_type": "text", "start_sha": "3272622abfbe608161aa51332e21b1cb23e3af93" }, "project_id": 35, "resolvable": true, "resolved": false, "resolved_at": null, "resolved_by": null, "suggestions": [ { "appliable": false, "applied": false, "from_content": "::ng-deep .mat-mdc-form-field-subscript-wrapper {\n", "from_line": 19, "id": 14, "to_content": "/* To hide the subscript wrapper, add a custom class to your mat-form-field in the template, e.g. */\n/* Then use the following selector: */\n.no-subscript .mat-mdc-form-field-subscript-wrapper {\n", "to_line": 19 } ], "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": true, "resolved": false }, { "id": "681ee639bd51834d741e238c9729248515c3977c", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The date calculation creates multiple `new Date()` objects unnecessarily. The expression `new Date(new Date().setFullYear(...))` first creates a Date, mutates it with `setFullYear` (which returns a timestamp), then creates a new Date from that timestamp. This is convoluted. Use a clearer approach: `const date = new Date(); date.setFullYear(date.getFullYear() + 1); return date;` or simply `new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)`", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:14.000Z", "id": 330, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "d74352aa2000f9dde96d5fd76b81b2ca70187d9b", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "This line has the same date calculation issue as the previous occurrences. See comments on lines 19, 25, and 31.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:15.000Z", "id": 331, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "0f3672abec1f39144599e81597215f2992daea76", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "The test suite only includes a basic creation test. The component has several behavioral methods (`onInput`, `onTypeChange`, `addEvent`) that lack test coverage. Consider adding tests to verify:\n- `onInput` emits `dayChange` with updated `numberOfDays`\n- `onTypeChange` emits `dayChange` with updated `type`\n- `addEvent` emits `dayChange` with updated `expiryDate`\n- `deleteEntry` output works correctly", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:15.000Z", "id": 332, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "bb6675dcb5dc016ce9452832cf55f2419bd9befe", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "On each iteration of the while loop, the code creates a copy of `vacationDays`, filters it, and sorts it. This is inefficient for large arrays. Consider moving the filtering and sorting outside the loop, or using a more efficient data structure like a priority queue to avoid repeated sorting.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:15.000Z", "id": 333, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "81650705b6a374188a977cf37808b48042490202", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "This line has the same date calculation issue as line 19. Consider extracting this logic into a helper method to avoid duplication and improve maintainability.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:15.000Z", "id": 334, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "f159bf413193357c908e065fe7f958ed35ed0bc0", "individual_note": false, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "Adding `--legacy-peer-deps` flag suggests there are peer dependency conflicts with Angular 21. While this may work, it bypasses npm's peer dependency validation which could hide compatibility issues. Consider documenting why this flag is necessary, or investigate resolving the underlying peer dependency conflicts.", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:16.000Z", "id": 335, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17.000Z" } ], "resolvable": false }, { "id": "c1afd237d0459e7fd0d0cf55085c5995d1da07c6", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "**Review:** Commented\n\n## Pull request overview\n\nCopilot reviewed 14 out of 15 changed files in this pull request and generated 19 comments.\n\n\n
\nComments suppressed due to low confidence (1)\n\n**src/app/services/calendar-service.ts:925**\n* The `lookForVacation` method has been modified to filter candidate days based on the expiry date of the `VacDay` object. This new behavior lacks test coverage to ensure days after the expiry date are correctly excluded from consideration.\n```\n\tlookForVacation(\n\t\tvacDay: VacDay,\n\t\tcalendarService: CalendarService,\n\t): { apply: () => void; heuristic: number } {\n\t\tconst daysOfYear = calendarService.monthsInAYearFromNow();\n\t\tconst now = new Date();\n\t\tnow.setHours(0, 0, 0, 0);\n\t\tconst nowTime = now.getTime();\n\t\tconst expiryTime = vacDay.expiryDate.getTime();\n\n\t\t// Preprocess: filter out past days, already selected days, and days after expiry date\n\t\tconst candidateDays: Date[] = [];\n\t\tfor (const month of daysOfYear) {\n\t\t\tfor (const week of month) {\n\t\t\t\tfor (const day of week) {\n\t\t\t\t\tif (day) {\n\t\t\t\t\t\tday.setHours(0, 0, 0, 0);\n\t\t\t\t\t\tconst dayTime = day.getTime();\n\t\t\t\t\t\tif (dayTime >= nowTime && dayTime <= expiryTime && !this.isSelected(day)) {\n\t\t\t\t\t\t\tcandidateDays.push(day);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Early exit if no candidates\n\t\tif (candidateDays.length === 0) {\n\t\t\treturn { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };\n\t\t}\n\n\t\tconst vacType = vacDay.type as unknown as DayType;\n\t\tconst tempDatesBackup = [...this._datesSelected];\n\t\tlet bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;\n\t\tlet bestHeuristic = -Infinity;\n\t\tlet daysWithBestHeuristic = 0;\n\n\t\t// Evaluate each candidate day\n\t\tfor (const day of candidateDays) {\n\t\t\t// Reset to backup state\n\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\tthis.datesSelected = this._datesSelected.slice();\n\t\t\tthis.grouping();\n\n\t\t\t// Mark this day as vacation (reuse the day object since it's already normalized)\n\t\t\tthis.datesSelected.push(new SelectedDate(vacType, new DateRange(day, day)));\n\n\t\t\tconst heuristic = this.computeHeuristics();\n\n\t\t\t// Track only if better than current best\n\t\t\tif (heuristic > bestHeuristic) {\n\t\t\t\tbestHeuristic = heuristic;\n\t\t\t\tdaysWithBestHeuristic = 1;\n\t\t\t\tconst capturedDay = day;\n\t\t\t\tbestStrategy = {\n\t\t\t\t\tapply: () => {\n\t\t\t\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\t\t\t\tthis.push(\n\t\t\t\t\t\t\tnew SelectedDate(\n\t\t\t\t\t\t\t\tvacType,\n\t\t\t\t\t\t\t\tnew DateRange(capturedDay, capturedDay),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\theuristic: heuristic,\n\t\t\t\t\tvalue: capturedDay,\n\t\t\t\t};\n\t\t\t} else if (heuristic === bestHeuristic) {\n\t\t\t\tdaysWithBestHeuristic++;\n\t\t\t}\n\t\t}\n\n\t\t// Check if too many days have the same best heuristic after evaluating all days\n\t\tif (daysWithBestHeuristic > SelectedDates.MAX_DAYS_WITH_SAME_HEURISTIC) {\n\t\t\t// Restore backup before stopping\n\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\tthis.datesSelected = this._datesSelected.slice();\n\t\t\tthis.grouping();\n\n\t\t\t// Stop optimization - let user decide manually\n\t\t\treturn {\n\t\t\t\tapply: () => {},\n\t\t\t\theuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC,\n\t\t\t};\n\t\t}\n\n\t\t// Restore backup before returning\n\t\tthis._datesSelected = [...tempDatesBackup];\n\t\tthis.datesSelected = this._datesSelected.slice();\n\t\tthis.grouping();\n\n\t\t// Apply best strategy if found\n\t\tif (bestStrategy) {\n\t\t\tbestStrategy.apply();\n\t\t\treturn bestStrategy;\n\t\t}\n\n\t\treturn { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };\n\t}\n```\n
\n\n", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:07:16.000Z", "id": 499, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2025-11-30T02:07:16.000Z" } ], "resolvable": false }, { "id": "1f07e2584a87d1dda1ec8eb9129921262cee2c06", "individual_note": true, "notes": [ { "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "body": "*Merged by: THoguet at 2025-11-30 02:17:57 UTC*", "commands_changes": {}, "confidential": false, "created_at": "2025-11-30T02:17:57.000Z", "id": 501, "imported": true, "imported_from": "github", "internal": false, "noteable_id": 117, "noteable_iid": 61, "noteable_type": "MergeRequest", "project_id": 35, "resolvable": false, "system": false, "type": null, "updated_at": "2026-05-03T16:02:21.635Z" } ], "resolvable": false } ], "format_version": 1, "notes": [ { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T18:11:41", "discussion_id": "5ba409d22fa8b52a5ecf8e4a9a6dfe39c4bf8331", "id": 486, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "requested review from `@Copilot`", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2025-11-29T18:11:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:14", "discussion_id": "3caed3d5f92329d595fdcc3a5c42dc315e8e1a2f", "id": 285, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_13", "namespace_id": null, "note": "The mat-select event handler is incorrect. The `valueChange` event emits the new value directly, not an event object. Change `$event.value` to just `$event`, or use the proper approach by updating the day object and emitting it. The current code will emit `undefined`.\n```suggestion:-0+0\n\t\t\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,36 @@\\n+\\n+\\n+\\t\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\t\"\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:15", "discussion_id": "50a41af89022b0623ab74427bfce75a0a82c3d28", "id": 286, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_94", "namespace_id": null, "note": "Direct mutation of an input signal's value is not allowed and violates Angular's signal immutability principle. Instead, create a new object with the updated property. Replace lines 93-94 with: `this.dayChange.emit({ ...this.day(), numberOfDays: value });`\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), numberOfDays: value });\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,101 @@\\n+import { Component, input, output, ChangeDetectionStrategy\n } from '@angular/core';\\n+import { SelectableDayType, tooltipTypeMapping, VacDay\n } from '../../../services/calendar-service';\\n+import { MatCard } from '@angular/material/card';\\n+import\n { MatOption, MatSelect } from '@angular/material/select';\\n+import { MatInput, MatInputModule,\n MatLabel } from '@angular/material/input';\\n+import {\\n+\\tMatDatepicker,\\n+\\tMatDatepickerInput,\\n+\\tMatDatepickerInputEvent,\\n+\\tMatDatepickerToggle,\\n+}\n from '@angular/material/datepicker';\\n+import {\\n+\\tDateAdapter,\\n+\\tMAT_DATE_FORMATS,\\n+\\tMAT_DATE_LOCALE,\\n+\\tNativeDateAdapter,\\n+}\n from '@angular/material/core';\\n+import { MatIconModule } from '@angular/material/icon';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+\\n+class CustomDateAdapter\n extends NativeDateAdapter {\\n+\\toverride format(date: Date, displayFormat: unknown):\n string {\\n+\\t\\tif (displayFormat === 'input') {\\n+\\t\\t\\tconst day = date.getDate().toString().padStart(2,\n '0');\\n+\\t\\t\\tconst month = (date.getMonth() + 1).toString().padStart(2, '0');\\n+\\t\\t\\tconst\n year = date.getFullYear();\\n+\\t\\t\\treturn `${day}/${month}/${year}`;\\n+\\t\\t}\\n+\\t\\treturn\n super.format(date, displayFormat as object);\\n+\\t}\\n+\\n+\\toverride parse(value:\n string): Date | null {\\n+\\t\\tif (!value) return null;\\n+\\t\\tconst parts = value.split('/');\\n+\\t\\tif\n (parts.length === 3) {\\n+\\t\\t\\tconst day = parseInt(parts[0], 10);\\n+\\t\\t\\tconst\n month = parseInt(parts[1], 10) - 1;\\n+\\t\\t\\tconst year = parseInt(parts[2], 10);\\n+\\t\\t\\tif\n (!isNaN(day) && !isNaN(month) && !isNaN(year)) {\\n+\\t\\t\\t\\treturn new Date(year,\n month, day);\\n+\\t\\t\\t}\\n+\\t\\t}\\n+\\t\\treturn null;\\n+\\t}\\n+}\\n+\\n+const MY_DATE_FORMATS\n = {\\n+\\tparse: {\\n+\\t\\tdateInput: 'input',\\n+\\t},\\n+\\tdisplay: {\\n+\\t\\tdateInput:\n 'input',\\n+\\t\\tmonthYearLabel: { year: 'numeric', month: 'short' },\\n+\\t\\tdateA11yLabel:\n { year: 'numeric', month: 'long', day: 'numeric' },\\n+\\t\\tmonthYearA11yLabel: {\n year: 'numeric', month: 'long' },\\n+\\t},\\n+};\\n+\\n+@Component({\\n+\\tselector: 'app-day-input',\\n+\\timports:\n [\\n+\\t\\tMatIconModule,\\n+\\t\\tMatCard,\\n+\\t\\tMatSelect,\\n+\\t\\tMatOption,\\n+\\t\\tMatInput,\\n+\\t\\tMatDatepicker,\\n+\\t\\tMatLabel,\\n+\\t\\tMatDatepickerToggle,\\n+\\t\\tMatDatepickerInput,\\n+\\t\\tMatInputModule,\\n+\\t\\tMatButtonModule,\\n+\\t],\\n+\\ttemplateUrl:\n './day-input.html',\\n+\\tstyleUrl: './day-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n+\\tproviders:\n [\\n+\\t\\t{ provide: DateAdapter, useClass: CustomDateAdapter },\\n+\\t\\t{ provide:\n MAT_DATE_LOCALE, useValue: 'en-GB' },\\n+\\t\\t{ provide: MAT_DATE_FORMATS, useValue:\n MY_DATE_FORMATS },\\n+\\t],\\n+})\\n+export class DayInput {\\n+\\tpublic day = input.required();\\n+\\tpublic\n dayChange = output();\\n+\\tpublic deleteEntry = output();\\n+\\n+\\tprotected\n readonly selectableDayTypes = Object.values(SelectableDayType);\\n+\\tprotected readonly\n tooltipTypeMapping = tooltipTypeMapping;\\n+\\n+\\tonInput(event: Event) {\\n+\\t\\tconst\n value = Number((event.target as HTMLInputElement).value);\\n+\\t\\tthis.day().numberOfDays\n = value;\\n+\\t\\tthis.dayChange.emit(this.day());\"\n:new_path: src/app/components/userInput/day-input/day-input.ts\n:old_path: src/app/components/userInput/day-input/day-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:15", "discussion_id": "dff0aad8188fcd88a1d0c2ef4076c3d1299ea43d", "id": 287, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_100", "namespace_id": null, "note": "Direct mutation of an input signal's value is not allowed and violates Angular's signal immutability principle. Instead, create a new object with the updated property. Replace lines 98-99 with: `this.dayChange.emit({ ...this.day(), expiryDate: event.value! });`\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n\t}\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,101 @@\\n+import { Component, input, output, ChangeDetectionStrategy\n } from '@angular/core';\\n+import { SelectableDayType, tooltipTypeMapping, VacDay\n } from '../../../services/calendar-service';\\n+import { MatCard } from '@angular/material/card';\\n+import\n { MatOption, MatSelect } from '@angular/material/select';\\n+import { MatInput, MatInputModule,\n MatLabel } from '@angular/material/input';\\n+import {\\n+\\tMatDatepicker,\\n+\\tMatDatepickerInput,\\n+\\tMatDatepickerInputEvent,\\n+\\tMatDatepickerToggle,\\n+}\n from '@angular/material/datepicker';\\n+import {\\n+\\tDateAdapter,\\n+\\tMAT_DATE_FORMATS,\\n+\\tMAT_DATE_LOCALE,\\n+\\tNativeDateAdapter,\\n+}\n from '@angular/material/core';\\n+import { MatIconModule } from '@angular/material/icon';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+\\n+class CustomDateAdapter\n extends NativeDateAdapter {\\n+\\toverride format(date: Date, displayFormat: unknown):\n string {\\n+\\t\\tif (displayFormat === 'input') {\\n+\\t\\t\\tconst day = date.getDate().toString().padStart(2,\n '0');\\n+\\t\\t\\tconst month = (date.getMonth() + 1).toString().padStart(2, '0');\\n+\\t\\t\\tconst\n year = date.getFullYear();\\n+\\t\\t\\treturn `${day}/${month}/${year}`;\\n+\\t\\t}\\n+\\t\\treturn\n super.format(date, displayFormat as object);\\n+\\t}\\n+\\n+\\toverride parse(value:\n string): Date | null {\\n+\\t\\tif (!value) return null;\\n+\\t\\tconst parts = value.split('/');\\n+\\t\\tif\n (parts.length === 3) {\\n+\\t\\t\\tconst day = parseInt(parts[0], 10);\\n+\\t\\t\\tconst\n month = parseInt(parts[1], 10) - 1;\\n+\\t\\t\\tconst year = parseInt(parts[2], 10);\\n+\\t\\t\\tif\n (!isNaN(day) && !isNaN(month) && !isNaN(year)) {\\n+\\t\\t\\t\\treturn new Date(year,\n month, day);\\n+\\t\\t\\t}\\n+\\t\\t}\\n+\\t\\treturn null;\\n+\\t}\\n+}\\n+\\n+const MY_DATE_FORMATS\n = {\\n+\\tparse: {\\n+\\t\\tdateInput: 'input',\\n+\\t},\\n+\\tdisplay: {\\n+\\t\\tdateInput:\n 'input',\\n+\\t\\tmonthYearLabel: { year: 'numeric', month: 'short' },\\n+\\t\\tdateA11yLabel:\n { year: 'numeric', month: 'long', day: 'numeric' },\\n+\\t\\tmonthYearA11yLabel: {\n year: 'numeric', month: 'long' },\\n+\\t},\\n+};\\n+\\n+@Component({\\n+\\tselector: 'app-day-input',\\n+\\timports:\n [\\n+\\t\\tMatIconModule,\\n+\\t\\tMatCard,\\n+\\t\\tMatSelect,\\n+\\t\\tMatOption,\\n+\\t\\tMatInput,\\n+\\t\\tMatDatepicker,\\n+\\t\\tMatLabel,\\n+\\t\\tMatDatepickerToggle,\\n+\\t\\tMatDatepickerInput,\\n+\\t\\tMatInputModule,\\n+\\t\\tMatButtonModule,\\n+\\t],\\n+\\ttemplateUrl:\n './day-input.html',\\n+\\tstyleUrl: './day-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n+\\tproviders:\n [\\n+\\t\\t{ provide: DateAdapter, useClass: CustomDateAdapter },\\n+\\t\\t{ provide:\n MAT_DATE_LOCALE, useValue: 'en-GB' },\\n+\\t\\t{ provide: MAT_DATE_FORMATS, useValue:\n MY_DATE_FORMATS },\\n+\\t],\\n+})\\n+export class DayInput {\\n+\\tpublic day = input.required();\\n+\\tpublic\n dayChange = output();\\n+\\tpublic deleteEntry = output();\\n+\\n+\\tprotected\n readonly selectableDayTypes = Object.values(SelectableDayType);\\n+\\tprotected readonly\n tooltipTypeMapping = tooltipTypeMapping;\\n+\\n+\\tonInput(event: Event) {\\n+\\t\\tconst\n value = Number((event.target as HTMLInputElement).value);\\n+\\t\\tthis.day().numberOfDays\n = value;\\n+\\t\\tthis.dayChange.emit(this.day());\\n+\\t}\\n+\\n+\\taddEvent(event: MatDatepickerInputEvent)\n {\\n+\\t\\tthis.day().expiryDate = event.value!;\\n+\\t\\tthis.dayChange.emit(this.day());\\n+\\t}\"\n:new_path: src/app/components/userInput/day-input/day-input.ts\n:old_path: src/app/components/userInput/day-input/day-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:15", "discussion_id": "0c69003a6c9493b69e39ba99d31da76fe30300c4", "id": 288, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_67_55", "namespace_id": null, "note": "Directly mutating the array returned from a signal violates signal immutability. Use the signal's `update` method instead: `this.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);`\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,68 +1,61 @@\\n-import { Component, OnInit, OnDestroy, effect } from '@angular/core';\\n-import\n { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n+import\n { MatCard, MatCardContent } from '@angular/material/card';\\n+import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n-\\t\\tCalendarSettings,\\n+\\t\\tMatCard,\\n+\\t\\tMatCardContent,\\n+\\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection:\n ChangeDetectionStrategy.OnPush,\\n })\\n export class UserInput implements OnInit,\n OnDestroy {\\n-\\tconstructor(private userInputService: UserInputService) {\\n-\\t\\t//\n Watch for signal changes and update form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData\n = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.length;\\n \\t});\\n \\n-\\tngOnInit() {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500),\n takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(() => this.submitForm());\\n-\\t}\\n+\\tprotected\n hasRemainingDays = computed(() => this.remainingDays() > 0);\\n+\\n+\\tngOnInit() {}\\n\n \\n \\tngOnDestroy() {\\n \\t\\tthis.destroy$.next();\\n \\t\\tthis.destroy$.complete();\\n\n \\t}\\n \\n-\\tprivate submitForm() {\\n-\\t\\tif (this.userInputForm.valid) {\\n-\\t\\t\\tconst\n vacationData: VacationsNumber = {\\n-\\t\\t\\t\\tcp: this.userInputForm.get('CP')?.value\n || 0,\\n-\\t\\t\\t\\trtt: this.userInputForm.get('RTT')?.value || 0,\\n-\\t\\t\\t\\tother:\n this.userInputForm.get('Others')?.value || 0,\\n-\\t\\t\\t};\\n-\\t\\t\\tthis.userInputService.vacationNumberSignal.set(vacationData);\\n-\\t\\t}\\n+\\tpublic\n addDay() {\\n+\\t\\tthis.daysOff.push(this.userInputService.createVacationDay());\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:16", "discussion_id": "f0b103b136fdcf50706e94dab767a1aa16658698", "id": 283, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_540_547", "namespace_id": null, "note": "Using `any` type defeats TypeScript's type safety. The parameter type should match the actual data structure being used. Based on the code, this should be `VacationsNumber` (with properties cp, rtt, other), not the new `VacDay[]` type.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -535,36 +543,76 @@ export class SelectedDates implements SelectedDateInterface\n {\\n \\t\\treturn totalScore;\\n \\t}\\n \\n-\\toptimizeVacations(\\n-\\t\\tvacationsNumber:\n VacationsNumber,\\n+\\tasync optimizeVacations(\\n+\\t\\tvacationsNumber: any,\"\n:new_path: src/app/services/calendar-service.ts\n:old_path: src/app/services/calendar-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:16", "discussion_id": "04b1c5e0799a7d2164512b3360e2a4df08b68136", "id": 284, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_568_615", "namespace_id": null, "note": "Using `any` type defeats TypeScript's type safety. The parameter type should be `VacationsNumber` (with properties cp, rtt, other) to match how it's used throughout the method.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -535,36 +543,76 @@ export class SelectedDates implements SelectedDateInterface\n {\\n \\t\\treturn totalScore;\\n \\t}\\n \\n-\\toptimizeVacations(\\n-\\t\\tvacationsNumber:\n VacationsNumber,\\n+\\tasync optimizeVacations(\\n+\\t\\tvacationsNumber: any,\\n \\t\\tcalendarSettingsService:\n CalendarSettingsService,\\n \\t\\tcalendarService: CalendarService,\\n-\\t): void {\\n+\\t\\tprogressCallback?:\n (progress: number) => void,\\n+\\t): Promise {\\n \\t\\tif (calendarSettingsService.get('samediMalin')())\n this.samediMalin(vacationsNumber);\\n+\\n+\\t\\tconst initialVacations = { ...vacationsNumber\n };\\n+\\t\\tconst totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;\\n+\\t\\tlet\n processedDays = 0;\\n+\\n+\\t\\t// Helper function to update progress\\n+\\t\\tconst updateProgress\n = async () => {\\n+\\t\\t\\tif (progressCallback && totalDays > 0) {\\n+\\t\\t\\t\\tconst\n progress = Math.round((processedDays / totalDays) * 100);\\n+\\t\\t\\t\\tprogressCallback(progress);\\n+\\t\\t\\t\\t//\n Allow UI to update with a microtask\\n+\\t\\t\\t\\tawait new Promise((resolve) => setTimeout(resolve,\n 0));\\n+\\t\\t\\t}\\n+\\t\\t};\\n+\\n+\\t\\tawait updateProgress();\\n+\\n \\t\\twhile (vacationsNumber.cp\n > 0) {\\n \\t\\t\\tconst result = this.lookForVacation(DayType.CP, calendarService);\\n\n \\t\\t\\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\\n \\t\\t\\t\\tbreak;\\n\n \\t\\t\\t}\\n \\t\\t\\tvacationsNumber.cp -= 1;\\n+\\t\\t\\tprocessedDays++;\\n+\\t\\t\\tawait\n updateProgress();\\n \\t\\t}\\n \\t\\twhile (vacationsNumber.rtt > 0) {\\n \\t\\t\\tconst\n result = this.lookForVacation(DayType.RTT, calendarService);\\n \\t\\t\\tif (result.heuristic\n === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\\n \\t\\t\\t\\tbreak;\\n \\t\\t\\t}\\n \\t\\t\\tvacationsNumber.rtt\n -= 1;\\n+\\t\\t\\tprocessedDays++;\\n+\\t\\t\\tawait updateProgress();\\n \\t\\t}\\n \\t\\twhile\n (vacationsNumber.other > 0) {\\n \\t\\t\\tconst result = this.lookForVacation(DayType.OTHER,\n calendarService);\\n \\t\\t\\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC)\n {\\n \\t\\t\\t\\tbreak;\\n \\t\\t\\t}\\n \\t\\t\\tvacationsNumber.other -= 1;\\n+\\t\\t\\tprocessedDays++;\\n+\\t\\t\\tawait\n updateProgress();\\n+\\t\\t}\\n+\\n+\\t\\t// Final progress update (100%)\\n+\\t\\tif (progressCallback)\n {\\n+\\t\\t\\tprogressCallback(100);\\n+\\t\\t}\\n+\\n+\\t\\t// Inform user if not all days\n were placed\\n+\\t\\tconst remainingDays = vacationsNumber.cp + vacationsNumber.rtt\n + vacationsNumber.other;\\n+\\t\\tif (remainingDays > 0) {\\n+\\t\\t\\tconsole.warn(\\n+\\t\\t\\t\\t`Optimization\n stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +\\n+\\t\\t\\t\\t\\t`Too\n many days have equivalent placement quality - please place remaining days manually.`,\\n+\\t\\t\\t);\\n\n \\t\\t}\\n+\\n+\\t\\treturn vacationsNumber;\\n \\t}\\n \\n-\\tsamediMalin(vacationsNumber:\n VacationsNumber): void {\\n+\\tsamediMalin(vacationsNumber: any): void {\"\n:new_path: src/app/services/calendar-service.ts\n:old_path: src/app/services/calendar-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:16", "discussion_id": "7e3b58c13aafbd18f5c54ed61a40729693d42eb5", "id": 290, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_67_59", "namespace_id": null, "note": "Directly mutating the array returned from a signal violates signal immutability. Use the signal's `update` method instead: `this.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));`\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,68 +1,61 @@\\n-import { Component, OnInit, OnDestroy, effect } from '@angular/core';\\n-import\n { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n+import\n { MatCard, MatCardContent } from '@angular/material/card';\\n+import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n-\\t\\tCalendarSettings,\\n+\\t\\tMatCard,\\n+\\t\\tMatCardContent,\\n+\\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection:\n ChangeDetectionStrategy.OnPush,\\n })\\n export class UserInput implements OnInit,\n OnDestroy {\\n-\\tconstructor(private userInputService: UserInputService) {\\n-\\t\\t//\n Watch for signal changes and update form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData\n = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.length;\\n \\t});\\n \\n-\\tngOnInit() {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500),\n takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(() => this.submitForm());\\n-\\t}\\n+\\tprotected\n hasRemainingDays = computed(() => this.remainingDays() > 0);\\n+\\n+\\tngOnInit() {}\\n\n \\n \\tngOnDestroy() {\\n \\t\\tthis.destroy$.next();\\n \\t\\tthis.destroy$.complete();\\n\n \\t}\\n \\n-\\tprivate submitForm() {\\n-\\t\\tif (this.userInputForm.valid) {\\n-\\t\\t\\tconst\n vacationData: VacationsNumber = {\\n-\\t\\t\\t\\tcp: this.userInputForm.get('CP')?.value\n || 0,\\n-\\t\\t\\t\\trtt: this.userInputForm.get('RTT')?.value || 0,\\n-\\t\\t\\t\\tother:\n this.userInputForm.get('Others')?.value || 0,\\n-\\t\\t\\t};\\n-\\t\\t\\tthis.userInputService.vacationNumberSignal.set(vacationData);\\n-\\t\\t}\\n+\\tpublic\n addDay() {\\n+\\t\\tthis.daysOff.push(this.userInputService.createVacationDay());\\n+\\t}\\n+\\n+\\tpublic\n removeFromList(index: number) {\\n+\\t\\tthis.daysOff.splice(index, 1);\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:17", "discussion_id": "5c39376514e132d47e3031b1154e1f4bb3d55590", "id": 292, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "039c4c160f861e3a0e271f37b5fff76a6ad3adcf_51_57", "namespace_id": null, "note": "Type mismatch: `vacationData` is `VacDay[]` from the signal, but `optimizeVacations` expects an object with `{cp: number, rtt: number, other: number}` properties. The spread operator `{ ...vacationData }` will create an object with numeric keys, not the expected structure. You need to transform the `VacDay[]` into the expected format before calling `optimizeVacations`, or update `optimizeVacations` to accept the new type.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -39,15 +40,21 @@ export class CalendarYear {\\n \\t\\t\\tconsole.log('Samedi\n Malin setting:', c);\\n \\t\\t\\tconsole.log('Optimizing vacations with data:', vacationData);\\n\n \\t\\t\\tthis.isLoading.set(true);\\n+\\t\\t\\tthis.loadingProgress.set(0);\\n \\t\\t\\t//\n Simulate server-side calculation with a small delay\\n-\\t\\t\\tsetTimeout(() => {\\n+\\t\\t\\tsetTimeout(async\n () => {\\n \\t\\t\\t\\tif (this.selectedDates) {\\n \\t\\t\\t\\t\\t// Create a copy to avoid\n mutating the original signal data\\n-\\t\\t\\t\\t\\tthis.selectedDates.optimizeVacations(\\n+\\t\\t\\t\\t\\tconst\n remainingDays = await this.selectedDates.optimizeVacations(\\n \\t\\t\\t\\t\\t\\t{ ...vacationData\n },\\n \\t\\t\\t\\t\\t\\tthis.calendarSettingsService,\\n \\t\\t\\t\\t\\t\\tthis.calendarService,\\n+\\t\\t\\t\\t\\t\\t(progress:\n number) => {\\n+\\t\\t\\t\\t\\t\\t\\tthis.loadingProgress.set(progress);\\n+\\t\\t\\t\\t\\t\\t},\\n\n \\t\\t\\t\\t\\t);\\n+\\t\\t\\t\\t\\t// Update the remaining vacation days signal\\n+\\t\\t\\t\\t\\tthis.userInput.remainingVacationDaysSignal.set(remainingDays);\"\n:new_path: src/app/components/calendar-year/calendar-year.ts\n:old_path: src/app/components/calendar-year/calendar-year.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:17", "discussion_id": "a364b66c53b342a6d7f478f3b50c24f16539aea1", "id": 293, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "d69b448b6d6842612fa7ccc03914410d4bddeeab_0_16", "namespace_id": null, "note": "This test will fail because the component has a required input `day` that is not provided. You need to either provide the required input using `fixture.componentRef.setInput('day', mockVacDay)` before `detectChanges()`, or add `provideAnimationsAsync()` to the providers array since the component uses Material components that require animations.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,22 @@\\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\\n+\\n+import\n { DayInput } from './day-input';\\n+\\n+describe('DayInput', () => {\\n+\\tlet component:\n DayInput;\\n+\\tlet fixture: ComponentFixture;\\n+\\n+\\tbeforeEach(async ()\n => {\\n+\\t\\tawait TestBed.configureTestingModule({\\n+\\t\\t\\timports: [DayInput],\\n+\\t\\t}).compileComponents();\\n+\\n+\\t\\tfixture\n = TestBed.createComponent(DayInput);\\n+\\t\\tcomponent = fixture.componentInstance;\\n+\\t\\tfixture.detectChanges();\"\n:new_path: src/app/components/userInput/day-input/day-input.spec.ts\n:old_path: src/app/components/userInput/day-input/day-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:17", "discussion_id": "93543d316cffffe9f66f4b8687093cdb3da0efcb", "id": 294, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_52_47", "namespace_id": null, "note": "Empty lifecycle hook. Since `ngOnInit` has no implementation, it should be removed along with the `OnInit` interface from the class declaration (line 35).", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,68 +1,61 @@\\n-import { Component, OnInit, OnDestroy, effect } from '@angular/core';\\n-import\n { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n+import\n { MatCard, MatCardContent } from '@angular/material/card';\\n+import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n-\\t\\tCalendarSettings,\\n+\\t\\tMatCard,\\n+\\t\\tMatCardContent,\\n+\\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection:\n ChangeDetectionStrategy.OnPush,\\n })\\n export class UserInput implements OnInit,\n OnDestroy {\\n-\\tconstructor(private userInputService: UserInputService) {\\n-\\t\\t//\n Watch for signal changes and update form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData\n = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.length;\\n \\t});\\n \\n-\\tngOnInit() {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500),\n takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(() => this.submitForm());\\n-\\t}\\n+\\tprotected\n hasRemainingDays = computed(() => this.remainingDays() > 0);\\n+\\n+\\tngOnInit() {}\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:17", "discussion_id": "04a0ad2d79301a9c55533101d6980a74364a6402", "id": 295, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_5_10", "namespace_id": null, "note": "Unused import. `ReactiveFormsModule` is no longer used after removing the form-based implementation. This import should be removed from both the import statement and the component's imports array (line 23).", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,68 +1,61 @@\\n-import { Component, OnInit, OnDestroy, effect } from '@angular/core';\\n-import\n { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "e10f2ff4e5c3aa7b4c6484d95885732d654a7559", "confidential": null, "created_at": "2025-11-29T18:15:17", "discussion_id": "2ba11518378e5ea27e281332f48ac420d8c691fa", "id": 296, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_8_12", "namespace_id": null, "note": "Unused import and variable. The `destroy$` Subject (line 38) is created and completed in `ngOnDestroy`, but it's never actually used (no `.pipe(takeUntil(this.destroy$))` calls remain after form removal). Both the import and the `destroy$` property can be removed, along with the `OnDestroy` interface and `ngOnDestroy` method.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,68 +1,61 @@\\n-import { Component, OnInit, OnDestroy, effect } from '@angular/core';\\n-import\n { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T18:15:19", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T18:15:18", "discussion_id": "3bcebd4ea06434845dcb16fc2e72e4ae9d5046df", "id": 488, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "**Review:** Commented\n\n## Pull request overview\n\nThis PR refactors the vacation management system from a simple count-based approach to a more structured day-tracking system with expiry dates, and upgrades Angular from v20 to v21. Key changes include:\n- Migration from `VacationsNumber` (simple object with cp/rtt/other counts) to `VacDay[]` (array of vacation day objects with IDs, types, counts, and expiry dates)\n- Addition of async progress tracking for vacation optimization\n- New `DayInput` component for managing individual vacation day entries\n- Angular version upgrade to v21.0.1 with animations support\n\n### Reviewed changes\n\nCopilot reviewed 16 out of 17 changed files in this pull request and generated 12 comments.\n\n
\nShow a summary per file\n\n| File | Description |\r\n| ---- | ----------- |\r\n| src/main.server.ts | Adds zone change detection configuration for server-side rendering |\r\n| src/app/services/user-input-service.ts | Refactors service to use VacDay[] structure instead of VacationsNumber |\r\n| src/app/services/calendar-service.ts | Updates types, adds async optimization with progress tracking, and implements heuristic limit |\r\n| src/app/components/userInput/day-input/* | New component for managing individual vacation day inputs |\r\n| src/app/components/user-input/* | Refactored to use signal-based state with new VacDay structure |\r\n| src/app/components/calendar-year/* | Updated to handle async optimization and remaining days tracking |\r\n| package.json | Upgrades Angular from v20 to v21 and adds animations package |\n
\n\n\n\n
\nComments suppressed due to low confidence (1)\n\n**src/app/components/user-input/user-input.spec.ts:40**\n* These tests reference `component.userInputForm` which no longer exists after the refactoring. The component now uses `daysOff` signal instead of a form. These tests should be updated to test the new implementation: verify `daysOff` is defined, test `addDay()` and `removeFromList()` methods.\n```\n\tit('should initialize form with values from service', () => {\n\t\texpect(component.userInputForm).toBeDefined();\n\t\texpect(component.userInputForm.get('CP')).toBeDefined();\n\t\texpect(component.userInputForm.get('RTT')).toBeDefined();\n\t\texpect(component.userInputForm.get('Others')).toBeDefined();\n\t});\n\n\tit('should have valid form controls', () => {\n\t\tconst cpControl = component.userInputForm.get('CP');\n\t\tconst rttControl = component.userInputForm.get('RTT');\n\t\tconst othersControl = component.userInputForm.get('Others');\n\n\t\texpect(cpControl).toBeTruthy();\n\t\texpect(rttControl).toBeTruthy();\n\t\texpect(othersControl).toBeTruthy();\n\t});\n```\n
\n\n", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-29T18:15:18", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T20:09:52", "discussion_id": "43a86e9824ea9eb4fc0c4dce15eb48107e59cbac", "id": 487, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "requested review from `@Copilot`", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2025-11-29T20:09:52", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T20:15:39", "discussion_id": "5083bd8d88fc93d304e91aff980758f7c301d9c3", "id": 489, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@copilot` open a new pull request to apply changes based on the comments in [this thread](https://gitlab.nessar.fr/Nessar/vac-optimizer/-/merge_requests/61#pullrequestreview-3520781340)", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-29T20:15:39", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T20:15:46", "discussion_id": "7656397e772292d02fec6bd4f16d7cbe46a516eb", "id": 493, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "mentioned in merge request !62", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2026-05-03T16:02:20.581177", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T20:15:47", "discussion_id": "8b8c9e6a729ae8f6e2820693acf31b37666daac8", "id": 490, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@THoguet` I've opened a new pull request, #62, to work on those changes. Once the pull request is ready, I'll request review from you.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-29T20:15:47", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:33", "discussion_id": "2e713bc8f05358557662c9d60e9988b52528f691", "id": 302, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_24", "namespace_id": null, "note": "Input type should be \"text\" or \"date\", not \"datetime\". The \"datetime\" type is not a valid HTML input type. For date-only pickers (which this appears to be), use:\n```html\n\n```\n```suggestion:-0+0\n\t\t\ttype=\"text\"\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 24\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 24\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:34", "discussion_id": "4063b429da7483714f9dc1018dc9846a4a2d99ab", "id": 299, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "3d779d5cb5b415447eb2a87265084f8874aabd0b_17_6", "namespace_id": null, "note": "Missing `dayChange` output binding from `app-day-input`. The `DayInput` component emits changes through the `dayChange` output, but this parent component doesn't listen to it. Without handling this event, changes made in the child component won't be properly synchronized. Add the binding:\n```html\n\n\n```\n\nHowever, note that with the current direct mutation pattern in the component, you may not need this if you address the signal mutation issues identified in other comments.\n```suggestion:-0+0\n\t\t\t\t\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,29 +1,40 @@\\n
\\n \\t

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

\\n-\\t
\\n-\\t\\t\\n-\\t\\t\\tCombien de CP par an ?\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\t\\n-\\t\\t\\tCombien de jours de RTT par an ?\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\t\\n-\\t\\t\\tAutres jours\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\n+\\t\\n+\\t\\t@for (day of daysOff; track day.id) {\\n+\\t\\t\\t
\\n+\\t\\t\\t\\t\"\n:new_path: src/app/components/user-input/user-input.html\n:old_path: src/app/components/user-input/user-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:34", "discussion_id": "7418d7f17ef93b7e54070a85a7b3848ce11f927a", "id": 300, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_99", "namespace_id": null, "note": "Direct mutation of input signal object. Input signals return readonly values and should not be mutated directly. Instead, emit a new object with the updated value through `dayChange` output.\n\nConsider this approach:\n```typescript\nonInput(event: Event) {\n const value = Number((event.target as HTMLInputElement).value);\n this.dayChange.emit({ ...this.day(), numberOfDays: value });\n}\n```\n```suggestion:-0+0\n\t\tthis.dayChange.emit({ ...this.day(), numberOfDays: value });\n\t}\n\n\taddEvent(event: MatDatepickerInputEvent) {\n\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value! });\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,101 @@\\n+import { Component, input, output, ChangeDetectionStrategy\n } from '@angular/core';\\n+import { SelectableDayType, tooltipTypeMapping, VacDay\n } from '../../../services/calendar-service';\\n+import { MatCard } from '@angular/material/card';\\n+import\n { MatOption, MatSelect } from '@angular/material/select';\\n+import { MatInput, MatInputModule,\n MatLabel } from '@angular/material/input';\\n+import {\\n+\\tMatDatepicker,\\n+\\tMatDatepickerInput,\\n+\\tMatDatepickerInputEvent,\\n+\\tMatDatepickerToggle,\\n+}\n from '@angular/material/datepicker';\\n+import {\\n+\\tDateAdapter,\\n+\\tMAT_DATE_FORMATS,\\n+\\tMAT_DATE_LOCALE,\\n+\\tNativeDateAdapter,\\n+}\n from '@angular/material/core';\\n+import { MatIconModule } from '@angular/material/icon';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+\\n+class CustomDateAdapter\n extends NativeDateAdapter {\\n+\\toverride format(date: Date, displayFormat: unknown):\n string {\\n+\\t\\tif (displayFormat === 'input') {\\n+\\t\\t\\tconst day = date.getDate().toString().padStart(2,\n '0');\\n+\\t\\t\\tconst month = (date.getMonth() + 1).toString().padStart(2, '0');\\n+\\t\\t\\tconst\n year = date.getFullYear();\\n+\\t\\t\\treturn `${day}/${month}/${year}`;\\n+\\t\\t}\\n+\\t\\treturn\n super.format(date, displayFormat as object);\\n+\\t}\\n+\\n+\\toverride parse(value:\n string): Date | null {\\n+\\t\\tif (!value) return null;\\n+\\t\\tconst parts = value.split('/');\\n+\\t\\tif\n (parts.length === 3) {\\n+\\t\\t\\tconst day = parseInt(parts[0], 10);\\n+\\t\\t\\tconst\n month = parseInt(parts[1], 10) - 1;\\n+\\t\\t\\tconst year = parseInt(parts[2], 10);\\n+\\t\\t\\tif\n (!isNaN(day) && !isNaN(month) && !isNaN(year)) {\\n+\\t\\t\\t\\treturn new Date(year,\n month, day);\\n+\\t\\t\\t}\\n+\\t\\t}\\n+\\t\\treturn null;\\n+\\t}\\n+}\\n+\\n+const MY_DATE_FORMATS\n = {\\n+\\tparse: {\\n+\\t\\tdateInput: 'input',\\n+\\t},\\n+\\tdisplay: {\\n+\\t\\tdateInput:\n 'input',\\n+\\t\\tmonthYearLabel: { year: 'numeric', month: 'short' },\\n+\\t\\tdateA11yLabel:\n { year: 'numeric', month: 'long', day: 'numeric' },\\n+\\t\\tmonthYearA11yLabel: {\n year: 'numeric', month: 'long' },\\n+\\t},\\n+};\\n+\\n+@Component({\\n+\\tselector: 'app-day-input',\\n+\\timports:\n [\\n+\\t\\tMatIconModule,\\n+\\t\\tMatCard,\\n+\\t\\tMatSelect,\\n+\\t\\tMatOption,\\n+\\t\\tMatInput,\\n+\\t\\tMatDatepicker,\\n+\\t\\tMatLabel,\\n+\\t\\tMatDatepickerToggle,\\n+\\t\\tMatDatepickerInput,\\n+\\t\\tMatInputModule,\\n+\\t\\tMatButtonModule,\\n+\\t],\\n+\\ttemplateUrl:\n './day-input.html',\\n+\\tstyleUrl: './day-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n+\\tproviders:\n [\\n+\\t\\t{ provide: DateAdapter, useClass: CustomDateAdapter },\\n+\\t\\t{ provide:\n MAT_DATE_LOCALE, useValue: 'en-GB' },\\n+\\t\\t{ provide: MAT_DATE_FORMATS, useValue:\n MY_DATE_FORMATS },\\n+\\t],\\n+})\\n+export class DayInput {\\n+\\tpublic day = input.required();\\n+\\tpublic\n dayChange = output();\\n+\\tpublic deleteEntry = output();\\n+\\n+\\tprotected\n readonly selectableDayTypes = Object.values(SelectableDayType);\\n+\\tprotected readonly\n tooltipTypeMapping = tooltipTypeMapping;\\n+\\n+\\tonInput(event: Event) {\\n+\\t\\tconst\n value = Number((event.target as HTMLInputElement).value);\\n+\\t\\tthis.day().numberOfDays\n = value;\\n+\\t\\tthis.dayChange.emit(this.day());\\n+\\t}\\n+\\n+\\taddEvent(event: MatDatepickerInputEvent)\n {\\n+\\t\\tthis.day().expiryDate = event.value!;\\n+\\t\\tthis.dayChange.emit(this.day());\"\n:new_path: src/app/components/userInput/day-input/day-input.ts\n:old_path: src/app/components/userInput/day-input/day-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:34", "discussion_id": "ada2ad565ab2bac3476f115a72724036782e4531", "id": 301, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_36", "namespace_id": null, "note": "[nitpick] Commented-out code should be removed. If the `mat-list-item` wrapper was removed intentionally, the closing comment should be deleted as well.\n```suggestion:-0+0\n\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 36\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 36\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:34", "discussion_id": "2f12fa6725549528aa23827635b025862a1b7c3c", "id": 304, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_14", "namespace_id": null, "note": "Empty `mat-option` without value serves no clear purpose. If this is meant to be a \"clear selection\" option, it should have a descriptive label. If not needed, remove it:\n```html\n\n @for (dayType of selectableDayTypes; track $index) {\n {{ tooltipTypeMapping[dayType] }}\n }\n\n```\n```suggestion:-0+0\n\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 14\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.html\n :new_path: src/app/components/userInput/day-input/day-input.html\n :position_type: text\n :old_line:\n :new_line: 14\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:35", "discussion_id": "164c3928a28335974b84b8538e96890fc63cac07", "id": 297, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_78_72", "namespace_id": null, "note": "Direct mutation of signal array. The code mutates `this.daysOff` which is the result of calling a signal (`this.userInputService.vacationNumberSignal()`), but this doesn't trigger signal updates. The mutation affects the underlying array but Angular's change detection won't be notified.\n\nThis issue is partially acknowledged in `triggerComputation()` (line 80), but the pattern of mutating and then manually triggering updates is error-prone. Consider using `update()` instead:\n```typescript\npublic addDay() {\n this.userInputService.vacationNumberSignal.update(days => [...days, this.userInputService.createVacationDay()]);\n}\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,82 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n import\n { MatCard, MatCardContent } from '@angular/material/card';\\n import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+import { tooltipTypeMapping,\n DayType } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n \\t\\tMatCard,\\n \\t\\tMatCardContent,\\n\n \\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n\n })\\n export class UserInput implements OnInit, OnDestroy {\\n-\\tconstructor(private\n userInputService: UserInputService) {\\n-\\t\\t// Watch for signal changes and update\n form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.reduce((acc, day) => acc + day.numberOfDays, 0);\\n \\t});\\n \\n-\\tprotected\n remainingDays = computed(() => {\\n+\\tprotected remainingDaysDetails = computed(()\n => {\\n \\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n-\\t\\treturn\n remaining.cp + remaining.rtt + remaining.other;\\n+\\t\\treturn remaining\\n+\\t\\t\\t.filter((day)\n => day.numberOfDays > 0)\\n+\\t\\t\\t.map((day) => {\\n+\\t\\t\\t\\tconst typeName = tooltipTypeMapping[day.type\n as unknown as DayType];\\n+\\t\\t\\t\\tconst expiryDate = day.expiryDate.toLocaleDateString('fr-FR',\n {\\n+\\t\\t\\t\\t\\tday: 'numeric',\\n+\\t\\t\\t\\t\\tmonth: 'short',\\n+\\t\\t\\t\\t\\tyear: 'numeric',\\n+\\t\\t\\t\\t});\\n+\\t\\t\\t\\treturn\n `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;\\n+\\t\\t\\t});\\n \\t});\\n \\n\n \\tprotected hasRemainingDays = computed(() => this.remainingDays() > 0);\\n \\n-\\tngOnInit()\n {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500), takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(()\n => this.submitForm());\\n-\\t}\\n+\\tngOnInit() {}\\n \\n \\tngOnDestroy() {\\n \\t\\tthis.destroy$.next();\\n\n \\t\\tthis.destroy$.complete();\\n \\t}\\n \\n-\\tprivate submitForm() {\\n-\\t\\tif (this.userInputForm.valid)\n {\\n-\\t\\t\\tconst vacationData: VacationsNumber = {\\n-\\t\\t\\t\\tcp: this.userInputForm.get('CP')?.value\n || 0,\\n-\\t\\t\\t\\trtt: this.userInputForm.get('RTT')?.value || 0,\\n-\\t\\t\\t\\tother:\n this.userInputForm.get('Others')?.value || 0,\\n-\\t\\t\\t};\\n-\\t\\t\\tthis.userInputService.vacationNumberSignal.set(vacationData);\\n-\\t\\t}\\n+\\tpublic\n addDay() {\\n+\\t\\tthis.daysOff.push(this.userInputService.createVacationDay());\\n+\\t}\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:35", "discussion_id": "a60f375f2780b6240ec875c04233941e03ea0c72", "id": 298, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_63_63", "namespace_id": null, "note": "Empty `ngOnInit()` method serves no purpose and should be removed. If the `OnInit` lifecycle hook isn't needed, also remove `OnInit` from the implements clause and the import.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,82 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n import\n { MatCard, MatCardContent } from '@angular/material/card';\\n import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+import { tooltipTypeMapping,\n DayType } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n \\t\\tMatCard,\\n \\t\\tMatCardContent,\\n\n \\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n\n })\\n export class UserInput implements OnInit, OnDestroy {\\n-\\tconstructor(private\n userInputService: UserInputService) {\\n-\\t\\t// Watch for signal changes and update\n form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.reduce((acc, day) => acc + day.numberOfDays, 0);\\n \\t});\\n \\n-\\tprotected\n remainingDays = computed(() => {\\n+\\tprotected remainingDaysDetails = computed(()\n => {\\n \\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n-\\t\\treturn\n remaining.cp + remaining.rtt + remaining.other;\\n+\\t\\treturn remaining\\n+\\t\\t\\t.filter((day)\n => day.numberOfDays > 0)\\n+\\t\\t\\t.map((day) => {\\n+\\t\\t\\t\\tconst typeName = tooltipTypeMapping[day.type\n as unknown as DayType];\\n+\\t\\t\\t\\tconst expiryDate = day.expiryDate.toLocaleDateString('fr-FR',\n {\\n+\\t\\t\\t\\t\\tday: 'numeric',\\n+\\t\\t\\t\\t\\tmonth: 'short',\\n+\\t\\t\\t\\t\\tyear: 'numeric',\\n+\\t\\t\\t\\t});\\n+\\t\\t\\t\\treturn\n `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;\\n+\\t\\t\\t});\\n \\t});\\n \\n\n \\tprotected hasRemainingDays = computed(() => this.remainingDays() > 0);\\n \\n-\\tngOnInit()\n {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500), takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(()\n => this.submitForm());\\n-\\t}\\n+\\tngOnInit() {}\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:35", "discussion_id": "a11d8d687ee876976b270e025d127ea9d90c49cd", "id": 303, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_1", "namespace_id": null, "note": "[nitpick] Commented-out code should be removed. If the `mat-list-item` wrapper was removed intentionally, the comment should be deleted as well.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: |-\n @@ -0,0 +1,36 @@\n +\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:35", "discussion_id": "a88decb911911d2a482f1c621eaa0bea3fb3a099", "id": 308, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_54_39", "namespace_id": null, "note": "Type casting `day.type as unknown as DayType` indicates a design issue. Since `SelectableDayType` is a subset of `DayType` (with values CP, RTT, OTHER), the type system should handle this naturally. Consider redesigning the types to avoid unsafe casting, or if the relationship is valid, use a direct cast: `day.type as DayType`.\n```suggestion:-0+0\n\t\t\t\tconst typeName = tooltipTypeMapping[day.type as DayType];\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 39\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 39\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:36", "discussion_id": "c4ec72019396a5fa87181e1707194a8810b7796d", "id": 305, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_43_39", "namespace_id": null, "note": "The `destroy$` Subject is defined but never used since `ngOnInit()` no longer subscribes to observables. If not needed for cleanup, remove both `destroy$` and the `OnDestroy` implementation.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,82 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n import\n { MatCard, MatCardContent } from '@angular/material/card';\\n import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+import { tooltipTypeMapping,\n DayType } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n \\t\\tMatCard,\\n \\t\\tMatCardContent,\\n\n \\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n\n })\\n export class UserInput implements OnInit, OnDestroy {\\n-\\tconstructor(private\n userInputService: UserInputService) {\\n-\\t\\t// Watch for signal changes and update\n form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:36", "discussion_id": "6b815b32e7febd4a318fa04fccee2bf7f55ee5af", "id": 306, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_19", "namespace_id": null, "note": "Select dropdown for day type is missing an accessible label. Screen readers won't be able to describe this field to users. Add a ``:\n```html\n\n Type of leave\n \n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,36 @@\\n+\\n+\\n+\\t\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\t\\n+\\t\\t\\t\\n+\\t\\t\\t@for (dayType\n of selectableDayTypes; track $index) {\\n+\\t\\t\\t\\t{{\n tooltipTypeMapping[dayType] }}\\n+\\t\\t\\t}\\n+\\t\\t\\n+\\t\"\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:36", "discussion_id": "660d3baf903280b280fa39b6b6fd44327e58d7cb", "id": 307, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "81944682105cb64e67b05146dbe82be48d9092a5_0_21", "namespace_id": null, "note": "`::ng-deep` is deprecated and should be avoided. It pierces component encapsulation and can cause unexpected styling issues. Consider using Angular's host context or moving this style to a global stylesheet if it needs to affect Material components. Alternatively, use ViewEncapsulation.None if you need to style child components, though this is also not recommended.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,21 @@\\n+.card {\\n+\\tdisplay: flex;\\n+\\tflex-direction: row;\\n+\\talign-items:\n center;\\n+\\tjustify-content: space-between;\\n+\\tpadding: 16px;\\n+\\tgap: 16px;\\n+\\tflex-wrap:\n wrap;\\n+}\\n+\\n+.dateInput {\\n+\\tflex-grow: 1;\\n+}\\n+\\n+.numberInput {\\n+\\twidth:\n 80px;\\n+}\\n+\\n+::ng-deep .mat-mdc-form-field-subscript-wrapper {\\n+\\tdisplay: none;\\n+}\"\n:new_path: src/app/components/userInput/day-input/day-input.scss\n:old_path: src/app/components/userInput/day-input/day-input.scss\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:36", "discussion_id": "db7f9a124de73adf638ed2274c8c9d1614dfe02e", "id": 309, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_18_24", "namespace_id": null, "note": "Unused import in component's imports array. `ReactiveFormsModule` should be removed from the imports array since reactive forms are no longer used in this component.\n```suggestion:-0+0\n\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,82 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n import\n { MatCard, MatCardContent } from '@angular/material/card';\\n import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+import { tooltipTypeMapping,\n DayType } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:36", "discussion_id": "5253b331dbe788afc4f323ce0aa899d39385178b", "id": 310, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_99", "namespace_id": null, "note": "Direct mutation of input signal object and non-null assertion without validation. Input signals should not be mutated directly, and `event.value` may be null for invalid dates.\n\nConsider this approach:\n```typescript\naddEvent(event: MatDatepickerInputEvent) {\n if (event.value) {\n this.dayChange.emit({ ...this.day(), expiryDate: event.value });\n }\n}\n```\n```suggestion:-0+0\n\t\tif (event.value) {\n\t\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value });\n\t\t}\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,101 @@\\n+import { Component, input, output, ChangeDetectionStrategy\n } from '@angular/core';\\n+import { SelectableDayType, tooltipTypeMapping, VacDay\n } from '../../../services/calendar-service';\\n+import { MatCard } from '@angular/material/card';\\n+import\n { MatOption, MatSelect } from '@angular/material/select';\\n+import { MatInput, MatInputModule,\n MatLabel } from '@angular/material/input';\\n+import {\\n+\\tMatDatepicker,\\n+\\tMatDatepickerInput,\\n+\\tMatDatepickerInputEvent,\\n+\\tMatDatepickerToggle,\\n+}\n from '@angular/material/datepicker';\\n+import {\\n+\\tDateAdapter,\\n+\\tMAT_DATE_FORMATS,\\n+\\tMAT_DATE_LOCALE,\\n+\\tNativeDateAdapter,\\n+}\n from '@angular/material/core';\\n+import { MatIconModule } from '@angular/material/icon';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+\\n+class CustomDateAdapter\n extends NativeDateAdapter {\\n+\\toverride format(date: Date, displayFormat: unknown):\n string {\\n+\\t\\tif (displayFormat === 'input') {\\n+\\t\\t\\tconst day = date.getDate().toString().padStart(2,\n '0');\\n+\\t\\t\\tconst month = (date.getMonth() + 1).toString().padStart(2, '0');\\n+\\t\\t\\tconst\n year = date.getFullYear();\\n+\\t\\t\\treturn `${day}/${month}/${year}`;\\n+\\t\\t}\\n+\\t\\treturn\n super.format(date, displayFormat as object);\\n+\\t}\\n+\\n+\\toverride parse(value:\n string): Date | null {\\n+\\t\\tif (!value) return null;\\n+\\t\\tconst parts = value.split('/');\\n+\\t\\tif\n (parts.length === 3) {\\n+\\t\\t\\tconst day = parseInt(parts[0], 10);\\n+\\t\\t\\tconst\n month = parseInt(parts[1], 10) - 1;\\n+\\t\\t\\tconst year = parseInt(parts[2], 10);\\n+\\t\\t\\tif\n (!isNaN(day) && !isNaN(month) && !isNaN(year)) {\\n+\\t\\t\\t\\treturn new Date(year,\n month, day);\\n+\\t\\t\\t}\\n+\\t\\t}\\n+\\t\\treturn null;\\n+\\t}\\n+}\\n+\\n+const MY_DATE_FORMATS\n = {\\n+\\tparse: {\\n+\\t\\tdateInput: 'input',\\n+\\t},\\n+\\tdisplay: {\\n+\\t\\tdateInput:\n 'input',\\n+\\t\\tmonthYearLabel: { year: 'numeric', month: 'short' },\\n+\\t\\tdateA11yLabel:\n { year: 'numeric', month: 'long', day: 'numeric' },\\n+\\t\\tmonthYearA11yLabel: {\n year: 'numeric', month: 'long' },\\n+\\t},\\n+};\\n+\\n+@Component({\\n+\\tselector: 'app-day-input',\\n+\\timports:\n [\\n+\\t\\tMatIconModule,\\n+\\t\\tMatCard,\\n+\\t\\tMatSelect,\\n+\\t\\tMatOption,\\n+\\t\\tMatInput,\\n+\\t\\tMatDatepicker,\\n+\\t\\tMatLabel,\\n+\\t\\tMatDatepickerToggle,\\n+\\t\\tMatDatepickerInput,\\n+\\t\\tMatInputModule,\\n+\\t\\tMatButtonModule,\\n+\\t],\\n+\\ttemplateUrl:\n './day-input.html',\\n+\\tstyleUrl: './day-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n+\\tproviders:\n [\\n+\\t\\t{ provide: DateAdapter, useClass: CustomDateAdapter },\\n+\\t\\t{ provide:\n MAT_DATE_LOCALE, useValue: 'en-GB' },\\n+\\t\\t{ provide: MAT_DATE_FORMATS, useValue:\n MY_DATE_FORMATS },\\n+\\t],\\n+})\\n+export class DayInput {\\n+\\tpublic day = input.required();\\n+\\tpublic\n dayChange = output();\\n+\\tpublic deleteEntry = output();\\n+\\n+\\tprotected\n readonly selectableDayTypes = Object.values(SelectableDayType);\\n+\\tprotected readonly\n tooltipTypeMapping = tooltipTypeMapping;\\n+\\n+\\tonInput(event: Event) {\\n+\\t\\tconst\n value = Number((event.target as HTMLInputElement).value);\\n+\\t\\tthis.day().numberOfDays\n = value;\\n+\\t\\tthis.dayChange.emit(this.day());\\n+\\t}\\n+\\n+\\taddEvent(event: MatDatepickerInputEvent)\n {\\n+\\t\\tthis.day().expiryDate = event.value!;\\n+\\t\\tthis.dayChange.emit(this.day());\"\n:new_path: src/app/components/userInput/day-input/day-input.ts\n:old_path: src/app/components/userInput/day-input/day-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:37", "discussion_id": "e2d12b4c80dfea6e4158940b4e2d4b768510b41e", "id": 311, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_78_75", "namespace_id": null, "note": "Direct mutation of signal array. Similar to `addDay()`, this mutates the signal's underlying array without triggering updates. Use `update()` instead:\n```typescript\npublic removeFromList(index: number) {\n this.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n}\n```\n```suggestion:-0+0\n\t\tthis.userInputService.vacationNumberSignal.update(days => days.filter((_, i) => i !== index));\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,82 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n {\\n+\\tComponent,\\n+\\tOnInit,\\n+\\tOnDestroy,\\n+\\tcomputed,\\n+\\tinject,\\n+\\tChangeDetectionStrategy,\\n+}\n from '@angular/core';\\n import { MatInputModule } from '@angular/material/input';\\n-import\n { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\\n+import\n { ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService } from\n '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n+import { Subject } from 'rxjs';\\n\n import { CalendarSettings } from '../calendar-settings/calendar-settings';\\n import\n { MatCard, MatCardContent } from '@angular/material/card';\\n import { MatIcon }\n from '@angular/material/icon';\\n+import { DayInput } from '../userInput/day-input/day-input';\\n+import\n { MatButtonModule } from '@angular/material/button';\\n+import { tooltipTypeMapping,\n DayType } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector:\n 'app-user-input',\\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n\n \\t\\tReactiveFormsModule,\\n \\t\\tCalendarSettings,\\n \\t\\tMatCard,\\n \\t\\tMatCardContent,\\n\n \\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection: ChangeDetectionStrategy.OnPush,\\n\n })\\n export class UserInput implements OnInit, OnDestroy {\\n-\\tconstructor(private\n userInputService: UserInputService) {\\n-\\t\\t// Watch for signal changes and update\n form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n+\\tprivate userInputService:\n UserInputService = inject(UserInputService);\\n+\\tpublic daysOff = this.userInputService.vacationNumberSignal();\\n\n \\tprivate destroy$ = new Subject();\\n \\n-\\tuserInputForm: FormGroup = new\n FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new FormControl(null),\\n+\\tprotected\n remainingDays = computed(() => {\\n+\\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn\n remaining.reduce((acc, day) => acc + day.numberOfDays, 0);\\n \\t});\\n \\n-\\tprotected\n remainingDays = computed(() => {\\n+\\tprotected remainingDaysDetails = computed(()\n => {\\n \\t\\tconst remaining = this.userInputService.remainingVacationDaysSignal();\\n-\\t\\treturn\n remaining.cp + remaining.rtt + remaining.other;\\n+\\t\\treturn remaining\\n+\\t\\t\\t.filter((day)\n => day.numberOfDays > 0)\\n+\\t\\t\\t.map((day) => {\\n+\\t\\t\\t\\tconst typeName = tooltipTypeMapping[day.type\n as unknown as DayType];\\n+\\t\\t\\t\\tconst expiryDate = day.expiryDate.toLocaleDateString('fr-FR',\n {\\n+\\t\\t\\t\\t\\tday: 'numeric',\\n+\\t\\t\\t\\t\\tmonth: 'short',\\n+\\t\\t\\t\\t\\tyear: 'numeric',\\n+\\t\\t\\t\\t});\\n+\\t\\t\\t\\treturn\n `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;\\n+\\t\\t\\t});\\n \\t});\\n \\n\n \\tprotected hasRemainingDays = computed(() => this.remainingDays() > 0);\\n \\n-\\tngOnInit()\n {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500), takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(()\n => this.submitForm());\\n-\\t}\\n+\\tngOnInit() {}\\n \\n \\tngOnDestroy() {\\n \\t\\tthis.destroy$.next();\\n\n \\t\\tthis.destroy$.complete();\\n \\t}\\n \\n-\\tprivate submitForm() {\\n-\\t\\tif (this.userInputForm.valid)\n {\\n-\\t\\t\\tconst vacationData: VacationsNumber = {\\n-\\t\\t\\t\\tcp: this.userInputForm.get('CP')?.value\n || 0,\\n-\\t\\t\\t\\trtt: this.userInputForm.get('RTT')?.value || 0,\\n-\\t\\t\\t\\tother:\n this.userInputForm.get('Others')?.value || 0,\\n-\\t\\t\\t};\\n-\\t\\t\\tthis.userInputService.vacationNumberSignal.set(vacationData);\\n-\\t\\t}\\n+\\tpublic\n addDay() {\\n+\\t\\tthis.daysOff.push(this.userInputService.createVacationDay());\\n+\\t}\\n+\\n+\\tpublic\n removeFromList(index: number) {\\n+\\t\\tthis.daysOff.splice(index, 1);\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:37", "discussion_id": "94a2efbe55b78a4cf1befeaea906f092142341c8", "id": 312, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "d69b448b6d6842612fa7ccc03914410d4bddeeab_0_16", "namespace_id": null, "note": "The test will fail because `DayInput` has a required input `day` that is not provided. The component requires a `VacDay` object to be passed as input.\n\nAdd the required input:\n```typescript\nbeforeEach(async () => {\n await TestBed.configureTestingModule({\n imports: [DayInput],\n }).compileComponents();\n\n fixture = TestBed.createComponent(DayInput);\n fixture.componentRef.setInput('day', {\n id: 'test-id',\n type: SelectableDayType.CP,\n numberOfDays: 5,\n expiryDate: new Date()\n });\n component = fixture.componentInstance;\n fixture.detectChanges();\n});\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,22 @@\\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\\n+\\n+import\n { DayInput } from './day-input';\\n+\\n+describe('DayInput', () => {\\n+\\tlet component:\n DayInput;\\n+\\tlet fixture: ComponentFixture;\\n+\\n+\\tbeforeEach(async ()\n => {\\n+\\t\\tawait TestBed.configureTestingModule({\\n+\\t\\t\\timports: [DayInput],\\n+\\t\\t}).compileComponents();\\n+\\n+\\t\\tfixture\n = TestBed.createComponent(DayInput);\\n+\\t\\tcomponent = fixture.componentInstance;\\n+\\t\\tfixture.detectChanges();\"\n:new_path: src/app/components/userInput/day-input/day-input.spec.ts\n:old_path: src/app/components/userInput/day-input/day-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:37", "discussion_id": "74a3920641cca225eac5668a4df4fa05b356064a", "id": 313, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "d69b448b6d6842612fa7ccc03914410d4bddeeab_0_12", "namespace_id": null, "note": "Missing test provider for animations. The `DayInput` component uses Material components that depend on animations. Add `provideAnimationsAsync()` to the test configuration to avoid console warnings and potential test failures:\n```typescript\nimport { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\n\nawait TestBed.configureTestingModule({\n imports: [DayInput],\n providers: [provideAnimationsAsync()],\n}).compileComponents();\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,22 @@\\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\\n+\\n+import\n { DayInput } from './day-input';\\n+\\n+describe('DayInput', () => {\\n+\\tlet component:\n DayInput;\\n+\\tlet fixture: ComponentFixture;\\n+\\n+\\tbeforeEach(async ()\n => {\\n+\\t\\tawait TestBed.configureTestingModule({\\n+\\t\\t\\timports: [DayInput],\\n+\\t\\t}).compileComponents();\"\n:new_path: src/app/components/userInput/day-input/day-input.spec.ts\n:old_path: src/app/components/userInput/day-input/day-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:37", "discussion_id": "6304be20f39ab024eb6a253dd8eb1d9a5b06de66", "id": 317, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "039c4c160f861e3a0e271f37b5fff76a6ad3adcf_49_49", "namespace_id": null, "note": "Shallow copy doesn't deep copy the `expiryDate` Date objects. Since `optimizeVacations()` modifies `numberOfDays` on the vacation days objects, and those objects contain mutable `Date` objects, a shallow spread may not be sufficient if dates are mutated. While the current code appears to only mutate `numberOfDays`, the comment claims \"deep copy\" but provides shallow copy.\n\nConsider using a proper deep copy or clarify the comment:\n```typescript\nconst vacationDaysCopy = vacationData.map((vd) => ({ \n ...vd, \n expiryDate: new Date(vd.expiryDate) \n}));\n```\n```suggestion:-0+0\n\t\t\t\t\t\tconst vacationDaysCopy = vacationData.map((vd) => ({\n\t\t\t\t\t\t\t...vd,\n\t\t\t\t\t\t\texpiryDate: vd.expiryDate ? new Date(vd.expiryDate) : vd.expiryDate,\n\t\t\t\t\t\t}));\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/calendar-year/calendar-year.ts\n :new_path: src/app/components/calendar-year/calendar-year.ts\n :position_type: text\n :old_line:\n :new_line: 49\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/calendar-year/calendar-year.ts\n :new_path: src/app/components/calendar-year/calendar-year.ts\n :position_type: text\n :old_line:\n :new_line: 49\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:41", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:38", "discussion_id": "204454ab035f599535b8efe5701de45e33346b6b", "id": 314, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "d69b448b6d6842612fa7ccc03914410d4bddeeab_0_21", "namespace_id": null, "note": "Missing tests for the component's key behaviors. The test suite only verifies component creation but doesn't test the `onInput()` and `addEvent()` methods, or the output events (`dayChange`, `deleteEntry`). Given the repository has comprehensive test coverage for other components and services, consider adding tests for:\n- Input changes emitting `dayChange` event\n- Date changes emitting `dayChange` event \n- Delete button emitting `deleteEntry` event", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,22 @@\\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\\n+\\n+import\n { DayInput } from './day-input';\\n+\\n+describe('DayInput', () => {\\n+\\tlet component:\n DayInput;\\n+\\tlet fixture: ComponentFixture;\\n+\\n+\\tbeforeEach(async ()\n => {\\n+\\t\\tawait TestBed.configureTestingModule({\\n+\\t\\t\\timports: [DayInput],\\n+\\t\\t}).compileComponents();\\n+\\n+\\t\\tfixture\n = TestBed.createComponent(DayInput);\\n+\\t\\tcomponent = fixture.componentInstance;\\n+\\t\\tfixture.detectChanges();\\n+\\t});\\n+\\n+\\tit('should\n create', () => {\\n+\\t\\texpect(component).toBeTruthy();\\n+\\t});\"\n:new_path: src/app/components/userInput/day-input/day-input.spec.ts\n:old_path: src/app/components/userInput/day-input/day-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:42", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:38", "discussion_id": "a7e5abdbeec0eb6faaf6e943874893a064b31116", "id": 316, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_13", "namespace_id": null, "note": "The `valueChange` event emits only the selected value, not an object with a `value` property. Accessing `$event.value` will result in `undefined`. Also, emitting just the type value instead of updating the entire `VacDay` object doesn't align with how other changes are handled.\n\nUse:\n```html\n\n```\n\nOr better, follow the pattern of not mutating inputs:\n```html\n\n```\n```suggestion:-0+0\n\t\t\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,36 @@\\n+\\n+\\n+\\t\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\t\"\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-29T20:16:42", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:38", "discussion_id": "3efd62c46ec1a8f490cf2e2edfdcf1841a45725a", "id": 321, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_92", "namespace_id": null, "note": "No validation for negative or invalid number inputs. The HTML has `min=\"0\"`, but the `Number()` conversion can still produce negative values or `NaN` if the user enters invalid text. Consider adding validation:\n```typescript\nonInput(event: Event) {\n const value = Number((event.target as HTMLInputElement).value);\n if (isNaN(value) || value < 0) return;\n this.dayChange.emit({ ...this.day(), numberOfDays: value });\n}\n```\n```suggestion:-0+0\n\t\tconst value = Number((event.target as HTMLInputElement).value);\n\t\tif (isNaN(value) || value < 0) return;\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.ts\n :new_path: src/app/components/userInput/day-input/day-input.ts\n :position_type: text\n :old_line:\n :new_line: 92\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.ts\n :new_path: src/app/components/userInput/day-input/day-input.ts\n :position_type: text\n :old_line:\n :new_line: 92\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:42", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "5cd45b86c83cb8a89fcf142ac5134a4d7963d737", "confidential": null, "created_at": "2025-11-29T20:16:38", "discussion_id": "b19a66e4b0892155f4dc5f49b37469d42d772b35", "id": 323, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_797_858", "namespace_id": null, "note": "Type casting `vacDay.type as unknown as DayType` indicates a design issue. Since `SelectableDayType` enum values are defined as equal to `DayType` values (lines 207-211), a direct cast should work: `vacDay.type as DayType`. The double cast through `unknown` suggests the types aren't properly aligned.\n```suggestion:-0+0\n\t\tconst vacType = vacDay.type as DayType;\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/services/calendar-service.ts\n :new_path: src/app/services/calendar-service.ts\n :position_type: text\n :old_line:\n :new_line: 858\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/services/calendar-service.ts\n :new_path: src/app/services/calendar-service.ts\n :position_type: text\n :old_line:\n :new_line: 858\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-29T20:16:42", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-29T20:16:39", "discussion_id": "1d9ac7e37b50d40dfadc013c291c83da010c5cb4", "id": 492, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "**Review:** Commented\n\n## Pull request overview\n\nCopilot reviewed 13 out of 14 changed files in this pull request and generated 22 comments.\n\n\n\n", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-29T20:16:39", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T01:47:17", "discussion_id": "455602e37aceae09683cf3eed40c50960f6c1784", "id": 491, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@copilot` fix pipeline\r\n", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-30T01:47:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T01:47:25", "discussion_id": "a0e32bb1ffefb56c45be223f1753bc05656bf28c", "id": 497, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "mentioned in merge request !63", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2026-05-03T16:02:20.950249", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T01:47:27", "discussion_id": "f9b7ab79e0ad436a3773e140cac27afaa267fe49", "id": 494, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@THoguet` I've opened a new pull request, #63, to work on those changes. Once the pull request is ready, I'll request review from you.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-30T01:47:27", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T01:59:05", "discussion_id": "19584670a79e0d0b4c442c9d099405b0fdcb99d3", "id": 496, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "requested review from `@Copilot`", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2025-11-30T01:59:05", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T02:06:25", "discussion_id": "2ba7594cc2f70d3a0fac2ebd65cc9bd6968d5d72", "id": 495, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@copilot` fix unit test and linter", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-30T02:06:25", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T02:06:34", "discussion_id": "cb1d519561ee336fe12ac9006b045c8293006e90", "id": 500, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "mentioned in merge request !64", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": true, "type": null, "updated_at": "2026-05-03T16:02:21.534506", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T02:06:35", "discussion_id": "759fe2d8ffb50e2c039ac076aab31903760f3214", "id": 498, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "`@THoguet` I've opened a new pull request, #64, to work on those changes. Once the pull request is ready, I'll request review from you.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-30T02:06:35", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:11", "discussion_id": "faccdc68c3122f98db4829852b47706fa6c0948d", "id": 315, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_62_53", "namespace_id": null, "note": "The `daysOff` getter directly returns a signal instead of its value. This is inconsistent with Angular best practices for signals. Consider either making this a computed signal or calling the signal to return its value, e.g., `return this.userInputService.vacationNumberSignal();`", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,79 +1,78 @@\\n-import { Component, OnInit, OnDestroy, effect, computed\n } from '@angular/core';\\n-import { MatFormField, MatLabel } from '@angular/material/form-field';\\n+import\n { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';\\n\n import { MatInputModule } from '@angular/material/input';\\n-import { FormControl,\n FormGroup, ReactiveFormsModule } from '@angular/forms';\\n import { UserInputService\n } from '../../services/user-input-service';\\n-import { VacationsNumber } from '../../services/calendar-service';\\n-import\n { debounceTime, takeUntil, Subject } from 'rxjs';\\n import { CalendarSettings }\n from '../calendar-settings/calendar-settings';\\n import { MatCard, MatCardContent\n } from '@angular/material/card';\\n import { MatIcon } from '@angular/material/icon';\\n+import\n { DayInput } from '../userInput/day-input/day-input';\\n+import { MatButtonModule\n } from '@angular/material/button';\\n+import { tooltipTypeMapping, DayType, VacDay\n } from '../../services/calendar-service';\\n \\n @Component({\\n \\tselector: 'app-user-input',\\n\n \\timports: [\\n-\\t\\tMatFormField,\\n-\\t\\tMatLabel,\\n \\t\\tMatInputModule,\\n-\\t\\tReactiveFormsModule,\\n\n \\t\\tCalendarSettings,\\n \\t\\tMatCard,\\n \\t\\tMatCardContent,\\n \\t\\tMatIcon,\\n+\\t\\tMatButtonModule,\\n+\\t\\tDayInput,\\n\n \\t],\\n \\ttemplateUrl: './user-input.html',\\n \\tstyleUrl: './user-input.scss',\\n+\\tchangeDetection:\n ChangeDetectionStrategy.OnPush,\\n })\\n-export class UserInput implements OnInit,\n OnDestroy {\\n-\\tconstructor(private userInputService: UserInputService) {\\n-\\t\\t//\n Watch for signal changes and update form\\n-\\t\\teffect(() => {\\n-\\t\\t\\tconst vacationData\n = this.userInputService.vacationNumberSignal();\\n-\\t\\t\\tthis.userInputForm.patchValue(\\n-\\t\\t\\t\\t{\\n-\\t\\t\\t\\t\\tCP:\n vacationData.cp,\\n-\\t\\t\\t\\t\\tRTT: vacationData.rtt,\\n-\\t\\t\\t\\t\\tOthers: vacationData.other,\\n-\\t\\t\\t\\t},\\n-\\t\\t\\t\\t{\n emitEvent: false },\\n-\\t\\t\\t);\\n-\\t\\t});\\n-\\t}\\n-\\n-\\tprivate destroy$ = new Subject();\\n+export\n class UserInput {\\n+\\tprivate userInputService: UserInputService = inject(UserInputService);\\n\n \\n-\\tuserInputForm: FormGroup = new FormGroup({\\n-\\t\\tCP: new FormControl(null),\\n-\\t\\tRTT: new FormControl(null),\\n-\\t\\tOthers: new\n FormControl(null),\\n+\\tprotected remainingDays = computed(() => {\\n+\\t\\tconst\n remaining = this.userInputService.remainingVacationDaysSignal();\\n+\\t\\treturn remaining.reduce((acc,\n day) => acc + day.numberOfDays, 0);\\n \\t});\\n \\n-\\tprotected remainingDays = computed(()\n => {\\n+\\tprotected remainingDaysDetails = computed(() => {\\n \\t\\tconst remaining\n = this.userInputService.remainingVacationDaysSignal();\\n-\\t\\treturn remaining.cp\n + remaining.rtt + remaining.other;\\n+\\t\\treturn remaining\\n+\\t\\t\\t.filter((day)\n => day.numberOfDays > 0)\\n+\\t\\t\\t.map((day) => {\\n+\\t\\t\\t\\tconst typeName = tooltipTypeMapping[day.type\n as unknown as DayType];\\n+\\t\\t\\t\\tconst expiryDate = day.expiryDate.toLocaleDateString('fr-FR',\n {\\n+\\t\\t\\t\\t\\tday: 'numeric',\\n+\\t\\t\\t\\t\\tmonth: 'short',\\n+\\t\\t\\t\\t\\tyear: 'numeric',\\n+\\t\\t\\t\\t});\\n+\\t\\t\\t\\treturn\n `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;\\n+\\t\\t\\t});\\n \\t});\\n \\n\n \\tprotected hasRemainingDays = computed(() => this.remainingDays() > 0);\\n \\n-\\tngOnInit()\n {\\n-\\t\\tthis.userInputForm.valueChanges\\n-\\t\\t\\t.pipe(debounceTime(1500), takeUntil(this.destroy$))\\n-\\t\\t\\t.subscribe(()\n => this.submitForm());\\n+\\tprotected get daysOff() {\\n+\\t\\treturn this.userInputService.vacationNumberSignal();\\n+\\t}\"\n:new_path: src/app/components/user-input/user-input.ts\n:old_path: src/app/components/user-input/user-input.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:12", "discussion_id": "76afbb7677f332efabeed44167085ab82a209516", "id": 318, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "3d779d5cb5b415447eb2a87265084f8874aabd0b_17_14", "namespace_id": null, "note": "The clickable card uses `(click)=\"addDay()\"` on a `mat-card` element instead of the button inside it. This creates a redundant click handler since the button inside already has keyboard accessibility. The card itself isn't keyboard-accessible unless additional attributes like `tabindex=\"0\"` and `(keydown.enter)` or `(keydown.space)` are added. Consider moving the click handler to just the button, or add proper keyboard event handlers to the card.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -1,29 +1,40 @@\\n
\\n \\t

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

\\n-\\t
\\n-\\t\\t\\n-\\t\\t\\tCombien de CP par an ?\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\t\\n-\\t\\t\\tCombien de jours de RTT par an ?\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\t\\n-\\t\\t\\tAutres jours\\n-\\t\\t\\t\\n-\\t\\t\\n-\\t\\n+\\t\\n+\\t\\t@for (day of daysOff; track day.id) {\\n+\\t\\t\\t
\\n+\\t\\t\\t\\t\\n+\\t\\t\\t
\\n+\\t\\t}\\n+\\t\\t\\n+\\t\\t\\t\\n+\\t\\t\\t

Ajouter\n un jour de vacances

\\n+\\t\\t\"\n:new_path: src/app/components/user-input/user-input.html\n:old_path: src/app/components/user-input/user-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:12", "discussion_id": "8845366b3cc593bf7017a0afd2564884f59b17ed", "id": 319, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_11", "namespace_id": null, "note": "The number input field lacks a label, making it inaccessible to screen reader users. Consider adding a `mat-label` inside the `mat-form-field` to describe what this input represents (e.g., \"Number of days\").", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,36 @@\\n+\\n+\\n+\\t\\n+\\t\\t\\n+\\t\"\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:12", "discussion_id": "da71a155ac1c9d7c97b2d6b8fa2577b9a246b27b", "id": 324, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8e1f6cc26d76fc0712c1ef59d583f5195fa1c31d_0_101", "namespace_id": null, "note": "The non-null assertion operator `!` is used on `event.value`, but there's no guarantee that `event.value` is non-null. If the user clears the date field, `event.value` could be `null`, causing a runtime error. Consider handling the null case explicitly, e.g., by not emitting if the value is null, or using a default date.\n```suggestion:-0+0\n\t\tif (event.value !== null) {\n\t\t\tthis.dayChange.emit({ ...this.day(), expiryDate: event.value });\n\t\t}\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.ts\n :new_path: src/app/components/userInput/day-input/day-input.ts\n :position_type: text\n :old_line:\n :new_line: 101\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.ts\n :new_path: src/app/components/userInput/day-input/day-input.ts\n :position_type: text\n :old_line:\n :new_line: 101\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:12", "discussion_id": "e57d09ac1c9a403401a14e733a29d7e740226762", "id": 326, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_44_27", "namespace_id": null, "note": "[nitpick] The explicit type annotation `: UserInputService` is redundant when using `inject()`, as TypeScript can infer the type. Consider simplifying to: `private userInputService = inject(UserInputService);`\n```suggestion:-0+0\n\tprivate userInputService = inject(UserInputService);\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 27\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 27\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:13", "discussion_id": "6ad84f6ed6c0369d36c14d0c57ab5be1059f9f4c", "id": 320, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "251d3fc6f2820530a2a140154a387327b70bb36b_13_31", "namespace_id": null, "note": "This line has the same date calculation issue. The duplicated logic appears 4 times in this file (lines 19, 25, 31, and 41). Extract this into a helper method like `private getExpiryDateOneYearFromNow(): Date` to eliminate duplication.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -9,18 +9,39 @@ export class UserInputService {\\n \\t\\treturn Math.floor(Math.random()\n * 9) + 1;\\n \\t}\\n \\n-\\tprivate getInitialValues(): VacationsNumber {\\n+\\tprivate\n getInitialValues(): VacDay[] {\\n+\\t\\treturn [\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.CP,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\t//\n today + 365 days\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear()\n + 1)),\\n+\\t\\t\\t},\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype: SelectableDayType.RTT,\\n+\\t\\t\\t\\tnumberOfDays:\n this.getRandomNumber(),\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new\n Date().getFullYear() + 1)),\\n+\\t\\t\\t},\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.OTHER,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\texpiryDate:\n new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\"\n:new_path: src/app/services/user-input-service.ts\n:old_path: src/app/services/user-input-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:13", "discussion_id": "8b41e355df4ce527a6a48f22e74408318dd3a4e7", "id": 322, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_1", "namespace_id": null, "note": "Commented-out code should be removed rather than left in the codebase. If this `mat-list-item` wrapper is no longer needed, delete it. If it might be needed in the future, rely on version control to retrieve it.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: |-\n @@ -0,0 +1,36 @@\n +\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:13", "discussion_id": "d3ad97fb1423601f830fccb9e78f254d3eab6cbc", "id": 325, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "40906f5bb9cb224394fd1a2efe70343fcdea5d2b_0_36", "namespace_id": null, "note": "Commented-out code should be removed. See comment on line 1.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,36 @@\\n+\\n+\\n+\\t\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\t\\n+\\t\\t\\t\\n+\\t\\t\\t@for (dayType\n of selectableDayTypes; track $index) {\\n+\\t\\t\\t\\t{{\n tooltipTypeMapping[dayType] }}\\n+\\t\\t\\t}\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\tChoose a date\\n+\\t\\t\\n+\\t\\t\\n+\\t\\t\\n+\\t\\n+\\t\\n+\\t\\tclose\\n+\\t\\n+\\n+\"\n:new_path: src/app/components/userInput/day-input/day-input.html\n:old_path: src/app/components/userInput/day-input/day-input.html\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:13", "discussion_id": "4d1e419e0f5ebb3f3834621229c4b19a3b6aa6ab", "id": 328, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "afd2e0885e7e7f9007163959d2b32821ce5a7037_54_39", "namespace_id": null, "note": "The type cast `day.type as unknown as DayType` is a code smell. Since `SelectableDayType` values are derived from `DayType`, this cast should be unnecessary. Consider refactoring the type relationship or using a type-safe mapping function.\n```suggestion:-0+0\n\t\t\t\tconst typeName = isDayType(day.type)\n\t\t\t\t\t? tooltipTypeMapping[day.type]\n\t\t\t\t\t: 'Unknown';\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 39\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/user-input/user-input.ts\n :new_path: src/app/components/user-input/user-input.ts\n :position_type: text\n :old_line:\n :new_line: 39\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:14", "discussion_id": "02f44efb1258265012bd52c9213200ecdcc3b429", "id": 327, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "8a1c914605bfd000c88c8b7f451fdd24274756d4_13_13", "namespace_id": null, "note": "The component has been significantly refactored with new methods (`addDay`, `removeFromList`, `updateDay`, `triggerComputation`) and computed signals, but the test file only adds `provideAnimationsAsync()`. Consider adding tests to verify the new functionality works correctly.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -9,7 +10,7 @@ describe('UserInput', () => {\\n \\tbeforeEach(async () =>\n {\\n \\t\\tawait TestBed.configureTestingModule({\\n \\t\\t\\timports: [UserInput],\\n-\\t\\t\\tproviders:\n [provideZonelessChangeDetection()],\\n+\\t\\t\\tproviders: [provideZonelessChangeDetection(),\n provideAnimationsAsync()],\"\n:new_path: src/app/components/user-input/user-input.spec.ts\n:old_path: src/app/components/user-input/user-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:14", "discussion_id": "fb8c9462b424cb123af8f99cb74024b6ee4eeac2", "id": 329, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "81944682105cb64e67b05146dbe82be48d9092a5_0_19", "namespace_id": null, "note": "Using `::ng-deep` is deprecated and discouraged in Angular. It's a workaround that pierces view encapsulation and can cause global style pollution. Consider using Angular Material's theming system or custom form field appearance options instead to hide the subscript wrapper.\n```suggestion:-0+0\n/* To hide the subscript wrapper, add a custom class to your mat-form-field in the template, e.g. */\n/* Then use the following selector: */\n.no-subscript .mat-mdc-form-field-subscript-wrapper {\n```", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.scss\n :new_path: src/app/components/userInput/day-input/day-input.scss\n :position_type: text\n :old_line:\n :new_line: 19\n :line_range:\n :ignore_whitespace_change: false\n", "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :start_sha: 3272622abfbe608161aa51332e21b1cb23e3af93\n :head_sha: a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14\n :old_path: src/app/components/userInput/day-input/day-input.scss\n :new_path: src/app/components/userInput/day-input/day-input.scss\n :position_type: text\n :old_line:\n :new_line: 19\n :line_range:\n :ignore_whitespace_change: false\n", "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": "DiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:14", "discussion_id": "681ee639bd51834d741e238c9729248515c3977c", "id": 330, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "251d3fc6f2820530a2a140154a387327b70bb36b_13_19", "namespace_id": null, "note": "The date calculation creates multiple `new Date()` objects unnecessarily. The expression `new Date(new Date().setFullYear(...))` first creates a Date, mutates it with `setFullYear` (which returns a timestamp), then creates a new Date from that timestamp. This is convoluted. Use a clearer approach: `const date = new Date(); date.setFullYear(date.getFullYear() + 1); return date;` or simply `new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)`", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -9,18 +9,39 @@ export class UserInputService {\\n \\t\\treturn Math.floor(Math.random()\n * 9) + 1;\\n \\t}\\n \\n-\\tprivate getInitialValues(): VacationsNumber {\\n+\\tprivate\n getInitialValues(): VacDay[] {\\n+\\t\\treturn [\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.CP,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\t//\n today + 365 days\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear()\n + 1)),\"\n:new_path: src/app/services/user-input-service.ts\n:old_path: src/app/services/user-input-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:15", "discussion_id": "d74352aa2000f9dde96d5fd76b81b2ca70187d9b", "id": 331, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "251d3fc6f2820530a2a140154a387327b70bb36b_17_41", "namespace_id": null, "note": "This line has the same date calculation issue as the previous occurrences. See comments on lines 19, 25, and 31.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -9,18 +9,39 @@ export class UserInputService {\\n \\t\\treturn Math.floor(Math.random()\n * 9) + 1;\\n \\t}\\n \\n-\\tprivate getInitialValues(): VacationsNumber {\\n+\\tprivate\n getInitialValues(): VacDay[] {\\n+\\t\\treturn [\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.CP,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\t//\n today + 365 days\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear()\n + 1)),\\n+\\t\\t\\t},\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype: SelectableDayType.RTT,\\n+\\t\\t\\t\\tnumberOfDays:\n this.getRandomNumber(),\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new\n Date().getFullYear() + 1)),\\n+\\t\\t\\t},\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.OTHER,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\texpiryDate:\n new Date(new Date().setFullYear(new Date().getFullYear() + 1)),\\n+\\t\\t\\t},\\n+\\t\\t];\\n+\\t}\\n+\\n+\\tpublic\n createVacationDay(): VacDay {\\n \\t\\treturn {\\n-\\t\\t\\tcp: this.getRandomNumber(),\\n-\\t\\t\\trtt:\n this.getRandomNumber(),\\n-\\t\\t\\tother: this.getRandomNumber(),\\n+\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\ttype:\n SelectableDayType.CP,\\n+\\t\\t\\tnumberOfDays: 1,\\n+\\t\\t\\texpiryDate: new Date(new\n Date().setFullYear(new Date().getFullYear() + 1)),\"\n:new_path: src/app/services/user-input-service.ts\n:old_path: src/app/services/user-input-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:15", "discussion_id": "0f3672abec1f39144599e81597215f2992daea76", "id": 332, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "d69b448b6d6842612fa7ccc03914410d4bddeeab_0_32", "namespace_id": null, "note": "The test suite only includes a basic creation test. The component has several behavioral methods (`onInput`, `onTypeChange`, `addEvent`) that lack test coverage. Consider adding tests to verify:\n- `onInput` emits `dayChange` with updated `numberOfDays`\n- `onTypeChange` emits `dayChange` with updated `type`\n- `addEvent` emits `dayChange` with updated `expiryDate`\n- `deleteEntry` output works correctly", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -0,0 +1,33 @@\\n+import { ComponentFixture, TestBed } from '@angular/core/testing';\\n+import\n { provideAnimationsAsync } from '@angular/platform-browser/animations/async';\\n+\\n+import\n { DayInput } from './day-input';\\n+import { SelectableDayType, VacDay } from '../../../services/calendar-service';\\n+\\n+describe('DayInput',\n () => {\\n+\\tlet component: DayInput;\\n+\\tlet fixture: ComponentFixture;\\n+\\n+\\tconst\n mockVacDay: VacDay = {\\n+\\t\\tid: 'test-id',\\n+\\t\\ttype: SelectableDayType.CP,\\n+\\t\\tnumberOfDays:\n 5,\\n+\\t\\texpiryDate: new Date('2025-12-31'),\\n+\\t};\\n+\\n+\\tbeforeEach(async () =>\n {\\n+\\t\\tawait TestBed.configureTestingModule({\\n+\\t\\t\\timports: [DayInput],\\n+\\t\\t\\tproviders:\n [provideAnimationsAsync()],\\n+\\t\\t}).compileComponents();\\n+\\n+\\t\\tfixture = TestBed.createComponent(DayInput);\\n+\\t\\tcomponent\n = fixture.componentInstance;\\n+\\t\\tfixture.componentRef.setInput('day', mockVacDay);\\n+\\t\\tfixture.detectChanges();\\n+\\t});\\n+\\n+\\tit('should\n create', () => {\\n+\\t\\texpect(component).toBeTruthy();\\n+\\t});\"\n:new_path: src/app/components/userInput/day-input/day-input.spec.ts\n:old_path: src/app/components/userInput/day-input/day-input.spec.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:15", "discussion_id": "bb6675dcb5dc016ce9452832cf55f2419bd9befe", "id": 333, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_580_583", "namespace_id": null, "note": "On each iteration of the while loop, the code creates a copy of `vacationDays`, filters it, and sorts it. This is inefficient for large arrays. Consider moving the filtering and sorting outside the loop, or using a more efficient data structure like a priority queue to avoid repeated sorting.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -565,30 +572,30 @@ export class SelectedDates implements SelectedDateInterface\n {\\n \\n \\t\\tawait updateProgress();\\n \\n-\\t\\twhile (vacationsNumber.cp > 0) {\\n-\\t\\t\\tconst\n result = this.lookForVacation(DayType.CP, calendarService);\\n-\\t\\t\\tif (result.heuristic\n === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {\\n-\\t\\t\\t\\tbreak;\\n-\\t\\t\\t}\\n-\\t\\t\\tvacationsNumber.cp\n -= 1;\\n-\\t\\t\\tprocessedDays++;\\n-\\t\\t\\tawait updateProgress();\\n-\\t\\t}\\n-\\t\\twhile\n (vacationsNumber.rtt > 0) {\\n-\\t\\t\\tconst result = this.lookForVacation(DayType.RTT,\n calendarService);\\n-\\t\\t\\tif (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC)\n {\\n+\\t\\t// Track VacDays that can't find valid placements\\n+\\t\\tconst exhaustedVacDayIds\n = new Set();\\n+\\n+\\t\\t// Process vacation days ordered by expiration date\n (earliest first)\\n+\\t\\twhile (vacationDays.some((vd) => vd.numberOfDays > 0 && !exhaustedVacDayIds.has(vd.id)))\n {\\n+\\t\\t\\t// Sort by expiration date to use days that expire first, excluding exhausted\n ones\\n+\\t\\t\\tconst sortedVacDays = [...vacationDays]\\n+\\t\\t\\t\\t.filter((vd) => vd.numberOfDays\n > 0 && !exhaustedVacDayIds.has(vd.id))\\n+\\t\\t\\t\\t.sort((a, b) => a.expiryDate.getTime()\n - b.expiryDate.getTime());\"\n:new_path: src/app/services/calendar-service.ts\n:old_path: src/app/services/calendar-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:15", "discussion_id": "81650705b6a374188a977cf37808b48042490202", "id": 334, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "251d3fc6f2820530a2a140154a387327b70bb36b_13_25", "namespace_id": null, "note": "This line has the same date calculation issue as line 19. Consider extracting this logic into a helper method to avoid duplication and improve maintainability.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -9,18 +9,39 @@ export class UserInputService {\\n \\t\\treturn Math.floor(Math.random()\n * 9) + 1;\\n \\t}\\n \\n-\\tprivate getInitialValues(): VacationsNumber {\\n+\\tprivate\n getInitialValues(): VacDay[] {\\n+\\t\\treturn [\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype:\n SelectableDayType.CP,\\n+\\t\\t\\t\\tnumberOfDays: this.getRandomNumber(),\\n+\\t\\t\\t\\t//\n today + 365 days\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new Date().getFullYear()\n + 1)),\\n+\\t\\t\\t},\\n+\\t\\t\\t{\\n+\\t\\t\\t\\tid: crypto.randomUUID(),\\n+\\t\\t\\t\\ttype: SelectableDayType.RTT,\\n+\\t\\t\\t\\tnumberOfDays:\n this.getRandomNumber(),\\n+\\t\\t\\t\\texpiryDate: new Date(new Date().setFullYear(new\n Date().getFullYear() + 1)),\"\n:new_path: src/app/services/user-input-service.ts\n:old_path: src/app/services/user-input-service.ts\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": "480bcc897e79b456741454b6a098a1901ece326b", "confidential": null, "created_at": "2025-11-30T02:07:16", "discussion_id": "f159bf413193357c908e065fe7f958ed35ed0bc0", "id": 335, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": "e74f810a9e9d48c7cdf65ebea7d9755d835f9a1f_24_23", "namespace_id": null, "note": "Adding `--legacy-peer-deps` flag suggests there are peer dependency conflicts with Angular 21. While this may work, it bypasses npm's peer dependency validation which could hide compatibility issues. Consider documenting why this flag is necessary, or investigate resolving the underlying peer dependency conflicts.", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": "---\n:diff: \"@@ -20,7 +20,7 @@ jobs:\\n cache: 'npm'\\n \\n -\n name: Install dependencies\\n- run: npm ci\\n+ run: npm\n ci --legacy-peer-deps\"\n:new_path: \".github/workflows/pr-validation.yml\"\n:old_path: \".github/workflows/pr-validation.yml\"\n:a_mode: '100644'\n:b_mode: '100644'\n:new_file: false\n", "system": false, "type": "LegacyDiffNote", "updated_at": "2025-11-30T02:07:17", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T02:07:16", "discussion_id": "c1afd237d0459e7fd0d0cf55085c5995d1da07c6", "id": 499, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "**Review:** Commented\n\n## Pull request overview\n\nCopilot reviewed 14 out of 15 changed files in this pull request and generated 19 comments.\n\n\n
\nComments suppressed due to low confidence (1)\n\n**src/app/services/calendar-service.ts:925**\n* The `lookForVacation` method has been modified to filter candidate days based on the expiry date of the `VacDay` object. This new behavior lacks test coverage to ensure days after the expiry date are correctly excluded from consideration.\n```\n\tlookForVacation(\n\t\tvacDay: VacDay,\n\t\tcalendarService: CalendarService,\n\t): { apply: () => void; heuristic: number } {\n\t\tconst daysOfYear = calendarService.monthsInAYearFromNow();\n\t\tconst now = new Date();\n\t\tnow.setHours(0, 0, 0, 0);\n\t\tconst nowTime = now.getTime();\n\t\tconst expiryTime = vacDay.expiryDate.getTime();\n\n\t\t// Preprocess: filter out past days, already selected days, and days after expiry date\n\t\tconst candidateDays: Date[] = [];\n\t\tfor (const month of daysOfYear) {\n\t\t\tfor (const week of month) {\n\t\t\t\tfor (const day of week) {\n\t\t\t\t\tif (day) {\n\t\t\t\t\t\tday.setHours(0, 0, 0, 0);\n\t\t\t\t\t\tconst dayTime = day.getTime();\n\t\t\t\t\t\tif (dayTime >= nowTime && dayTime <= expiryTime && !this.isSelected(day)) {\n\t\t\t\t\t\t\tcandidateDays.push(day);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Early exit if no candidates\n\t\tif (candidateDays.length === 0) {\n\t\t\treturn { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };\n\t\t}\n\n\t\tconst vacType = vacDay.type as unknown as DayType;\n\t\tconst tempDatesBackup = [...this._datesSelected];\n\t\tlet bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;\n\t\tlet bestHeuristic = -Infinity;\n\t\tlet daysWithBestHeuristic = 0;\n\n\t\t// Evaluate each candidate day\n\t\tfor (const day of candidateDays) {\n\t\t\t// Reset to backup state\n\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\tthis.datesSelected = this._datesSelected.slice();\n\t\t\tthis.grouping();\n\n\t\t\t// Mark this day as vacation (reuse the day object since it's already normalized)\n\t\t\tthis.datesSelected.push(new SelectedDate(vacType, new DateRange(day, day)));\n\n\t\t\tconst heuristic = this.computeHeuristics();\n\n\t\t\t// Track only if better than current best\n\t\t\tif (heuristic > bestHeuristic) {\n\t\t\t\tbestHeuristic = heuristic;\n\t\t\t\tdaysWithBestHeuristic = 1;\n\t\t\t\tconst capturedDay = day;\n\t\t\t\tbestStrategy = {\n\t\t\t\t\tapply: () => {\n\t\t\t\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\t\t\t\tthis.push(\n\t\t\t\t\t\t\tnew SelectedDate(\n\t\t\t\t\t\t\t\tvacType,\n\t\t\t\t\t\t\t\tnew DateRange(capturedDay, capturedDay),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\theuristic: heuristic,\n\t\t\t\t\tvalue: capturedDay,\n\t\t\t\t};\n\t\t\t} else if (heuristic === bestHeuristic) {\n\t\t\t\tdaysWithBestHeuristic++;\n\t\t\t}\n\t\t}\n\n\t\t// Check if too many days have the same best heuristic after evaluating all days\n\t\tif (daysWithBestHeuristic > SelectedDates.MAX_DAYS_WITH_SAME_HEURISTIC) {\n\t\t\t// Restore backup before stopping\n\t\t\tthis._datesSelected = [...tempDatesBackup];\n\t\t\tthis.datesSelected = this._datesSelected.slice();\n\t\t\tthis.grouping();\n\n\t\t\t// Stop optimization - let user decide manually\n\t\t\treturn {\n\t\t\t\tapply: () => {},\n\t\t\t\theuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC,\n\t\t\t};\n\t\t}\n\n\t\t// Restore backup before returning\n\t\tthis._datesSelected = [...tempDatesBackup];\n\t\tthis.datesSelected = this._datesSelected.slice();\n\t\tthis.grouping();\n\n\t\t// Apply best strategy if found\n\t\tif (bestStrategy) {\n\t\t\tbestStrategy.apply();\n\t\t\treturn bestStrategy;\n\t\t}\n\n\t\treturn { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };\n\t}\n```\n
\n\n", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2025-11-30T02:07:16", "updated_by_id": null }, { "author_id": 3, "change_position": null, "commit_id": null, "confidential": null, "created_at": "2025-11-30T02:17:57", "discussion_id": "1f07e2584a87d1dda1ec8eb9129921262cee2c06", "id": 501, "imported_from": 4, "internal": false, "last_edited_at": null, "line_code": null, "namespace_id": null, "note": "*Merged by: THoguet at 2025-11-30 02:17:57 UTC*", "noteable_id": 117, "noteable_type": "MergeRequest", "organization_id": null, "original_position": null, "position": null, "project_id": 35, "resolved_at": null, "resolved_by_id": null, "resolved_by_push": null, "review_id": null, "source_iid": 61, "source_item_project_id": 35, "st_diff": null, "system": false, "type": null, "updated_at": "2026-05-03T16:02:21.635646", "updated_by_id": null } ], "preexisting_missing_alternate_head": false, "source_assignees": [], "source_author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "source_created_at": "2025-11-29T18:11:40.000Z", "source_discussion_locked": null, "source_id": 117, "source_iid": 61, "source_item_metadata": { "assignee": null, "assignees": [], "author": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "blocking_discussions_resolved": true, "closed_at": null, "closed_by": null, "created_at": "2025-11-29T18:11:40.000Z", "description": null, "detailed_merge_status": "unchecked", "discussion_locked": null, "downvotes": 0, "draft": false, "force_remove_source_branch": null, "has_conflicts": false, "id": 117, "iid": 61, "imported": true, "imported_from": "github", "labels": [], "merge_after": null, "merge_commit_sha": null, "merge_status": "unchecked", "merge_user": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "merge_when_pipeline_succeeds": false, "merged_at": "2025-11-30T02:17:57.000Z", "merged_by": { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" }, "milestone": null, "prepared_at": null, "project_id": 35, "reference": "!61", "references": { "full": "nessar/vac-optimizer!61", "relative": "!61", "short": "!61" }, "reviewers": [ { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" } ], "sha": "a1f4b6b3f49aa74bc5b4fa4d4796fdabc3983b14", "should_remove_source_branch": null, "source_branch": "THoguet/issue16", "source_project_id": 35, "squash": false, "squash_commit_sha": null, "squash_on_merge": false, "state": "merged", "target_branch": "main", "target_project_id": 35, "task_completion_status": { "completed_count": 0, "count": 0 }, "time_stats": { "human_time_estimate": null, "human_total_time_spent": null, "time_estimate": 0, "total_time_spent": 0 }, "title": "T hoguet/issue16", "updated_at": "2026-05-03T16:01:09.803Z", "upvotes": 0, "user_notes_count": 61, "web_url": "https://gitlab.nessar.fr/nessar/vac-optimizer/-/merge_requests/61", "work_in_progress": false }, "source_kind": "MergeRequest", "source_path": "nessar/vac-optimizer", "source_project_id": 35, "source_reviewers": [ { "avatar_url": "https://secure.gravatar.com/avatar/5e51e115b51304582ee7b2cd8f249fe3ee71b46510e381f74c032e6de81d2757?s=80&d=identicon", "id": 3, "locked": false, "name": "admin", "public_email": null, "state": "active", "username": "admin-backup", "web_url": "https://gitlab.nessar.fr/admin-backup" } ], "source_updated_at": "2026-05-03T16:01:09.803Z", "state_events": [ { "close_after_error_tracking_resolve": false, "close_auto_resolve_prometheus_alert": false, "created_at": "2025-11-30T02:17:57+00:00", "epic_id": null, "id": 239, "imported_from": 4, "issue_id": null, "merge_request_id": 117, "namespace_id": 38, "source_commit": null, "source_merge_request_id": null, "state": 2, "user_id": 3 }, { "close_after_error_tracking_resolve": false, "close_auto_resolve_prometheus_alert": false, "created_at": "2025-11-30T02:17:57+00:00", "epic_id": null, "id": 240, "imported_from": 4, "issue_id": null, "merge_request_id": 117, "namespace_id": 38, "source_commit": "d3515f89e518b20d83d2bd3d15ad2009e52f94a2", "source_merge_request_id": null, "state": 3, "user_id": 3 } ], "target_number": 139 }