Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andTHoguet 9b91f50a25 fix: add --legacy-peer-deps to npm ci for Angular 21 compatibility
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-30 01:51:23 +00:00
copilot-swe-agent[bot] 501bbc1e5a Initial plan 2025-11-30 01:47:24 +00:00
CopilotandTHoguet dea632f67d Fix signal immutability violations and cleanup unused code (#62)
* Initial plan

* fix: address PR review comments for signal immutability and cleanup

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-30 01:24:26 +01:00
nessar 5cd45b86c8 feat: enhance vacation optimization logic and UI components 2025-11-29 21:09:21 +01:00
Théophyl Hoguet a9c7e51d36 Merge branch 'main' into THoguet/issue16 2025-11-29 19:15:19 +01:00
nessar e10f2ff4e5 feat: upgrade Angular dependencies to version 21 and implement new animation features
- Updated Angular dependencies in package.json to version 21.x.x.
- Added `provideAnimationsAsync` to app configuration for improved animation handling.
- Refactored user input component to utilize a dynamic list of vacation days with animations.
- Introduced a new `DayInput` component for managing individual vacation days.
- Enhanced styling and animations for user input and day input components.
- Updated calendar service to support new vacation day structure.
- Adjusted tests to accommodate changes in component structure and dependencies.
2025-11-29 19:08:28 +01:00
nessar 5ab29ef770 feat: enhance loading spinner with progress indication and add warning for unplaced vacation days 2025-11-18 00:14:01 +01:00
17 changed files with 15283 additions and 15058 deletions

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v4
@@ -1,28 +0,0 @@
name: Dependabot Auto-merge
on:
pull_request:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
name: Auto-merge Dependabot minor and patch updates
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.5.0
- name: Auto-merge minor and patch updates
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -16,13 +16,13 @@ jobs:
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -52,7 +52,7 @@ jobs:
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.34.2 uses: aquasecurity/trivy-action@0.33.1
with: with:
image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }} image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }}
format: 'sarif' format: 'sarif'
+5 -5
View File
@@ -11,7 +11,7 @@ jobs:
name: Lint Check name: Lint Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -32,7 +32,7 @@ jobs:
name: TypeScript Type Check name: TypeScript Type Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -50,7 +50,7 @@ jobs:
name: Unit Tests name: Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -76,7 +76,7 @@ jobs:
name: Build Application name: Build Application
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -98,7 +98,7 @@ jobs:
echo "Build completed successfully" echo "Build completed successfully"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v5
with: with:
name: build-artifacts name: build-artifacts
path: dist/ path: dist/
+27 -27
View File
@@ -4,31 +4,31 @@ import prettier from 'eslint-config-prettier';
import prettierPlugin from 'eslint-plugin-prettier'; import prettierPlugin from 'eslint-plugin-prettier';
export default [ export default [
eslintJs.configs.recommended, eslintJs.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
prettier, prettier,
{ {
files: ['**/*.ts'], files: ['**/*.ts'],
plugins: { plugins: {
prettier: prettierPlugin, prettier: prettierPlugin,
}, },
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
projectService: true, projectService: true,
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
rules: { rules: {
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'error', 'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
], ],
}, },
}, },
{ {
ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'], ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'],
}, },
]; ];
+15134 -14854
View File
File diff suppressed because it is too large. Load diff
+69 -69
View File
@@ -1,71 +1,71 @@
{ {
"name": "vac-optimizer", "name": "vac-optimizer",
"version": "1.5.0", "version": "1.5.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs", "serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs",
"lint": "eslint \"src/**/*.ts\"", "lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix", "lint:fix": "eslint \"src/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"", "format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\"" "format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\""
}, },
"prettier": { "prettier": {
"printWidth": 100, "printWidth": 100,
"singleQuote": true, "singleQuote": true,
"overrides": [ "overrides": [
{ {
"files": "*.html", "files": "*.html",
"options": { "options": {
"parser": "angular" "parser": "angular"
} }
} }
] ]
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "21.2.0", "@angular/animations": "^21.0.1",
"@angular/cdk": "^21.2.0", "@angular/cdk": "^21.0.1",
"@angular/common": "21.2.0", "@angular/common": "^21.0.1",
"@angular/compiler": "21.2.0", "@angular/compiler": "^21.0.1",
"@angular/core": "21.2.0", "@angular/core": "^21.0.1",
"@angular/forms": "21.2.0", "@angular/forms": "^21.0.1",
"@angular/material": "^21.2.0", "@angular/material": "^21.0.1",
"@angular/platform-browser": "21.2.0", "@angular/platform-browser": "^21.0.1",
"@angular/platform-server": "21.2.0", "@angular/platform-server": "^21.0.1",
"@angular/router": "21.2.0", "@angular/router": "^21.0.1",
"@angular/ssr": "21.2.0", "@angular/ssr": "^21.0.1",
"date-holidays": "^3.26.8", "date-holidays": "^3.26.2",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"express": "^5.2.1", "express": "^5.1.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.0",
"tslib": "^2.8.1" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "21.2.0", "@angular/build": "^21.0.1",
"@angular/cli": "21.2.0", "@angular/cli": "^21.0.1",
"@angular/compiler-cli": "21.2.0", "@angular/compiler-cli": "^21.0.1",
"@eslint/js": "^10.0.1", "@eslint/js": "^9.39.1",
"@types/express": "^5.0.6", "@types/express": "^5.0.1",
"@types/jasmine": "~6.0.0", "@types/jasmine": "~5.1.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.3.3", "@types/node": "^24.10.0",
"eslint": "^10.0.2", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.4",
"jasmine-core": "~6.1.0", "jasmine-core": "~5.12.1",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-preset-angular": "^16.1.1", "jest-preset-angular": "^15.0.3",
"karma": "~6.4.4", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.2.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.8.1", "prettier": "^3.6.2",
"typescript": "~5.9.3", "typescript": "~5.9.2",
"typescript-eslint": "^8.56.1" "typescript-eslint": "^8.46.4"
} }
} }
@@ -1,4 +1,4 @@
import { Component, effect, signal, inject, untracked } from '@angular/core'; import { Component, effect, signal, inject } from '@angular/core';
import { UserInputService } from '../../services/user-input-service'; import { UserInputService } from '../../services/user-input-service';
import { MatCard, MatCardContent } from '@angular/material/card'; import { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month'; import { CalendarMonth } from '../calendar-month/calendar-month';
@@ -34,11 +34,9 @@ export class CalendarYear {
constructor() { constructor() {
effect(() => { effect(() => {
this.userInput.computeTriggerSignal();
this.selectedDates = this.calendarService.getSelectedDatesForYear(); this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = untracked(() => this.userInput.vacationNumberSignal()); const vacationData = this.userInput.vacationNumberSignal();
const c = untracked(() => this.calendarSettingsService.get('samediMalin')()); const c = this.calendarSettingsService.get('samediMalin')();
console.log('Samedi Malin setting:', c); console.log('Samedi Malin setting:', c);
console.log('Optimizing vacations with data:', vacationData); console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true); this.isLoading.set(true);
@@ -3,11 +3,7 @@
<div class="listOfDays"> <div class="listOfDays">
@for (day of daysOff; track day.id) { @for (day of daysOff; track day.id) {
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave"> <div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
<app-day-input <app-day-input [day]="day" (dayChange)="updateDay($index, $event)" (deleteEntry)="removeFromList($index)"></app-day-input>
[day]="day"
(dayChange)="updateDay($index, $event)"
(deleteEntry)="removeFromList($index)"
></app-day-input>
</div> </div>
} }
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()"> <mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
@@ -2,12 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core'; import { provideZonelessChangeDetection } from '@angular/core';
import { UserInput } from './user-input'; import { UserInput } from './user-input';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { UserInputService } from '../../services/user-input-service';
describe('UserInput', () => { describe('UserInput', () => {
let component: UserInput; let component: UserInput;
let fixture: ComponentFixture<UserInput>; let fixture: ComponentFixture<UserInput>;
let userInputService: UserInputService;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@@ -17,7 +15,6 @@ describe('UserInput', () => {
fixture = TestBed.createComponent(UserInput); fixture = TestBed.createComponent(UserInput);
component = fixture.componentInstance; component = fixture.componentInstance;
userInputService = TestBed.inject(UserInputService);
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -25,22 +22,20 @@ describe('UserInput', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should initialize with vacation days from service', () => { it('should initialize form with values from service', () => {
const vacationDays = userInputService.vacationNumberSignal(); expect(component.userInputForm).toBeDefined();
expect(vacationDays).toBeDefined(); expect(component.userInputForm.get('CP')).toBeDefined();
expect(Array.isArray(vacationDays)).toBe(true); expect(component.userInputForm.get('RTT')).toBeDefined();
expect(vacationDays.length).toBeGreaterThan(0); expect(component.userInputForm.get('Others')).toBeDefined();
}); });
it('should add a new vacation day', () => { it('should have valid form controls', () => {
const initialLength = userInputService.vacationNumberSignal().length; const cpControl = component.userInputForm.get('CP');
component.addDay(); const rttControl = component.userInputForm.get('RTT');
expect(userInputService.vacationNumberSignal().length).toBe(initialLength + 1); const othersControl = component.userInputForm.get('Others');
});
it('should remove a vacation day', () => { expect(cpControl).toBeTruthy();
const initialLength = userInputService.vacationNumberSignal().length; expect(rttControl).toBeTruthy();
component.removeFromList(0); expect(othersControl).toBeTruthy();
expect(userInputService.vacationNumberSignal().length).toBe(initialLength - 1);
}); });
}); });
+2 -1
View File
@@ -72,6 +72,7 @@ export class UserInput {
} }
public triggerComputation() { public triggerComputation() {
this.userInputService.requestComputation(); // Trigger signal update by setting a new array reference
this.userInputService.vacationNumberSignal.update((days) => [...days]);
} }
} }
@@ -5,6 +5,7 @@
justify-content: space-between; justify-content: space-between;
padding: 16px; padding: 16px;
gap: 16px; gap: 16px;
flex-wrap: wrap;
} }
.dateInput { .dateInput {
+1 -1
View File
@@ -273,7 +273,7 @@ export class SelectedDate implements SelectedDateInterface {
getClasses(date: Date): string { getClasses(date: Date): string {
let returnType: DayType | string = ''; let returnType: DayType | string = '';
if (this.type && date >= this.range.start! && date <= this.range.end!) { if (this.type && date >= this.range.start! && date <= this.range.end!) {
returnType += this.type + ' '; returnType = returnType += this.type + ' ';
} }
return returnType; return returnType;
} }
+9 -32
View File
@@ -1,9 +1,8 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core'; import { provideZonelessChangeDetection } from '@angular/core';
import { UserInputService } from './user-input-service'; import { UserInputService } from './user-input-service';
import { SelectableDayType, VacDay } from './calendar-service';
describe('UserInputService', () => { describe('UserInput', () => {
let service: UserInputService; let service: UserInputService;
beforeEach(() => { beforeEach(() => {
@@ -17,41 +16,19 @@ describe('UserInputService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should initialize with vacation days array', () => { it('should initialize with random vacation numbers', () => {
const vacations = service.vacationNumberSignal(); const vacations = service.vacationNumberSignal();
expect(Array.isArray(vacations)).toBe(true); expect(vacations.cp).toBeGreaterThanOrEqual(1);
expect(vacations.length).toBeGreaterThan(0); expect(vacations.cp).toBeLessThanOrEqual(9);
}); expect(vacations.rtt).toBeGreaterThanOrEqual(1);
expect(vacations.rtt).toBeLessThanOrEqual(9);
it('should have vacation days with valid properties', () => { expect(vacations.other).toBeGreaterThanOrEqual(1);
const vacations = service.vacationNumberSignal(); expect(vacations.other).toBeLessThanOrEqual(9);
vacations.forEach((day) => {
expect(day.id).toBeDefined();
expect(day.type).toBeDefined();
expect(day.numberOfDays).toBeGreaterThanOrEqual(1);
expect(day.numberOfDays).toBeLessThanOrEqual(9);
expect(day.expiryDate).toBeInstanceOf(Date);
});
}); });
it('should update vacation number signal', () => { it('should update vacation number signal', () => {
const newVacations: VacDay[] = [ const newVacations = { cp: 5, rtt: 3, other: 2 };
{
id: 'test-id',
type: SelectableDayType.CP,
numberOfDays: 5,
expiryDate: new Date(),
},
];
service.vacationNumberSignal.set(newVacations); service.vacationNumberSignal.set(newVacations);
expect(service.vacationNumberSignal()).toEqual(newVacations); expect(service.vacationNumberSignal()).toEqual(newVacations);
}); });
it('should create a new vacation day', () => {
const newDay = service.createVacationDay();
expect(newDay.id).toBeDefined();
expect(newDay.type).toBe(SelectableDayType.CP);
expect(newDay.numberOfDays).toBe(1);
expect(newDay.expiryDate).toBeInstanceOf(Date);
});
}); });
-5
View File
@@ -44,9 +44,4 @@ export class UserInputService {
readonly vacationNumberSignal = signal<VacDay[]>(this.getInitialValues()); readonly vacationNumberSignal = signal<VacDay[]>(this.getInitialValues());
readonly remainingVacationDaysSignal = signal<VacDay[]>([]); readonly remainingVacationDaysSignal = signal<VacDay[]>([]);
readonly computeTriggerSignal = signal(0);
public requestComputation(): void {
this.computeTriggerSignal.update((count) => count + 1);
}
} }
+9 -3
View File
@@ -4,8 +4,14 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": ["node"] "types": [
"node"
]
}, },
"include": ["src/**/*.ts"], "include": [
"exclude": ["src/**/*.spec.ts"] "src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
} }
+6 -2
View File
@@ -4,7 +4,11 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": ["jasmine"] "types": [
"jasmine"
]
}, },
"include": ["src/**/*.ts"] "include": [
"src/**/*.ts"
]
} }