Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andTHoguet 95f927386d Add logging service and refactor console.warn to use it
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-17 23:38:23 +00:00
copilot-swe-agent[bot] efe7b3da42 Initial plan 2025-11-17 23:30:03 +00:00
CopilotandTHoguet 54c2160f0d Fix setTimeout async anti-pattern in calendar-year component (#50)
* Initial plan

* fix: replace setTimeout async anti-pattern with queueMicrotask and void

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-18 00:26:18 +01:00
Théophyl HoguetandCopilot 1f3789d82b Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:23:50 +01:00
Théophyl HoguetandCopilot 3fab4c4922 Update src/app/components/user-input/user-input.html
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:21:40 +01:00
Théophyl HoguetandCopilot df1b8a509d Update src/app/components/calendar-year/calendar-year.scss
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:18:19 +01:00
Théophyl HoguetandCopilot b97e9486e8 Update src/app/services/calendar-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-18 00:18:10 +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
nessar 2d5cccff0c chore(deps): update Angular packages to version 20.3.10 2025-11-17 18:56:20 +01:00
dependabot[bot] f30f862aac Bump js-yaml from 3.14.1 to 3.14.2 (#47)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:51:14 +01:00
dependabot[bot] f3d3192aec Bump the development-dependencies group with 6 updates (#40)
Bumps the development-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `20.3.8` | `20.3.9` |
| [@angular/cli](https://github.com/angular/angular-cli) | `20.3.8` | `20.3.9` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `20.3.9` | `20.3.10` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.0` | `9.39.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.0` | `9.39.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.3` | `8.46.4` |


Updates `@angular/build` from 20.3.8 to 20.3.9
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/20.3.8...20.3.9)

Updates `@angular/cli` from 20.3.8 to 20.3.9
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/20.3.8...20.3.9)

Updates `@angular/compiler-cli` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/compiler-cli)

Updates `@eslint/js` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.1/packages/js)

Updates `eslint` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.0...v9.39.1)

Updates `typescript-eslint` from 8.46.3 to 8.46.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 20.3.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 20.3.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 20.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.46.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:50:58 +01:00
dependabot[bot] 97d7e520fc Bump the angular group with 10 updates (#39)
Bumps the angular group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `20.2.11` | `20.2.12` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `20.3.9` | `20.3.10` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `20.3.9` | `20.3.10` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `20.3.9` | `20.3.10` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `20.3.9` | `20.3.10` |
| [@angular/material](https://github.com/angular/components) | `20.2.11` | `20.2.12` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `20.3.9` | `20.3.10` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `20.3.9` | `20.3.10` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `20.3.9` | `20.3.10` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `20.3.8` | `20.3.9` |


Updates `@angular/cdk` from 20.2.11 to 20.2.12
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/20.2.11...20.2.12)

Updates `@angular/common` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/common)

Updates `@angular/compiler` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/compiler)

Updates `@angular/core` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/core)

Updates `@angular/forms` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/forms)

Updates `@angular/material` from 20.2.11 to 20.2.12
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/20.2.11...20.2.12)

Updates `@angular/platform-browser` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/platform-browser)

Updates `@angular/platform-server` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/platform-server)

Updates `@angular/router` from 20.3.9 to 20.3.10
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/20.3.10/packages/router)

