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
18 changed files with 15285 additions and 15060 deletions

No files matched your search

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