Updates `@angular/ssr` from 20.3.8 to 20.3.9
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/20.3.8...20.3.9)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-version: 20.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 20.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 20.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 20.3.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:50:50 +01:00
dependabot[bot] 9eeab40755 Bump node from 20-alpine to 25-alpine (#46)
Bumps node from 20-alpine to 25-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-version: 25-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:49:03 +01:00
dependabot[bot] a06b2077a0 Bump github/codeql-action from 3 to 4 (#45)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:48:51 +01:00
dependabot[bot] a850869282 Bump actions/setup-node from 4 to 6 (#44)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:48:38 +01:00
dependabot[bot] 8b36565816 Bump codecov/codecov-action from 4 to 5 (#43)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:48:27 +01:00
dependabot[bot] 437d5fca9c Bump aquasecurity/trivy-action from 0.27.0 to 0.33.1 (#42)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.27.0 to 0.33.1.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.27.0...0.33.1)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.33.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:48:17 +01:00
dependabot[bot] b86121efac Bump actions/upload-artifact from 4 to 5 (#41)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 18:48:05 +01:00
Théophyl HoguetandCopilot 81d974cbfe T hoguet/issue5 (#37)
* enregistrer les calendar settings dans le localstorage
Fixes #5

* Refactor calendar settings service to use a definitions map for settings and update related components to access settings via a getter method

* Update src/app/services/calendar-settings-service.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor calendar settings service to utilize a new storedSignal implementation for localStorage persistence and restore the stored-signal utility

* Update typescript-eslint to version 8.46.3 and enhance error handling in storedSignal function

* Update src/app/services/calendar-settings-service.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhance storedSignal function to accept an Injector for improved context handling in localStorage persistence

* Skip initial execution in storedSignal effect to prevent unnecessary localStorage writes

* Refactor get method in CalendarSettingsService to throw an error if the setting ID doesn't exist

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 00:19:38 +01:00
Théophyl Hoguet 1922b9acb2 [BUG] click behavior on tooltip day on mobile, slide not working (#36)
Fixes #34
2025-11-06 21:07:58 +01:00
21 changed files with 808 additions and 421 deletions

No files matched your search

+3 -3
View File
@@ -27,15 +27,15 @@ jobs:
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'
+2 -2
View File
@@ -52,14 +52,14 @@ jobs:
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.27.0
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
+6 -6
View File
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
@@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
@@ -65,7 +65,7 @@ jobs:
run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -79,7 +79,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
@@ -98,7 +98,7 @@ jobs:
echo "Build completed successfully"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: build-artifacts
path: dist/
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 1: Build the Angular application
FROM node:20-alpine AS builder
FROM node:25-alpine AS builder
# Set working directory
WORKDIR /app
@@ -17,7 +17,7 @@ COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:20-alpine AS production
FROM node:25-alpine AS production
# Set working directory
WORKDIR /app
+320 -320
View File
File diff suppressed because it is too large. Load diff
+16 -16
View File
@@ -27,16 +27,16 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^20.2.10",
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.7",
"@angular/forms": "^20.3.0",
"@angular/material": "^20.2.10",
"@angular/platform-browser": "^20.3.0",
"@angular/platform-server": "^20.3.0",
"@angular/router": "^20.3.0",
"@angular/ssr": "^20.3.7",
"@angular/cdk": "^20.2.12",
"@angular/common": "^20.3.10",
"@angular/compiler": "^20.3.10",
"@angular/core": "^20.3.10",
"@angular/forms": "^20.3.10",
"@angular/material": "^20.2.12",
"@angular/platform-browser": "^20.3.10",
"@angular/platform-server": "^20.3.10",
"@angular/router": "^20.3.10",
"@angular/ssr": "^20.3.10",
"date-holidays": "^3.26.2",
"dayjs": "^1.11.19",
"express": "^5.1.0",
@@ -44,15 +44,15 @@
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^20.3.7",
"@angular/cli": "^20.3.7",
"@angular/compiler-cli": "^20.3.0",
"@eslint/js": "^9.17.0",
"@angular/build": "^20.3.10",
"@angular/cli": "^20.3.10",
"@angular/compiler-cli": "^20.3.10",
"@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.38.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"jasmine-core": "~5.12.1",
@@ -65,6 +65,6 @@
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.19.1"
"typescript-eslint": "^8.46.4"
}
}
@@ -2,7 +2,7 @@
<h3>{{ getMonthName() }} {{ year }}</h3>
<div>
<div class="row header">
@if (calendarSettingsService.showWeekNumbers()) {
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p class="day empty week-number"></p>
}
<p class="day">L</p>
@@ -16,7 +16,7 @@
<div class="cols">
@for (week of weeksInAMonth; track $index) {
<div class="row week">
@if (calendarSettingsService.showWeekNumbers()) {
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p
class="day week-number"
[class.start]="$index === 0"
@@ -38,7 +38,6 @@
class="day number"
[matTooltip]="selectedDates.getTooltip(day)"
[matTooltipShowDelay]="500"
[matTooltipTouchGestures]="'on'"
>
{{ day.getDate() }}
</p>
@@ -2,7 +2,10 @@
@if (isLoading()) {
<div class="loading-overlay">
<div class="loading-spinner">
<mat-spinner></mat-spinner>
<div class="spinner-container">
<mat-spinner [mode]="'determinate'" [value]="loadingProgress()"></mat-spinner>
<div class="spinner-text">{{ loadingProgress() }}%</div>
</div>
<p>Calcul en cours...</p>
</div>
</div>
@@ -51,25 +51,32 @@
}
}
.spinner-container {
position: relative;
display: inline-block;
::ng-deep .mat-mdc-progress-spinner {
// Override Material spinner size
width: 100px;
height: 100px;
}
.spinner-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
font-weight: bold;
color: #333;
pointer-events: none;
@media (prefers-color-scheme: dark) {
color: #ddd;
}
}
}
.card {
margin: 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
:root {
@include mat.progress-spinner-overrides(
(
size: 0.5rem,
active-indicator-width: 0.25rem,
)
);
}
@@ -7,6 +7,7 @@ import { CommonModule } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
import { LoggerService } from '../../services/logger.service';
@Component({
selector: 'app-calendar-year',
@@ -19,8 +20,10 @@ export class CalendarYear {
private userInput = inject(UserInputService);
private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService);
private logger = inject(LoggerService);
protected isLoading = signal(true);
protected loadingProgress = signal(0);
// Use DateCacheService instead of creating Date objects
private get currentMonth() {
@@ -35,22 +38,31 @@ export class CalendarYear {
effect(() => {
this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = this.userInput.vacationNumberSignal();
const c = this.calendarSettingsService.samediMalin();
const c = this.calendarSettingsService.get('samediMalin')();
console.log('Samedi Malin setting:', c);
console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true);
// Simulate server-side calculation with a small delay
setTimeout(() => {
if (this.selectedDates) {
// Create a copy to avoid mutating the original signal data
this.selectedDates.optimizeVacations(
{ ...vacationData },
this.calendarSettingsService,
this.calendarService,
);
}
this.isLoading.set(false);
}, 1);
this.loadingProgress.set(0);
// Defer execution to allow UI to update
queueMicrotask(() => {
void (async () => {
if (this.selectedDates) {
// Create a copy to avoid mutating the original signal data
const remainingDays = await this.selectedDates.optimizeVacations(
{ ...vacationData },
this.calendarSettingsService,
this.calendarService,
(progress: number) => {
this.loadingProgress.set(progress);
},
this.logger,
);
// Update the remaining vacation days signal
this.userInput.remainingVacationDaysSignal.set(remainingDays);
}
this.isLoading.set(false);
})();
});
});
}
@@ -4,15 +4,15 @@
<p>Configure your calendar settings here.</p>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem">
@for (signal of calendarSettingsService.settings; track signal.id) {
@for (setting of calendarSettingsService.settingsArray; track setting.id) {
<div style="align-self: flex-start">
<mat-slide-toggle
[id]="signal.id"
[name]="signal.id"
[checked]="signal.signal()"
(change)="signal.signal.set($event.checked)"
[id]="setting.id"
[name]="setting.id"
[checked]="setting.signal()"
(change)="setting.signal.set($event.checked)"
>
{{ signal.description }}
{{ setting.description }}
</mat-slide-toggle>
</div>
}
@@ -14,5 +14,16 @@
<input matInput type="number" max="365" min="0" formControlName="Others" />
</mat-form-field>
</form>
@if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card">
<mat-card-content class="warning-content">
<mat-icon color="warn" aria-hidden="true">warning</mat-icon>
<span>
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non
placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement.
</span>
</mat-card-content>
</mat-card>
}
<app-calendar-settings></app-calendar-settings>
</div>
@@ -13,6 +13,16 @@
font-size: 0.875rem;
margin-top: 0.25rem;
}
.warning-card {
margin-bottom: 1rem;
}
.warning-content {
display: flex;
align-items: center;
gap: 0.5rem;
}
}
form {
+13 -2
View File
@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, effect } from '@angular/core';
import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@@ -6,6 +6,8 @@ import { UserInputService } from '../../services/user-input-service';
import { VacationsNumber } from '../../services/calendar-service';
import { debounceTime, takeUntil, Subject } from 'rxjs';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
@Component({
selector: 'app-user-input',
@@ -15,7 +17,9 @@ import { CalendarSettings } from '../calendar-settings/calendar-settings';
MatInputModule,
ReactiveFormsModule,
CalendarSettings,
CalendarSettings,
MatCard,
MatCardContent,
MatIcon,
],
templateUrl: './user-input.html',
styleUrl: './user-input.scss',
@@ -44,6 +48,13 @@ export class UserInput implements OnInit, OnDestroy {
Others: new FormControl<number | null>(null),
});
protected remainingDays = computed(() => {
const remaining = this.userInputService.remainingVacationDaysSignal();
return remaining.cp + remaining.rtt + remaining.other;
});
protected hasRemainingDays = computed(() => this.remainingDays() > 0);
ngOnInit() {
this.userInputForm.valueChanges
.pipe(debounceTime(1500), takeUntil(this.destroy$))
+71 -4
View File
@@ -6,6 +6,7 @@ import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek';
import { CalendarSettingsService } from './calendar-settings-service';
import { DateCacheService } from './date-cache.service';
import { LoggerService } from './logger.service';
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
@@ -21,6 +22,7 @@ const allTypesOptions: HolidaysTypes.Options = {
})
export class CalendarService {
private dateCache = inject(DateCacheService);
private logger = inject(LoggerService);
// Cache for holiday timestamps
private holidayTimestampsCache: Set<number> | null = null;
@@ -298,6 +300,7 @@ export class SelectedDates implements SelectedDateInterface {
// Constant to indicate no valid strategy was found
private static readonly NO_VALID_STRATEGY_HEURISTIC = -1;
private static readonly MAX_DAYS_WITH_SAME_HEURISTIC = 9;
update(): void {
this.datesSelected = this._datesSelected.slice();
@@ -501,7 +504,7 @@ export class SelectedDates implements SelectedDateInterface {
// Check if adjacent (next day) - this is the best case!
if (nextStartTime - currentEndTime === oneDayInMs) {
totalScore += 10; // Big bonus for adjacency
totalScore += 15; // 50% increased bonus for adjacency; adjacency is highly valued in the scoring system
} else if (nextStartTime > currentEndTime) {
// Penalty based on days between the two dates
const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1;
@@ -535,18 +538,44 @@ export class SelectedDates implements SelectedDateInterface {
return totalScore;
}
optimizeVacations(
async optimizeVacations(
vacationsNumber: VacationsNumber,
calendarSettingsService: CalendarSettingsService,
calendarService: CalendarService,
): void {
if (calendarSettingsService.samediMalin()) this.samediMalin(vacationsNumber);
progressCallback?: (progress: number) => void,
logger?: LoggerService,
): Promise<VacationsNumber> {
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
const initialVacations = { ...vacationsNumber };
const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other;
let processedDays = 0;
// Helper function to update progress, throttled to every 5%
let lastReportedProgress = 0;
const PROGRESS_STEP = 5; // percent
const updateProgress = async () => {
if (progressCallback && totalDays > 0) {
const progress = Math.round((processedDays / totalDays) * 100);
if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) {
progressCallback(progress);
lastReportedProgress = progress;
// Allow UI to update with a microtask
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
};
await updateProgress();
while (vacationsNumber.cp > 0) {
const result = this.lookForVacation(DayType.CP, calendarService);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
break;
}
vacationsNumber.cp -= 1;
processedDays++;
await updateProgress();
}
while (vacationsNumber.rtt > 0) {
const result = this.lookForVacation(DayType.RTT, calendarService);
@@ -554,6 +583,8 @@ export class SelectedDates implements SelectedDateInterface {
break;
}
vacationsNumber.rtt -= 1;
processedDays++;
await updateProgress();
}
while (vacationsNumber.other > 0) {
const result = this.lookForVacation(DayType.OTHER, calendarService);
@@ -561,7 +592,25 @@ export class SelectedDates implements SelectedDateInterface {
break;
}
vacationsNumber.other -= 1;
processedDays++;
await updateProgress();
}
// Final progress update (100%)
if (progressCallback) {
progressCallback(100);
}
// Inform user if not all days were placed
const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other;
if (remainingDays > 0 && logger) {
logger.warn(
`Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +
`Too many days have equivalent placement quality - please place remaining days manually.`,
);
}
return vacationsNumber;
}
samediMalin(vacationsNumber: VacationsNumber): void {
@@ -751,6 +800,7 @@ export class SelectedDates implements SelectedDateInterface {
const tempDatesBackup = [...this._datesSelected];
let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;
let bestHeuristic = -Infinity;
let daysWithBestHeuristic = 0;
// Evaluate each candidate day
for (const day of candidateDays) {
@@ -767,6 +817,7 @@ export class SelectedDates implements SelectedDateInterface {
// Track only if better than current best
if (heuristic > bestHeuristic) {
bestHeuristic = heuristic;
daysWithBestHeuristic = 1;
const capturedDay = day;
bestStrategy = {
apply: () => {
@@ -781,9 +832,25 @@ export class SelectedDates implements SelectedDateInterface {
heuristic: heuristic,
value: capturedDay,
};
} else if (heuristic === bestHeuristic) {
daysWithBestHeuristic++;
}
}
// Check if too many days have the same best heuristic after evaluating all days
if (daysWithBestHeuristic > SelectedDates.MAX_DAYS_WITH_SAME_HEURISTIC) {
// Restore backup before stopping
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
// Stop optimization - let user decide manually
return {
apply: () => {},
heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC,
};
}
// Restore backup before returning
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
@@ -17,17 +17,29 @@ describe('CalendarSettingsService', () => {
});
it('should initialize with default values', () => {
expect(service.showWeekNumbers()).toBe(false);
expect(service.samediMalin()).toBe(true);
expect(service.get('showWeekNumbers')()).toBe(false);
expect(service.get('samediMalin')()).toBe(true);
});
it('should allow updating showWeekNumbers', () => {
service.showWeekNumbers.set(true);
expect(service.showWeekNumbers()).toBe(true);
service.get('showWeekNumbers').set(true);
expect(service.get('showWeekNumbers')()).toBe(true);
});
it('should allow updating samediMalin', () => {
service.samediMalin.set(false);
expect(service.samediMalin()).toBe(false);
service.get('samediMalin').set(false);
expect(service.get('samediMalin')()).toBe(false);
});
it('should provide settings array for UI iteration', () => {
const settingsArray = service.settingsArray;
expect(settingsArray.length).toBe(2);
expect(settingsArray[0].id).toBe('showWeekNumbers');
expect(settingsArray[1].id).toBe('samediMalin');
});
it('should return correct descriptions', () => {
expect(service.getDescription('showWeekNumbers')).toBe('Afficher les numéros de semaine');
expect(service.getDescription('samediMalin')).toBe('Activer le Samedi Malin');
});
});
+58 -18
View File
@@ -1,8 +1,8 @@
import { Injectable, signal, WritableSignal } from '@angular/core';
import { Injectable, WritableSignal, inject, Injector } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
interface CalendarSetting {
signal: WritableSignal<boolean>;
id: string;
interface SettingDefinition {
defaultValue: boolean;
description: string;
}
@@ -10,19 +10,59 @@ interface CalendarSetting {
providedIn: 'root',
})
export class CalendarSettingsService {
readonly showWeekNumbers = signal(false);
readonly samediMalin = signal(true);
private readonly injector = inject(Injector);
readonly settings: readonly CalendarSetting[] = [
{
signal: this.showWeekNumbers,
id: 'showWeekNumbers',
description: 'Afficher les numéros de semaine',
},
{
signal: this.samediMalin,
id: 'samediMalin',
description: 'Activer le Samedi Malin',
},
];
private readonly definitions = new Map<string, SettingDefinition>([
[
'showWeekNumbers',
{
defaultValue: false,
description: 'Afficher les numéros de semaine',
},
],
[
'samediMalin',
{
defaultValue: true,
description: 'Activer le Samedi Malin',
},
],
]);
private readonly signals = new Map<string, WritableSignal<boolean>>();
readonly settingsArray: ReadonlyArray<{
id: string;
signal: WritableSignal<boolean>;
description: string;
}>;
constructor() {
for (const [id, def] of this.definitions) {
const storageKey = `calendarSettings.${id}`;
this.signals.set(id, storedSignal(storageKey, def.defaultValue, this.injector));
}
this.settingsArray = Array.from(this.definitions.entries()).map(([id, def]) => ({
id,
signal: this.signals.get(id)!,
description: def.description,
}));
}
/**
* Returns the signal for the given setting ID.
* @throws Error if the setting ID doesn't exist
*/
get(id: string): WritableSignal<boolean> {
const signal = this.signals.get(id);
if (!signal) {
throw new Error(`Setting with id "${id}" does not exist`);
}
return signal;
}
getDescription(id: string): string {
return this.definitions.get(id)?.description ?? '';
}
}
+52
View File
@@ -0,0 +1,52 @@
import { TestBed } from '@angular/core/testing';
import { LoggerService } from './logger.service';
describe('LoggerService', () => {
let service: LoggerService;
let consoleLogSpy: jasmine.Spy;
let consoleWarnSpy: jasmine.Spy;
let consoleErrorSpy: jasmine.Spy;
let consoleDebugSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LoggerService);
// Spy on console methods
consoleLogSpy = spyOn(console, 'log');
consoleWarnSpy = spyOn(console, 'warn');
consoleErrorSpy = spyOn(console, 'error');
consoleDebugSpy = spyOn(console, 'debug');
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should log messages', () => {
service.log('test message');
expect(consoleLogSpy).toHaveBeenCalledWith('test message');
});
it('should log warnings', () => {
service.warn('test warning');
expect(consoleWarnSpy).toHaveBeenCalledWith('test warning');
});
it('should log errors', () => {
service.error('test error');
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
});
it('should log debug messages', () => {
service.debug('test debug');
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug');
});
it('should pass additional arguments to console methods', () => {
const arg1 = { key: 'value' };
const arg2 = [1, 2, 3];
service.log('test', arg1, arg2);
expect(consoleLogSpy).toHaveBeenCalledWith('test', arg1, arg2);
});
});
+47
View File
@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
/**
* Logger service that wraps console methods.
* Can be configured to disable logging in production environments.
*/
@Injectable({
providedIn: 'root',
})
export class LoggerService {
private isProduction = false; // Can be configured based on environment
/**
* Log an informational message
*/
log(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.log(message, ...args);
}
}
/**
* Log a warning message
*/
warn(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.warn(message, ...args);
}
}
/**
* Log an error message
*/
error(message: string, ...args: unknown[]): void {
// Always log errors, even in production
console.error(message, ...args);
}
/**
* Log a debug message
*/
debug(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.debug(message, ...args);
}
}
}
+5
View File
@@ -18,4 +18,9 @@ export class UserInputService {
}
readonly vacationNumberSignal = signal<VacationsNumber>(this.getInitialValues());
readonly remainingVacationDaysSignal = signal<VacationsNumber>({
cp: 0,
rtt: 0,
other: 0,
});
}
+111
View File
@@ -0,0 +1,111 @@
import { signal, effect, WritableSignal, Injector, runInInjectionContext } from '@angular/core';
/**
* Checks if localStorage is available in the current environment.
* @returns true if localStorage is accessible, false otherwise
*/
function isLocalStorageAvailable(): boolean {
try {
return typeof localStorage !== 'undefined' && localStorage !== null;
} catch {
return false;
}
}
/**
* Creates a signal that automatically persists its value to localStorage.
*
* The signal will:
* - Initialize with the stored value from localStorage if available
* - Fall back to the provided initial value if no stored value exists or parsing fails
* - Automatically sync changes to localStorage with debouncing (300ms delay)
* - Work safely in SSR and environments where localStorage is unavailable
*
* @template T - The type of value stored in the signal (must be JSON-serializable)
* @param key - The localStorage key used to store the value
* @param initialValue - The default value used when no stored value exists
* @param injector - The Angular injector (use inject(Injector) to get current context)
* @param debounceMs - Optional debounce delay in milliseconds (default: 300ms)
* @returns A writable signal that persists to localStorage
*
* @example
* ```typescript
* const injector = inject(Injector);
* const darkMode = storedSignal('theme.darkMode', false, injector);
* darkMode.set(true); // Automatically saved to localStorage after 300ms
* console.log(darkMode()); // true
* ```
*
* @remarks
* - Requires an Injector to create the effect in the proper context
* - Values must be JSON-serializable (no functions, circular references, etc.)
* - In SSR or when localStorage is unavailable, the signal works normally but won't persist
* - Corrupted localStorage data is automatically cleared and reset to initial value
* - localStorage writes are debounced to improve performance with frequent updates
*/
export function storedSignal<T>(
key: string,
initialValue: T,
injector: Injector,
debounceMs = 300,
): WritableSignal<T> {
let parsedValue = initialValue;
if (isLocalStorageAvailable()) {
try {
const storedValue = localStorage.getItem(key);
if (storedValue) {
parsedValue = JSON.parse(storedValue) as T;
}
} catch (error) {
console.warn(
`Failed to parse stored value for key "${key}". Using initial value.`,
error,
);
try {
localStorage.removeItem(key);
} catch {
// Ignore removal errors
}
}
}
const s = signal(parsedValue);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let isFirstRun = true;
runInInjectionContext(injector, () => {
effect(() => {
const value = s();
// Skip the first execution to avoid writing back what we just read
if (isFirstRun) {
isFirstRun = false;
return;
}
// Clear any pending timeout
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// Debounce the localStorage write
timeoutId = setTimeout(() => {
if (isLocalStorageAvailable()) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(
`Failed to save value for key "${key}" to localStorage.`,
error,
);
}
}
timeoutId = null;
}, debounceMs);
});
});
return s;
}