Compare commits

...
Author SHA1 Message Date
nessar 2cf2833683 refactor: improve type safety in DayInput test and remove unused heuristic counter in calendar service 2026-05-31 01:32:53 +02:00
nessar fb3f0ba800 feat: add date range selection to user input service and update translations 2026-05-31 01:29:07 +02:00
nessar 9c9114879d feat: implement internationalization with multi-language support and runtime language selection 2026-05-30 21:03:12 +02:00
nessar 2de09383b9 chore: install Node.js dependency and disable prerendering in build configuration 2026-05-30 18:16:11 +02:00
nessar 9a77665593 build: add oven/bun:latest image to build stage 2026-05-30 17:43:36 +02:00
nessar d22c8845a6 feat: add dayjs dependencies, adjust budget, and initialize agent verification rules 2026-05-30 17:05:59 +02:00
nessar a508c42694 chore: migrate vitest coverage provider from v8 to istanbul 2026-05-30 16:20:21 +02:00
nessar 7ce139c310 forks 2026-05-30 16:12:14 +02:00
nessar edc003ff49 chore: remove jasmine from compiler options types in tsconfig.spec.json 2026-05-30 16:07:24 +02:00
nessar 598e071bd6 style: update indentation in test-setup.ts to use tabs 2026-05-30 16:02:17 +02:00
nessar 11924df19e refactor: migrate testing framework from Karma/Jasmine to Vitest with Angular support 2026-05-30 15:57:36 +02:00
nessar 237d995e16 refactor: clean up karma configuration by removing unused launcher flags and simplifying reporter formatting 2026-05-30 15:45:45 +02:00
nessar 3a105097c3 feat: add Karma configuration with ChromeHeadlessCI launcher and update test CI pipeline 2026-05-30 15:40:16 +02:00
nessar 7702efc165 style: apply consistent code formatting and indentation to rules panel component 2026-05-30 15:26:28 +02:00
nessar 17afa909a9 chore: update healthcheck command to use bun instead of node in compose files 2026-05-30 15:03:59 +02:00
nessar 3b042fa4f5 chore: migrate build and CI pipeline from npm to bun 2026-05-30 15:01:57 +02:00
nessar 08ee84e5c9 feat: implement rules engine and management panel for custom holiday optimization 2026-05-30 15:00:07 +02:00
nessar c0549de4dd ci: switch to --password-stdin for secure docker registry login 2026-05-05 21:26:27 +02:00
nessar 4dd5ce6f5f ci: configure and use chromium-no-sandbox wrapper for test execution 2026-05-05 21:17:25 +02:00
nessar 9e99fc5511 refactor: modularize npm setup using hidden job template and add Chromium dependency for test stage 2026-05-05 21:12:50 +02:00
nessar e73b68e8d2 chore: migrate CI/CD pipeline from GitHub Actions to GitLab CI 2026-05-05 21:07:25 +02:00
dependabot[bot] 82e16e2598 chore(deps-dev): bump the development-dependencies group with 4 updates (#164)
Bumps the development-dependencies group with 4 updates: [@angular/build](https://github.com/angular/angular-cli), [@angular/cli](https://github.com/angular/angular-cli), [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@angular/build` from 21.2.7 to 21.2.8
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v21.2.8/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v21.2.7...v21.2.8)

Updates `@angular/cli` from 21.2.7 to 21.2.8
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v21.2.8/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v21.2.7...v21.2.8)

Updates `@angular/compiler-cli` from 21.2.9 to 21.2.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/v21.2.10/packages/compiler-cli)

Updates `typescript-eslint` from 8.59.0 to 8.59.1
- [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.59.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.59.1
  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>
2026-04-27 22:12:11 +00:00
dependabot[bot] 58c13aa86c chore(deps): bump the angular group with 11 updates (#163)
Bumps the angular group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.9` | `21.2.10` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.7` | `21.2.8` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.9` | `21.2.10` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.9` | `21.2.10` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.9` | `21.2.10` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.9` | `21.2.10` |
| [@angular/material](https://github.com/angular/components) | `21.2.7` | `21.2.8` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.9` | `21.2.10` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.9` | `21.2.10` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.9` | `21.2.10` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.7` | `21.2.8` |


Updates `@angular/animations` from 21.2.9 to 21.2.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/v21.2.10/packages/animations)

Updates `@angular/cdk` from 21.2.7 to 21.2.8
- [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/v21.2.7...v21.2.8)

Updates `@angular/common` from 21.2.9 to 21.2.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/v21.2.10/packages/common)

Updates `@angular/compiler` from 21.2.9 to 21.2.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/v21.2.10/packages/compiler)

Updates `@angular/core` from 21.2.9 to 21.2.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/v21.2.10/packages/core)

Updates `@angular/forms` from 21.2.9 to 21.2.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/v21.2.10/packages/forms)

Updates `@angular/material` from 21.2.7 to 21.2.8
- [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/v21.2.7...v21.2.8)

Updates `@angular/platform-browser` from 21.2.9 to 21.2.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/v21.2.10/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.9 to 21.2.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/v21.2.10/packages/platform-server)

Updates `@angular/router` from 21.2.9 to 21.2.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/v21.2.10/packages/router)

Updates `@angular/ssr` from 21.2.7 to 21.2.8
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v21.2.8/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v21.2.7...v21.2.8)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.8
  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>
2026-04-27 22:11:19 +00:00
dependabot[bot] 52282705b3 chore(deps-dev): bump the development-dependencies group with 4 updates (#161)
Bumps the development-dependencies group with 4 updates: [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli), [eslint](https://github.com/eslint/eslint), [prettier](https://github.com/prettier/prettier) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@angular/compiler-cli` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/compiler-cli)

Updates `eslint` from 10.2.0 to 10.2.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1)

Updates `prettier` from 3.8.2 to 3.8.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.2...3.8.3)

Updates `typescript-eslint` from 8.58.2 to 8.59.0
- [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.59.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.59.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 20:37:22 +00:00
dependabot[bot] 3e5c1c4588 chore(deps): bump the angular group with 9 updates (#160)
Bumps the angular group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.8` | `21.2.9` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.6` | `21.2.7` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.8` | `21.2.9` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.8` | `21.2.9` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.8` | `21.2.9` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.8` | `21.2.9` |
| [@angular/material](https://github.com/angular/components) | `21.2.6` | `21.2.7` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.8` | `21.2.9` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.8` | `21.2.9` |


Updates `@angular/animations` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/animations)

Updates `@angular/cdk` from 21.2.6 to 21.2.7
- [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/v21.2.6...v21.2.7)

Updates `@angular/common` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/common)

Updates `@angular/compiler` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/compiler)

Updates `@angular/core` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/core)

Updates `@angular/forms` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/forms)

Updates `@angular/material` from 21.2.6 to 21.2.7
- [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/v21.2.6...v21.2.7)

Updates `@angular/platform-browser` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/platform-browser)

Updates `@angular/router` from 21.2.8 to 21.2.9
- [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/v21.2.9/packages/router)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.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>
2026-04-20 20:36:36 +00:00
dependabot[bot] 43083c1927 chore(deps): bump @angular/platform-server from 21.2.8 to 21.2.9 (#159)
Bumps [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) from 21.2.8 to 21.2.9.
- [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/v21.2.9/packages/platform-server)

---
updated-dependencies:
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 00:21:58 +00:00
dependabot[bot] 6f897b263a chore(deps-dev): bump hono from 4.12.12 to 4.12.14 (#158)
Bumps [hono](https://github.com/honojs/hono) from 4.12.12 to 4.12.14.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.12...v4.12.14)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 02:44:45 +00:00
dependabot[bot] a580cdf571 chore(deps): bump follow-redirects from 1.15.11 to 1.16.0 (#157)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 21:18:56 +00:00
dependabot[bot] 398964b8ab Bump date-holidays from 3.26.12 to 3.27.0 (#156)
Bumps [date-holidays](https://github.com/commenthol/date-holidays) from 3.26.12 to 3.27.0.
- [Changelog](https://github.com/commenthol/date-holidays/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commenthol/date-holidays/compare/v3.26.12...v3.27.0)

---
updated-dependencies:
- dependency-name: date-holidays
  dependency-version: 3.27.0
  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>
2026-04-13 19:31:44 +00:00
dependabot[bot] 7a16c1f716 Bump the development-dependencies group with 8 updates (#155)
Bumps the development-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.6` | `21.2.7` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.6` | `21.2.7` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.7` | `21.2.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `25.6.0` |
| [jasmine-core](https://github.com/jasmine/jasmine) | `6.1.0` | `6.2.0` |
| [jest-preset-angular](https://github.com/thymikee/jest-preset-angular) | `16.1.2` | `16.1.4` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.0` | `8.58.2` |


Updates `@angular/build` from 21.2.6 to 21.2.7
- [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/v21.2.6...v21.2.7)

Updates `@angular/cli` from 21.2.6 to 21.2.7
- [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/v21.2.6...v21.2.7)

Updates `@angular/compiler-cli` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/compiler-cli)

Updates `@types/node` from 25.5.2 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `jasmine-core` from 6.1.0 to 6.2.0
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v6.1.0...v6.2.0)

Updates `jest-preset-angular` from 16.1.2 to 16.1.4
- [Release notes](https://github.com/thymikee/jest-preset-angular/releases)
- [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/thymikee/jest-preset-angular/compare/v16.1.2...v16.1.4)

Updates `prettier` from 3.8.1 to 3.8.2
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2)

Updates `typescript-eslint` from 8.58.0 to 8.58.2
- [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.58.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: jasmine-core
  dependency-version: 6.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: jest-preset-angular
  dependency-version: 16.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  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>
2026-04-13 19:30:16 +00:00
dependabot[bot] 322b11c9bd Bump the angular group with 11 updates (#154)
Bumps the angular group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.7` | `21.2.8` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.5` | `21.2.6` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.7` | `21.2.8` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.7` | `21.2.8` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.7` | `21.2.8` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.7` | `21.2.8` |
| [@angular/material](https://github.com/angular/components) | `21.2.5` | `21.2.6` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.7` | `21.2.8` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.7` | `21.2.8` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.7` | `21.2.8` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.6` | `21.2.7` |


Updates `@angular/animations` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/animations)

Updates `@angular/cdk` from 21.2.5 to 21.2.6
- [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/v21.2.5...v21.2.6)

Updates `@angular/common` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/common)

Updates `@angular/compiler` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/compiler)

Updates `@angular/core` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/core)

Updates `@angular/forms` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/forms)

Updates `@angular/material` from 21.2.5 to 21.2.6
- [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/v21.2.5...v21.2.6)

Updates `@angular/platform-browser` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/platform-server)

Updates `@angular/router` from 21.2.7 to 21.2.8
- [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/v21.2.8/packages/router)

Updates `@angular/ssr` from 21.2.6 to 21.2.7
- [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/v21.2.6...v21.2.7)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.7
  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>
2026-04-13 19:29:17 +00:00
dependabot[bot] b162a8512a Bump hono from 4.12.7 to 4.12.12 (#153)
Bumps [hono](https://github.com/honojs/hono) from 4.12.7 to 4.12.12.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.7...v4.12.12)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 05:23:44 +00:00
dependabot[bot] afc51e60de Bump @hono/node-server from 1.19.10 to 1.19.13 (#152)
Bumps [@hono/node-server](https://github.com/honojs/node-server) from 1.19.10 to 1.19.13.
- [Release notes](https://github.com/honojs/node-server/releases)
- [Commits](https://github.com/honojs/node-server/compare/v1.19.10...v1.19.13)

---
updated-dependencies:
- dependency-name: "@hono/node-server"
  dependency-version: 1.19.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 03:14:38 +00:00
dependabot[bot] 0ad7aea819 Bump lodash from 4.17.23 to 4.18.1 (#151)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 18:27:38 +00:00
dependabot[bot] 789ff38b8c Bump the development-dependencies group with 5 updates (#150)
Bumps the development-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.5` | `21.2.6` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.5` | `21.2.6` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.6` | `21.2.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.0` | `25.5.2` |
| [eslint](https://github.com/eslint/eslint) | `10.1.0` | `10.2.0` |


Updates `@angular/build` from 21.2.5 to 21.2.6
- [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/v21.2.5...v21.2.6)

Updates `@angular/cli` from 21.2.5 to 21.2.6
- [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/v21.2.5...v21.2.6)

Updates `@angular/compiler-cli` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/compiler-cli)

Updates `@types/node` from 25.5.0 to 25.5.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.1.0...v10.2.0)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 18:20:37 +00:00
dependabot[bot] e29d54e3de Bump the angular group with 11 updates (#149)
Bumps the angular group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.6` | `21.2.7` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.4` | `21.2.5` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.6` | `21.2.7` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.6` | `21.2.7` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.6` | `21.2.7` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.6` | `21.2.7` |
| [@angular/material](https://github.com/angular/components) | `21.2.4` | `21.2.5` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.6` | `21.2.7` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.6` | `21.2.7` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.6` | `21.2.7` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.5` | `21.2.6` |


Updates `@angular/animations` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/animations)

Updates `@angular/cdk` from 21.2.4 to 21.2.5
- [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/v21.2.4...v21.2.5)

Updates `@angular/common` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/common)

Updates `@angular/compiler` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/compiler)

Updates `@angular/core` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/core)

Updates `@angular/forms` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/forms)

Updates `@angular/material` from 21.2.4 to 21.2.5
- [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/v21.2.4...v21.2.5)

Updates `@angular/platform-browser` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/platform-server)

Updates `@angular/router` from 21.2.6 to 21.2.7
- [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/v21.2.7/packages/router)

Updates `@angular/ssr` from 21.2.5 to 21.2.6
- [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/v21.2.5...v21.2.6)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.6
  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>
2026-04-06 18:19:39 +00:00
dependabot[bot] a873c6b6f9 Bump date-holidays from 3.26.11 to 3.26.12 (#148)
Bumps [date-holidays](https://github.com/commenthol/date-holidays) from 3.26.11 to 3.26.12.
- [Changelog](https://github.com/commenthol/date-holidays/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commenthol/date-holidays/compare/v3.26.11...v3.26.12)

---
updated-dependencies:
- dependency-name: date-holidays
  dependency-version: 3.26.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 19:43:15 +00:00
dependabot[bot] 7c05fb1944 Bump the development-dependencies group with 5 updates (#147)
Bumps the development-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.3` | `21.2.5` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.3` | `21.2.5` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.5` | `21.2.6` |
| [jest-preset-angular](https://github.com/thymikee/jest-preset-angular) | `16.1.1` | `16.1.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.2` | `8.58.0` |


Updates `@angular/build` from 21.2.3 to 21.2.5
- [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/v21.2.3...v21.2.5)

Updates `@angular/cli` from 21.2.3 to 21.2.5
- [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/v21.2.3...v21.2.5)

Updates `@angular/compiler-cli` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/compiler-cli)

Updates `jest-preset-angular` from 16.1.1 to 16.1.2
- [Release notes](https://github.com/thymikee/jest-preset-angular/releases)
- [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/thymikee/jest-preset-angular/compare/v16.1.1...v16.1.2)

Updates `typescript-eslint` from 8.57.2 to 8.58.0
- [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.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: jest-preset-angular
  dependency-version: 16.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 19:42:11 +00:00
dependabot[bot] 0769892aba Bump the angular group with 11 updates (#146)
Bumps the angular group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.5` | `21.2.6` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.3` | `21.2.4` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.5` | `21.2.6` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.5` | `21.2.6` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.5` | `21.2.6` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.5` | `21.2.6` |
| [@angular/material](https://github.com/angular/components) | `21.2.3` | `21.2.4` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.5` | `21.2.6` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.5` | `21.2.6` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.5` | `21.2.6` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.3` | `21.2.5` |


Updates `@angular/animations` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/animations)

Updates `@angular/cdk` from 21.2.3 to 21.2.4
- [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/v21.2.3...v21.2.4)

Updates `@angular/common` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/common)

Updates `@angular/compiler` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/compiler)

Updates `@angular/core` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/core)

Updates `@angular/forms` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/forms)

Updates `@angular/material` from 21.2.3 to 21.2.4
- [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/v21.2.3...v21.2.4)

Updates `@angular/platform-browser` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/platform-server)

Updates `@angular/router` from 21.2.5 to 21.2.6
- [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/v21.2.6/packages/router)

Updates `@angular/ssr` from 21.2.3 to 21.2.5
- [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/v21.2.3...v21.2.5)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.5
  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>
2026-03-30 19:41:14 +00:00
dependabot[bot] 5019b7bba8 Bump picomatch from 2.3.1 to 2.3.2 (#145)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-28 20:32:05 +00:00
dependabot[bot] 3c394a1823 Bump path-to-regexp from 8.3.0 to 8.4.0 (#144)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 8.3.0 to 8.4.0.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v8.3.0...v8.4.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-version: 8.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-28 20:31:54 +00:00
dependabot[bot] 0fa7368a92 Bump handlebars from 4.7.8 to 4.7.9 (#143)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 03:31:04 +00:00
CopilotandTHoguet cb5f440d02 Fix Docker publish not triggering after GITHUB_TOKEN-merged PRs (#142)
* Initial plan

* Fix Docker publish not running after GITHUB_TOKEN-merged PRs

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
Agent-Logs-Url: https://github.com/THoguet/vac-optimizer/sessions/6d2692bd-36b2-4000-ade6-8136fa4bbd26

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2026-03-23 23:51:02 +01:00
dependabot[bot] 257ccab43d Bump the development-dependencies group with 5 updates (#140)
Bumps the development-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.2` | `21.2.3` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.2` | `21.2.3` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.4` | `21.2.5` |
| [eslint](https://github.com/eslint/eslint) | `10.0.3` | `10.1.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.1` | `8.57.2` |


Updates `@angular/build` from 21.2.2 to 21.2.3
- [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/v21.2.2...v21.2.3)

Updates `@angular/cli` from 21.2.2 to 21.2.3
- [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/v21.2.2...v21.2.3)

Updates `@angular/compiler-cli` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/compiler-cli)

Updates `eslint` from 10.0.3 to 10.1.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.3...v10.1.0)

Updates `typescript-eslint` from 8.57.1 to 8.57.2
- [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.57.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.2
  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>
2026-03-23 18:39:07 +00:00
dependabot[bot] 59585328b2 Bump the angular group with 10 updates (#139)
Bumps the angular group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.4` | `21.2.5` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.2` | `21.2.3` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.4` | `21.2.5` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.4` | `21.2.5` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.4` | `21.2.5` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.4` | `21.2.5` |
| [@angular/material](https://github.com/angular/components) | `21.2.2` | `21.2.3` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.4` | `21.2.5` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.4` | `21.2.5` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.4` | `21.2.5` |


Updates `@angular/animations` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/animations)

Updates `@angular/cdk` from 21.2.2 to 21.2.3
- [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/v21.2.2...v21.2.3)

Updates `@angular/common` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/common)

Updates `@angular/compiler` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/compiler)

Updates `@angular/core` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/core)

Updates `@angular/forms` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/forms)

Updates `@angular/material` from 21.2.2 to 21.2.3
- [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/v21.2.2...v21.2.3)

Updates `@angular/platform-browser` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/platform-server)

Updates `@angular/router` from 21.2.4 to 21.2.5
- [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/v21.2.5/packages/router)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.5
  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>
2026-03-23 18:38:06 +00:00
dependabot[bot] 180a9540d8 Bump flatted from 3.3.3 to 3.4.2 (#138)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 21:27:12 +00:00
dependabot[bot] 1471f0cc24 Bump @angular/ssr from 21.2.2 to 21.2.3 (#137)
Bumps [@angular/ssr](https://github.com/angular/angular-cli) from 21.2.2 to 21.2.3.
- [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/v21.2.2...v21.2.3)

---
updated-dependencies:
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 21:25:41 +00:00
dependabot[bot] e69572c847 Bump date-holidays from 3.26.9 to 3.26.11 (#135)
Bumps [date-holidays](https://github.com/commenthol/date-holidays) from 3.26.9 to 3.26.11.
- [Changelog](https://github.com/commenthol/date-holidays/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commenthol/date-holidays/compare/v3.26.9...v3.26.11)

---
updated-dependencies:
- dependency-name: date-holidays
  dependency-version: 3.26.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 18:56:50 +00:00
dependabot[bot] 608874a11e Bump dayjs from 1.11.19 to 1.11.20 (#136)
Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.19 to 1.11.20.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.19...v1.11.20)

---
updated-dependencies:
- dependency-name: dayjs
  dependency-version: 1.11.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 18:56:06 +00:00
dependabot[bot] 3b1a95c98a Bump the development-dependencies group with 6 updates (#134)
Bumps the development-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.1` | `21.2.2` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.1` | `21.2.2` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.2` | `21.2.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.4.0` | `25.5.0` |
| [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `30.2.0` | `30.3.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.0` | `8.57.1` |


Updates `@angular/build` from 21.2.1 to 21.2.2
- [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/v21.2.1...v21.2.2)

Updates `@angular/cli` from 21.2.1 to 21.2.2
- [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/v21.2.1...v21.2.2)

Updates `@angular/compiler-cli` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/compiler-cli)

Updates `@types/node` from 25.4.0 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `jest` from 30.2.0 to 30.3.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.3.0/packages/jest)

Updates `typescript-eslint` from 8.57.0 to 8.57.1
- [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.57.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: jest
  dependency-version: 30.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  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>
2026-03-16 18:55:45 +00:00
dependabot[bot] 99fc952dd1 Bump the angular group with 10 updates (#133)
Bumps the angular group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.2` | `21.2.4` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.1` | `21.2.2` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.2` | `21.2.4` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.2` | `21.2.4` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.2` | `21.2.4` |
| [@angular/material](https://github.com/angular/components) | `21.2.1` | `21.2.2` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.2` | `21.2.4` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.2` | `21.2.4` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.2` | `21.2.4` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.1` | `21.2.2` |


Updates `@angular/animations` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/animations)

Updates `@angular/cdk` from 21.2.1 to 21.2.2
- [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/v21.2.1...v21.2.2)

Updates `@angular/common` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/common)

Updates `@angular/core` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/core)

Updates `@angular/forms` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/forms)

Updates `@angular/material` from 21.2.1 to 21.2.2
- [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/v21.2.1...v21.2.2)

Updates `@angular/platform-browser` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/platform-server)

Updates `@angular/router` from 21.2.2 to 21.2.4
- [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/v21.2.4/packages/router)

Updates `@angular/ssr` from 21.2.1 to 21.2.2
- [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/v21.2.1...v21.2.2)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.2
  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>
2026-03-16 18:54:40 +00:00
dependabot[bot] 7db0615f20 Bump @angular/compiler from 21.2.2 to 21.2.4 (#130)
Bumps [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) from 21.2.2 to 21.2.4.
- [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/v21.2.4/packages/compiler)

---
updated-dependencies:
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 02:58:24 +00:00
dependabot[bot] ca9477ad9e Bump tar from 7.5.10 to 7.5.11 (#129)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.10 to 7.5.11.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.10...v7.5.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 02:19:21 +00:00
dependabot[bot] 3d9fd8c334 Bump hono from 4.12.5 to 4.12.7 (#128)
Bumps [hono](https://github.com/honojs/hono) from 4.12.5 to 4.12.7.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.5...v4.12.7)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 02:03:41 +00:00
dependabot[bot] 95646daace Bump date-holidays from 3.26.8 to 3.26.9 (#127)
Bumps [date-holidays](https://github.com/commenthol/date-holidays) from 3.26.8 to 3.26.9.
- [Changelog](https://github.com/commenthol/date-holidays/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commenthol/date-holidays/compare/v3.26.8...v3.26.9)

---
updated-dependencies:
- dependency-name: date-holidays
  dependency-version: 3.26.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 19:53:10 +00:00
dependabot[bot] bacab0af5b Bump the development-dependencies group with 6 updates (#126)
Bumps the development-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.0` | `21.2.1` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.0` | `21.2.1` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.0` | `21.2.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.3` | `25.4.0` |
| [eslint](https://github.com/eslint/eslint) | `10.0.2` | `10.0.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.56.1` | `8.57.0` |


Updates `@angular/build` from 21.2.0 to 21.2.1
- [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/v21.2.0...v21.2.1)

Updates `@angular/cli` from 21.2.0 to 21.2.1
- [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/v21.2.0...v21.2.1)

Updates `@angular/compiler-cli` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/compiler-cli)

Updates `@types/node` from 25.3.3 to 25.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.0.2 to 10.0.3
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.2...v10.0.3)

Updates `typescript-eslint` from 8.56.1 to 8.57.0
- [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.57.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 19:51:26 +00:00
dependabot[bot] 4dd56650d8 Bump the angular group with 11 updates (#125)
Bumps the angular group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.0` | `21.2.2` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.0` | `21.2.1` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.0` | `21.2.2` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.0` | `21.2.2` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.0` | `21.2.2` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.0` | `21.2.2` |
| [@angular/material](https://github.com/angular/components) | `21.2.0` | `21.2.1` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.0` | `21.2.2` |
| [@angular/platform-server](https://github.com/angular/angular/tree/HEAD/packages/platform-server) | `21.2.0` | `21.2.2` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.0` | `21.2.2` |
| [@angular/ssr](https://github.com/angular/angular-cli) | `21.2.0` | `21.2.1` |


Updates `@angular/animations` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/animations)

Updates `@angular/cdk` from 21.2.0 to 21.2.1
- [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/v21.2.0...v21.2.1)

Updates `@angular/common` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/common)

Updates `@angular/compiler` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/compiler)

Updates `@angular/core` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/core)

Updates `@angular/forms` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/forms)

Updates `@angular/material` from 21.2.0 to 21.2.1
- [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/v21.2.0...v21.2.1)

Updates `@angular/platform-browser` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/platform-browser)

Updates `@angular/platform-server` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/platform-server)

Updates `@angular/router` from 21.2.0 to 21.2.2
- [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/v21.2.2/packages/router)

Updates `@angular/ssr` from 21.2.0 to 21.2.1
- [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/v21.2.0...v21.2.1)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.1
  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>
2026-03-09 19:50:16 +00:00
dependabot[bot] 3302bc77a2 Bump aquasecurity/trivy-action from 0.34.2 to 0.35.0 (#124)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.34.2 to 0.35.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.2...0.35.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.35.0
  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>
2026-03-09 19:48:54 +00:00
61 changed files with 6567 additions and 15619 deletions

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
---
trigger: always_on
---
# Agent Rules and Verification Constraints
All AI agents (including Cline, Antigravity, Cursor, and Copilot) must adhere to the following rules before declaring a task complete:
1. **Test Coverage (>= 80%)**:
- The overall project code coverage (Statements, Lines, and Functions) must be maintained at **80% or higher**.
- Verify coverage by running:
```bash
bun run test --watch=false --coverage
```
- If your changes introduce new code or modify existing code in a way that drops the coverage below 80%, you must write appropriate unit tests in `.spec.ts` files to bring it back up.
2. **Code Formatting (Prettier)**:
- All code files must be correctly formatted according to the project's Prettier config before finishing.
- Run the formatter with:
```bash
bun run format
```
3. **Successful Build**:
- The project must always build successfully for production without compilation or bundling errors before ending.
- Verify the build with:
```bash
bun run build
```
4. **GitLab Pipeline Checks**:
- You must execute the local equivalent of the GitLab CI/CD pipeline steps before completing your task. All of the following commands must pass:
- **Linting check**: `bun run lint` (or use `bun run lint:fix` to resolve auto-fixable issues)
- **Formatting check**: `bun run format:check`
- **TypeScript Type check**: `bunx tsc --noEmit`
- **Unit Tests and Coverage**: `bun run test --watch=false --coverage`
- **Production Build**: `bun run build -- --configuration production`
+33
View File
@@ -0,0 +1,33 @@
# Agent Rules and Verification Constraints
All AI agents (including Cline, Antigravity, Cursor, and Copilot) must adhere to the following rules before declaring a task complete:
1. **Test Coverage (>= 80%)**:
- The overall project code coverage (Statements, Lines, and Functions) must be maintained at **80% or higher**.
- Verify coverage by running:
```bash
bun run test --watch=false --coverage
```
- If your changes introduce new code or modify existing code in a way that drops the coverage below 80%, you must write appropriate unit tests in `.spec.ts` files to bring it back up.
2. **Code Formatting (Prettier)**:
- All code files must be correctly formatted according to the project's Prettier config before finishing.
- Run the formatter with:
```bash
bun run format
```
3. **Successful Build**:
- The project must always build successfully for production without compilation or bundling errors before ending.
- Verify the build with:
```bash
bun run build
```
4. **GitLab Pipeline Checks**:
- You must execute the local equivalent of the GitLab CI/CD pipeline steps before completing your task. All of the following commands must pass:
- **Linting check**: `bun run lint` (or use `bun run lint:fix` to resolve auto-fixable issues)
- **Formatting check**: `bun run format:check`
- **TypeScript Type check**: `bunx tsc --noEmit`
- **Unit Tests and Coverage**: `bun run test --watch=false --coverage`
- **Production Build**: `bun run build -- --configuration production`
-47
View File
@@ -1,47 +0,0 @@
You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.
## TypeScript Best Practices
- Use strict type checking
- Prefer type inference when the type is obvious
- Avoid the `any` type; use `unknown` when type is uncertain
## Angular Best Practices
- Always use standalone components over NgModules
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
- Use signals for state management
- Implement lazy loading for feature routes
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
- Use `NgOptimizedImage` for all static images.
- `NgOptimizedImage` does not work for inline base64 images.
## Components
- Keep components small and focused on a single responsibility
- Use `input()` and `output()` functions instead of decorators
- Use `computed()` for derived state
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
- Prefer inline templates for small components
- Prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead
- Do NOT use `ngStyle`, use `style` bindings instead
## State Management
- Use signals for local component state
- Use `computed()` for derived state
- Keep state transformations pure and predictable
- Do NOT use `mutate` on signals, use `update` or `set` instead
## Templates
- Keep templates simple and avoid complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables
## Services
- Design services around a single responsibility
- Use the `providedIn: 'root'` option for singleton services
- Use the `inject()` function instead of constructor injection
-34
View File
@@ -1,34 +0,0 @@
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 10
groups:
angular:
patterns:
- '@angular/*'
development-dependencies:
dependency-type: 'development'
update-types:
- 'minor'
- 'patch'
# Enable version updates for GitHub Actions
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 5
# Enable version updates for Docker
- package-ecosystem: 'docker'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 5
-41
View File
@@ -1,41 +0,0 @@
name: CodeQL Security Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 1' # Run every Monday at midnight UTC
jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: ['javascript-typescript']
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'
@@ -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 }}
-65
View File
@@ -1,65 +0,0 @@
name: Docker Build and Push
on:
push:
branches: [main]
release:
types: [published]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.34.2
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@v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
-105
View File
@@ -1,105 +0,0 @@
name: PR Validation
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Check code formatting with Prettier
run: npx prettier --check "src/**/*.{ts,html,scss,css,json}"
- name: Run ESLint
run: npx eslint "src/**/*.ts"
type-check:
name: TypeScript Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run TypeScript compiler
run: npx tsc --noEmit
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run unit tests
run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v5
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/vac-optimizer/lcov.info
fail_ci_if_error: false
build:
name: Build Application
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build production bundle
run: npm run build -- --configuration production
env:
NG_BUILD_CACHE: '.angular/cache'
- name: Check bundle size
run: |
du -sh dist/vac-optimizer/browser
echo "Build completed successfully"
- name: Upload build artifacts
uses: actions/upload-artifact@v6
with:
name: build-artifacts
path: dist/
retention-days: 7
+3
View File
@@ -41,3 +41,6 @@ __screenshots__/
# System files
.DS_Store
Thumbs.db
# Auto-generated i18n files
/src/app/i18n-registry.ts
+109
View File
@@ -0,0 +1,109 @@
image: oven/bun:latest
stages:
- validate
- test
- build
- security
- docker
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ''
cache:
key:
files:
- bun.lock
paths:
- .bun/
.node_setup:
before_script:
- bun install --frozen-lockfile
# -- Validate Stage --
lint:
extends: .node_setup
stage: validate
script:
- bunx eslint "src/**/*.ts"
prettier:
extends: .node_setup
stage: validate
script:
- bunx prettier --check "src/**/*.{ts,html,scss,css,json}"
type-check:
extends: .node_setup
stage: validate
script:
- bunx tsc --noEmit
# -- Test Stage --
test:
extends: .node_setup
stage: test
script:
- bun run test --watch=false --coverage
artifacts:
when: always
paths:
- coverage/
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 7 days
# -- Build Stage --
build:
image: oven/bun:latest
extends: .node_setup
stage: build
script:
- apt-get update && apt-get install -y nodejs
- bun run build -- --configuration production
artifacts:
paths:
- dist/
expire_in: 7 days
# -- Security Stage --
include:
- template: Security/SAST.gitlab-ci.yml
trivy_scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: ['']
before_script: [] # override global before_script
script:
# Trivy will scan the project directory
- trivy fs --format sarif --output trivy-results.sarif .
artifacts:
reports:
sast: trivy-results.sarif
expire_in: 7 days
# -- Docker Stage --
docker-build:
stage: docker
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" -t "$CI_REGISTRY_IMAGE:latest" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
- docker push "$CI_REGISTRY_IMAGE:latest"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+12 -9
View File
@@ -1,32 +1,35 @@
# Stage 1: Build the Angular application
FROM node:25-alpine AS builder
FROM oven/bun:alpine AS builder
# Install nodejs to support Angular prerendering worker threads
RUN apk add --no-cache nodejs
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY package.json bun.lock* ./
# Install dependencies
RUN npm ci --only=production=false
RUN bun install
# Copy source code
COPY . .
# Build the application for production
RUN npm run build
RUN bun run build
# Stage 2: Production image
FROM node:25-alpine AS production
FROM oven/bun:alpine AS production
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY package.json bun.lock* ./
# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force
RUN bun install --production
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
@@ -39,7 +42,7 @@ EXPOSE 4000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:4000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD bun -e "require('http').get('http://localhost:4000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start the application
CMD ["node", "dist/vac-optimizer/server/server.mjs"]
CMD ["bun", "run", "dist/vac-optimizer/server/server.mjs"]
+8 -8
View File
@@ -7,7 +7,7 @@ This project was generated using [Angular CLI](https://github.com/angular/angula
To start a local development server, run:
```bash
ng serve
bun start
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
@@ -21,13 +21,13 @@ This project uses ESLint and Prettier to maintain code quality and consistency.
Check for linting errors:
```bash
npm run lint
bun run lint
```
Auto-fix linting issues:
```bash
npm run lint:fix
bun run lint:fix
```
### Formatting
@@ -35,13 +35,13 @@ npm run lint:fix
Check code formatting:
```bash
npm run format:check
bun run format:check
```
Format code:
```bash
npm run format
bun run format
```
## Code scaffolding
@@ -63,17 +63,17 @@ ng generate --help
To build the project run:
```bash
ng build
bun run build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
To execute unit tests, use the following command:
```bash
ng test
bun run test
```
## Running end-to-end tests
+10 -15
View File
@@ -26,16 +26,22 @@
"input": "public"
}
],
"styles": ["src/styles.scss"],
"styles": [
"src/styles.scss"
],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
},
"prerender": false,
"allowedCommonJsDependencies": [
"deepmerge",
"moment-timezone",
"jalaali-js"
"jalaali-js",
"dayjs",
"dayjs/plugin/weekOfYear",
"dayjs/plugin/isoWeek"
]
},
"configurations": {
@@ -43,7 +49,7 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "2MB",
"maximumWarning": "2.5MB",
"maximumError": "3MB"
},
{
@@ -81,18 +87,7 @@
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.scss"]
}
"builder": "@analogjs/vitest-angular:test"
}
}
}
+3234
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -15,7 +15,7 @@ services:
test:
[
'CMD',
'node',
'bun',
'-e',
"require('http').get('http://localhost:4000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})",
]
+1 -1
View File
@@ -15,7 +15,7 @@ services:
test:
[
'CMD',
'node',
'bun',
'-e',
"require('http').get('http://localhost:4000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})",
]
-14856
View File
File diff suppressed because it is too large. Load diff
+46 -40
View File
@@ -3,15 +3,15 @@
"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}\""
"start": "bun scripts/generate-langs.js && ng serve",
"build": "bun scripts/generate-langs.js && ng build",
"watch": "bun scripts/generate-langs.js && ng build --watch --configuration development",
"test": "bun scripts/generate-langs.js && ng test",
"serve:ssr:vac-optimizer": "bun dist/vac-optimizer/server/server.mjs",
"lint": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\"",
"lint:fix": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\" --fix",
"format": "bun scripts/generate-langs.js && prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
"format:check": "bun scripts/generate-langs.js && prettier --check \"src/**/*.{ts,html,scss,css,json}\""
},
"prettier": {
"printWidth": 100,
@@ -27,45 +27,51 @@
},
"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",
"@angular/animations": "21.2.15",
"@angular/cdk": "^21.2.13",
"@angular/common": "21.2.15",
"@angular/compiler": "21.2.15",
"@angular/core": "21.2.15",
"@angular/forms": "21.2.15",
"@angular/material": "^21.2.13",
"@angular/platform-browser": "21.2.15",
"@angular/platform-browser-dynamic": "21.2.15",
"@angular/platform-server": "21.2.15",
"@angular/router": "21.2.15",
"@angular/ssr": "21.2.13",
"date-holidays": "^3.30.2",
"dayjs": "^1.11.21",
"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",
"@analogjs/platform": "^2.5.2",
"@analogjs/vite-plugin-angular": "^2.5.2",
"@analogjs/vitest-angular": "^2.5.2",
"@angular/build": "21.2.13",
"@angular/cli": "21.2.13",
"@angular/compiler-cli": "21.2.15",
"@eslint/js": "^10.0.1",
"@nx/devkit": "^22.7.5",
"@nx/vite": "^22.0.0",
"@types/express": "^5.0.6",
"@types/jasmine": "~6.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^25.3.3",
"eslint": "^10.0.2",
"@types/node": "^25.9.1",
"@vitest/coverage-istanbul": "^4.1.7",
"@vitest/coverage-v8": "^4.0.0",
"@vitest/ui": "^4.0.0",
"eslint": "^10.4.1",
"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"
"eslint-plugin-prettier": "^5.5.6",
"jest": "^30.4.2",
"jest-preset-angular": "^16.1.5",
"jsdom": "^22.0.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.60.0",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^4.0.0"
}
}
+52
View File
@@ -0,0 +1,52 @@
import "@angular/compiler";
import { CalendarService, SelectedDates, DayType, SelectableDayType } from './src/app/services/calendar-service';
import { DateCacheService } from './src/app/services/date-cache.service';
import { RuleService } from './src/app/services/rule-service';
const cs = new CalendarService();
cs['dateCache'] = {
getTodayComponents: () => {
const d = new Date();
return { date: d.getDate(), month: d.getMonth(), year: d.getFullYear() };
}
} as any;
const sd = cs.getSelectedDatesForPeriod(new Date(), new Date(new Date().setFullYear(new Date().getFullYear() + 1)));
console.log("Selected dates length:", sd._datesSelected.length);
const rules = [
{
id: 'default-samedi-malin',
name: 'Samedi Malin',
type: 'if_then',
enabled: true,
dayOfWeek: 6,
placeDays: 2,
placeType: 'cp',
placement: 'auto',
rewardDays: 1,
rewardType: 'other'
},
{
id: 'default-christmas',
name: 'Vacances de Noël',
type: 'time_window',
enabled: true,
startDate: '2026-12-19T23:00:00.000Z',
endDate: '2027-01-02T23:00:00.000Z',
placeDays: 5,
placeType: 'any'
}
] as any;
const vacs = [
{ id: '1', type: 'cp', numberOfDays: 5, expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)) },
{ id: '2', type: 'rtt', numberOfDays: 5, expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)) },
];
sd.optimizeVacations(vacs, rules, cs, new Date(), new Date(new Date().setFullYear(new Date().getFullYear() + 1))).then(res => {
console.log("Remaining:", res);
const vacPlaced = sd._datesSelected.filter(d => ['cp', 'rtt', 'other'].includes(d.type as string));
console.log("Placed vacs:", vacPlaced.length);
});
+33
View File
@@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
const i18nDir = path.join(__dirname, '../src/i18n');
const registryFile = path.join(__dirname, '../src/app/i18n-registry.ts');
if (!fs.existsSync(i18nDir)) {
console.error(`i18n directory not found at ${i18nDir}`);
process.exit(1);
}
const files = fs.readdirSync(i18nDir).filter((file) => file.endsWith('.json'));
let importStatements = '';
let registryEntries = '';
files.forEach((file) => {
const langCode = path.basename(file, '.json');
importStatements += `import ${langCode} from '../i18n/${file}';\n`;
registryEntries += `\t${langCode}: ${langCode} as AppTranslations,\n`;
});
const content = `// This file is auto-generated by scripts/generate-langs.js. Do not edit directly.
import { AppTranslations } from './services/translation-service';
${importStatements}
export const translationsMap: Record<string, AppTranslations> = {
${registryEntries}};
export const availableLanguagesList: string[] = Object.keys(translationsMap);
`;
fs.writeFileSync(registryFile, content, 'utf8');
console.log(`Successfully generated i18n registry at ${registryFile} with ${files.length} languages.`);
+41 -13
View File
@@ -9,14 +9,52 @@
.content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
align-items: flex-start;
justify-content: space-between;
width: 100%;
padding: 0 24px;
box-sizing: border-box;
}
@media (max-width: 768px) {
app-user-input {
display: block;
flex: 3;
min-width: 350px;
margin-right: 16px;
}
app-calendar-year {
display: block;
flex: 7;
min-width: 700px;
margin-left: 16px;
}
@media (max-width: 1024px) {
.content {
flex-direction: column;
align-items: stretch;
padding: 0 16px;
}
app-user-input,
app-calendar-year {
flex: none;
width: 100%;
min-width: 0;
margin: 0;
}
app-user-input {
margin-bottom: 24px;
}
.mat-divider-vertical {
display: none;
}
.mat-divider-horizontal {
display: block !important;
}
}
@@ -27,16 +65,6 @@
z-index: 1000;
}
@media (max-width: 768px) {
.mat-divider-vertical {
display: none;
}
.mat-divider-horizontal {
display: block !important;
}
}
.mat-divider-vertical {
height: 80vh;
width: 1px;
@@ -5,13 +5,9 @@
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p class="day empty week-number"></p>
}
<p class="day">L</p>
<p class="day">M</p>
<p class="day">M</p>
<p class="day">J</p>
<p class="day">V</p>
<p class="day">S</p>
<p class="day">D</p>
@for (letter of getWeekdayLetters(); track $index) {
<p class="day">{{ letter }}</p>
}
</div>
<div class="cols">
@for (week of weeksInAMonth; track $index) {
@@ -36,7 +32,7 @@
[class.selected]="selectedDates.isSelected(day)"
[class]="selectedDates.getClasses(day)"
class="day number"
[matTooltip]="selectedDates.getTooltip(day)"
[matTooltip]="getDayTooltip(day)"
[matTooltipShowDelay]="500"
>
{{ day.getDate() }}
@@ -1,8 +1,9 @@
import { Component, Input, OnInit, inject } from '@angular/core';
import { CalendarService, Month, monthNames, SelectedDates } from '../../services/calendar-service';
import { CalendarService, Month, SelectedDates } from '../../services/calendar-service';
import { MatTooltip } from '@angular/material/tooltip';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-month',
@@ -13,6 +14,7 @@ import { DateCacheService } from '../../services/date-cache.service';
export class CalendarMonth implements OnInit {
protected calendarSettingsService = inject(CalendarSettingsService);
private dateCache = inject(DateCacheService);
protected translationService = inject(TranslationService);
@Input({ required: true })
monthIndex!: number;
@@ -20,6 +22,10 @@ export class CalendarMonth implements OnInit {
@Input({ required: true })
year!: number;
get tr() {
return this.translationService.tr;
}
// Use DateCacheService instead of creating Date objects
getNow(): Date {
return this.dateCache.getNow();
@@ -33,7 +39,24 @@ export class CalendarMonth implements OnInit {
selectedDates!: SelectedDates;
getMonthName(): string {
return monthNames[this.monthIndex];
const locale = this.translationService.locale();
const tempDate = new Date(this.year, this.monthIndex, 1);
const monthName = tempDate.toLocaleString(locale, { month: 'long' });
return monthName.charAt(0).toUpperCase() + monthName.slice(1);
}
getWeekdayLetters(): string[] {
// January 5, 2026 is a Monday
const baseDate = new Date(2026, 0, 5);
const locale = this.translationService.locale();
const letters: string[] = [];
for (let i = 0; i < 7; i++) {
const date = new Date(baseDate);
date.setDate(baseDate.getDate() + i);
const letter = date.toLocaleString(locale, { weekday: 'narrow' });
letters.push(letter.toUpperCase());
}
return letters;
}
getWeekNumber(week: (Date | null)[]): number | null {
@@ -45,6 +68,13 @@ export class CalendarMonth implements OnInit {
return null;
}
getDayTooltip(day: Date): string | null {
const tooltips = this.selectedDates.datesSelected
.filter((dateSelected) => dateSelected.isSelected(day) && dateSelected.type)
.map((dateSelected) => this.translationService.getDayTypeLabel(dateSelected.type!));
return tooltips.length > 0 ? tooltips.join(' + ') : null;
}
ngOnInit(): void {
this.weeksInAMonth = CalendarService.weeksInMonth(this.monthIndex, this.year);
}
@@ -1,4 +1,4 @@
<button matFab extended (click)="openDialog()">
<mat-icon>settings</mat-icon>
Calendar Settings
{{ tr().settings.title }}
</button>
@@ -1,9 +1,10 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { CalendarSettingsDialog } from '../dialog/calendar-settings-dialog/calendar-settings-dialog';
import { MatDividerModule } from '@angular/material/divider';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-settings',
@@ -12,6 +13,12 @@ import { MatDividerModule } from '@angular/material/divider';
styleUrl: './calendar-settings.scss',
})
export class CalendarSettings {
private readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
constructor(private dialog: MatDialog) {}
openDialog() {
@@ -6,16 +6,16 @@
<mat-spinner [mode]="'determinate'" [value]="loadingProgress()"></mat-spinner>
<div class="spinner-text">{{ loadingProgress() }}%</div>
</div>
<p>Calcul en cours...</p>
<p>{{ tr().loading }}</p>
</div>
</div>
}
@for (month of months; track $index) {
@for (item of monthsAndYears(); track $index) {
<mat-card class="card">
<mat-card-content>
<app-calendar-month
[monthIndex]="month"
[year]="getYear(month)"
[monthIndex]="item.month"
[year]="item.year"
[selectedDates]="selectedDates"
></app-calendar-month>
</mat-card-content>
@@ -5,16 +5,11 @@
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
max-width: 65vw;
width: 100%;
max-width: 100%;
position: relative;
}
@media (max-width: 768px) {
.calendar-year {
max-width: 100vw;
}
}
.loading-overlay {
position: absolute;
top: 0;
@@ -1,12 +1,12 @@
import { Component, effect, signal, inject, untracked } from '@angular/core';
import { Component, effect, signal, inject, untracked, computed } from '@angular/core';
import { UserInputService } from '../../services/user-input-service';
import { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month';
import { CalendarService, SelectedDates } from '../../services/calendar-service';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
import { RuleService } from '../../services/rule-service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-year',
@@ -18,9 +18,15 @@ export class CalendarYear {
private dateCache = inject(DateCacheService);
private userInput = inject(UserInputService);
private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService);
private ruleService = inject(RuleService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
protected isLoading = signal(true);
protected loadingProgress = signal(0);
// Use DateCacheService instead of creating Date objects
@@ -36,11 +42,16 @@ export class CalendarYear {
effect(() => {
this.userInput.computeTriggerSignal();
this.selectedDates = this.calendarService.getSelectedDatesForYear();
const startDate = this.userInput.startDate();
const endDate = this.userInput.endDate();
this.selectedDates = this.calendarService.getSelectedDatesForPeriod(startDate, endDate);
const vacationData = untracked(() => this.userInput.vacationNumberSignal());
const c = untracked(() => this.calendarSettingsService.get('samediMalin')());
console.log('Samedi Malin setting:', c);
const currentRules = untracked(() => this.ruleService.rules);
console.log('Optimizing vacations with data:', vacationData);
console.log('Using rules:', currentRules);
this.isLoading.set(true);
this.loadingProgress.set(0);
// Defer execution to allow UI to update
@@ -51,8 +62,10 @@ export class CalendarYear {
const vacationDaysCopy = vacationData.map((vd) => ({ ...vd }));
const remainingDays = await this.selectedDates.optimizeVacations(
vacationDaysCopy,
this.calendarSettingsService,
currentRules,
this.calendarService,
startDate,
endDate,
(progress: number) => {
this.loadingProgress.set(progress);
},
@@ -68,11 +81,22 @@ export class CalendarYear {
protected selectedDates: SelectedDates = new SelectedDates();
getYear(monthIndex: number): number {
const offset = this.currentMonth <= monthIndex ? 0 : 1;
return this.currentYear + offset;
}
protected monthsAndYears = computed(() => {
const start = this.userInput.startDate();
const end = this.userInput.endDate();
// months starting from actual date - cache this array
protected months: number[] = Array.from({ length: 12 }, (_, i) => (this.currentMonth + i) % 12);
const result: { month: number; year: number }[] = [];
const current = new Date(start.getFullYear(), start.getMonth(), 1);
const limit = new Date(end.getFullYear(), end.getMonth(), 1);
while (current <= limit) {
result.push({
month: current.getMonth(),
year: current.getFullYear(),
});
current.setMonth(current.getMonth() + 1);
}
return result;
});
}
@@ -1,21 +1,37 @@
<div class="container">
<div>
<h1>Calendar Settings</h1>
<p>Configure your calendar settings here.</p>
<div class="container" style="min-width: 280px; padding: 8px">
<div style="margin-bottom: 1.5rem; text-align: center">
<h1 style="margin: 0 0 0.5rem 0; font-size: 1.5rem">{{ tr().settings.title }}</h1>
<p style="margin: 0; opacity: 0.7; font-size: 0.9rem">{{ tr().settings.description }}</p>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem">
<div style="display: flex; flex-direction: column; gap: 1rem; width: 100%">
@for (setting of calendarSettingsService.settingsArray; track setting.id) {
<div style="align-self: flex-start">
<div style="align-self: flex-start; margin-bottom: 0.5rem">
<mat-slide-toggle
[id]="setting.id"
[name]="setting.id"
[checked]="setting.signal()"
(change)="setting.signal.set($event.checked)"
>
{{ setting.description }}
{{ getSettingDescription(setting.id) }}
</mat-slide-toggle>
</div>
}
<mat-form-field appearance="outline" style="width: 100%; margin-top: 0.5rem">
<mat-label>{{ tr().settings.selectLanguage }}</mat-label>
<mat-select
[value]="translationService.language()"
(valueChange)="translationService.setLanguage($event)"
>
@for (lang of translationService.availableLanguages; track lang) {
<mat-option [value]="lang">{{
translationService.getLanguageName(lang)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<button mat-button (click)="closeDialog()">Fermer</button>
<button mat-button (click)="closeDialog()" style="margin-top: 1.5rem; width: 100%">
{{ tr().settings.close }}
</button>
</div>
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { vi } from 'vitest';
import { CalendarSettingsDialog } from './calendar-settings-dialog';
describe('CalendarSettingsDialog', () => {
@@ -15,7 +16,7 @@ describe('CalendarSettingsDialog', () => {
{
provide: MatDialogRef,
useValue: {
close: jasmine.createSpy('close'),
close: vi.fn(),
},
},
],
@@ -3,16 +3,32 @@ import { MatButton } from '@angular/material/button';
import { MatDialogRef } from '@angular/material/dialog';
import { CalendarSettingsService } from '../../../services/calendar-settings-service';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { TranslationService } from '../../../services/translation-service';
@Component({
selector: 'app-calendar-settings-dialog',
imports: [MatButton, MatSlideToggle],
imports: [MatButton, MatSlideToggle, MatFormFieldModule, MatSelectModule],
templateUrl: './calendar-settings-dialog.html',
styleUrl: './calendar-settings-dialog.scss',
})
export class CalendarSettingsDialog {
public readonly dialogRef = inject<MatDialogRef<CalendarSettingsDialog>>(MatDialogRef);
protected readonly calendarSettingsService = inject(CalendarSettingsService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
getSettingDescription(id: string): string {
const tr = this.tr();
if (id === 'showWeekNumbers') {
return tr.settings.showWeekNumbers;
}
return id;
}
closeDialog() {
this.dialogRef.close();
@@ -0,0 +1,357 @@
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>rule</mat-icon>
{{ tr().rules.title }}
</mat-panel-title>
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
</mat-expansion-panel-header>
<div class="priority-statement">
<mat-icon color="primary">info</mat-icon>
<span>{{ tr().rules.priorityInfo }}</span>
</div>
<div class="rules-list" cdkDropList (cdkDropListDropped)="drop($event)">
@for (rule of rules; track rule.id; let i = $index) {
<div
class="rule-item"
cdkDrag
[cdkDragDisabled]="editingRuleId() === rule.id"
[class.editing]="editingRuleId() === rule.id"
>
@if (editingRuleId() !== rule.id) {
<div class="rule-drag-grip">
<mat-icon class="drag-handle-icon">drag_handle</mat-icon>
</div>
<div class="rule-display">
<div class="rule-header">
<mat-slide-toggle
[checked]="rule.enabled"
(change)="toggleRule(rule.id)"
color="primary"
>
<span class="rule-name"
><span class="rule-number">{{ i + 1 }}.</span>
{{ rule.name }}</span
>
</mat-slide-toggle>
<div class="rule-actions">
<button mat-icon-button (click)="startEdit(rule, $event)">
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="deleteRule(rule.id, $event)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="rule-summary" [innerHTML]="getRuleSummary(rule)"></div>
</div>
} @else {
<div class="rule-editor">
<h4>{{ tr().rules.editRule }}</h4>
<mat-form-field class="full-width">
<mat-label>{{ tr().rules.ruleName }}</mat-label>
<input matInput [(ngModel)]="$any(editBuffer).name" required />
</mat-form-field>
@if ($any(editBuffer).type === RuleType.IF_THEN) {
<div class="editor-row">
<mat-form-field>
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
@for (day of daysOfWeek; track day.value) {
<mat-option [value]="day.value">{{
day.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.place }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.position }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placement">
@for (opt of placementOptions; track opt.value) {
<mat-option [value]="opt.value">{{
opt.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.getBonus }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).rewardDays"
min="0"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
@for (type of rewardDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="editor-row">
<mat-form-field>
<mat-label>{{ tr().rules.startDate }}</mat-label>
<input
matInput
[matDatepicker]="startPicker"
[(ngModel)]="$any(editBuffer).startDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="startPicker"
></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.endDate }}</mat-label>
<input
matInput
[matDatepicker]="endPicker"
[(ngModel)]="$any(editBuffer).endDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="endPicker"
></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.place }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
<div class="editor-actions">
<button mat-button (click)="cancelEdit()">
{{ tr().rules.cancel }}
</button>
<button mat-flat-button color="primary" (click)="saveEdit()">
{{ tr().rules.save }}
</button>
</div>
</div>
}
</div>
}
@if (isAddingNew()) {
<div class="rule-item editing">
<div class="rule-editor">
<h4>{{ tr().rules.addRule }}</h4>
<mat-form-field class="full-width">
<mat-label>{{ tr().rules.ruleName }}</mat-label>
<input matInput [(ngModel)]="$any(editBuffer).name" required />
</mat-form-field>
@if (newRuleType() === RuleType.IF_THEN) {
<div class="editor-row">
<mat-form-field>
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
@for (day of daysOfWeek; track day.value) {
<mat-option [value]="day.value">{{ day.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.place }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.position }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placement">
@for (opt of placementOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.getBonus }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).rewardDays"
min="0"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
@for (type of rewardDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="editor-row">
<mat-form-field>
<mat-label>{{ tr().rules.startDate }}</mat-label>
<input
matInput
[matDatepicker]="startPicker"
[(ngModel)]="$any(editBuffer).startDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="startPicker"
></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.endDate }}</mat-label>
<input
matInput
[matDatepicker]="endPicker"
[(ngModel)]="$any(editBuffer).endDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="endPicker"
></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">{{ tr().rules.place }}</span>
<mat-form-field class="small-input">
<mat-label>{{ tr().rules.days }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ tr().rules.type }}</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
<div class="editor-actions">
<button mat-button (click)="cancelEdit()">{{ tr().rules.cancel }}</button>
<button mat-flat-button color="primary" (click)="saveEdit()">
{{ tr().rules.add }}
</button>
</div>
</div>
</div>
}
<div class="add-buttons" [class.hidden]="isAddingNew() || editingRuleId()">
<button mat-stroked-button (click)="startAddNew(RuleType.IF_THEN)">
<mat-icon>add</mat-icon> {{ tr().rules.newIfThenRule }}
</button>
<button mat-stroked-button (click)="startAddNew(RuleType.TIME_WINDOW)">
<mat-icon>add</mat-icon> {{ tr().rules.newTimeWindowRule }}
</button>
</div>
</div>
</mat-expansion-panel>
@@ -0,0 +1,218 @@
.rules-panel-container {
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 12px;
overflow: hidden;
container-type: inline-size;
mat-panel-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
@container (max-width: 480px) {
mat-panel-description {
display: none !important;
}
}
}
.rules-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.rule-item {
border: 1px solid var(--mat-sys-outline-variant);
border-radius: 8px;
transition: all 0.2s ease;
background: var(--mat-sys-surface-container);
&:not(.editing) {
cursor: grab;
&:active {
cursor: grabbing;
}
}
&:hover:not(.editing) {
border-color: var(--mat-sys-outline);
}
&.editing {
border: 1px solid var(--mat-sys-primary);
}
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 8px;
box-shadow:
0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.rules-list.cdk-drop-list-dragging .rule-item:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.rule-display {
padding: 16px;
}
.priority-statement {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--mat-sys-primary-container);
color: var(--mat-sys-on-primary-container);
border-radius: 8px;
font-size: 0.95rem;
margin-top: 8px;
margin-bottom: 8px;
mat-icon {
color: var(--mat-sys-primary);
flex-shrink: 0;
}
}
.rule-drag-grip {
display: flex;
justify-content: center;
align-items: center;
padding-top: 8px;
margin-bottom: -8px;
color: var(--mat-sys-outline);
opacity: 0.6;
cursor: grab;
transition: opacity 0.2s;
.rule-item:active & {
cursor: grabbing;
}
&:hover {
opacity: 1;
}
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.rule-name {
font-weight: 600;
font-size: 1.05rem;
display: flex;
align-items: center;
gap: 6px;
.rule-number {
color: var(--mat-sys-primary);
font-weight: 700;
}
}
.rule-actions {
display: flex;
gap: 4px;
opacity: 0.6;
transition: opacity 0.2s;
.rule-item:hover & {
opacity: 1;
}
}
}
.rule-summary {
font-size: 0.95rem;
line-height: 1.5;
padding-left: 48px; /* Align with toggle switch text */
strong {
font-weight: 600;
}
}
.rule-editor {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
background: var(--mat-sys-surface-container-high);
border-radius: 8px;
h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.editor-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
.action-text {
font-weight: 500;
margin-right: 4px;
}
mat-form-field {
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
}
.small-input {
width: 100px;
}
}
.full-width {
width: 100%;
}
.editor-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
}
.add-buttons {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
&.hidden {
display: none;
}
button {
border-radius: 24px;
width: 100%;
}
}
@@ -0,0 +1,109 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { RulesPanel } from './rules-panel';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RuleService, RuleType } from '../../services/rule-service';
import { TranslationService } from '../../services/translation-service';
describe('RulesPanel', () => {
let component: RulesPanel;
let fixture: ComponentFixture<RulesPanel>;
beforeEach(async () => {
// Clear localStorage before testing
localStorage.clear();
await TestBed.configureTestingModule({
imports: [RulesPanel, BrowserAnimationsModule],
providers: [provideZonelessChangeDetection(), RuleService],
}).compileComponents();
fixture = TestBed.createComponent(RulesPanel);
component = fixture.componentInstance;
// Force language to 'fr' so the default rule names match French assertions
const translationService = TestBed.inject(TranslationService);
translationService.setLanguage('fr');
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display initial rules from service', () => {
expect(component.rules.length).toBe(2);
});
it('should start adding new If-Then rule', () => {
component.startAddNew(RuleType.IF_THEN);
expect(component['isAddingNew']()).toBe(true);
expect(component['newRuleType']()).toBe(RuleType.IF_THEN);
expect(component['editBuffer'].type).toBe(RuleType.IF_THEN);
expect(component['editBuffer'].name).toBe('Nouvelle Règle');
});
it('should start adding new Time Window rule', () => {
component.startAddNew(RuleType.TIME_WINDOW);
expect(component['isAddingNew']()).toBe(true);
expect(component['newRuleType']()).toBe(RuleType.TIME_WINDOW);
expect(component['editBuffer'].type).toBe(RuleType.TIME_WINDOW);
expect(component['editBuffer'].name).toBe('Nouvelle Période');
});
it('should cancel edit', () => {
component.startAddNew(RuleType.IF_THEN);
component.cancelEdit();
expect(component['isAddingNew']()).toBe(false);
expect(component['editingRuleId']()).toBeNull();
expect(Object.keys(component['editBuffer']).length).toBe(0);
});
it('should save a new rule', () => {
const initialCount = component.rules.length;
component.startAddNew(RuleType.IF_THEN);
component['editBuffer'].name = 'Custom Test Rule';
component.saveEdit();
expect(component.rules.length).toBe(initialCount + 1);
expect(component.rules.some((r) => r.name === 'Custom Test Rule')).toBe(true);
});
it('should update an existing rule', () => {
const targetRule = component.rules[0];
// Create a mock event with stopPropagation
const mockEvent = { stopPropagation: () => {} } as Event;
component.startEdit(targetRule, mockEvent);
component['editBuffer'].name = 'Updated Name';
component.saveEdit();
expect(component.rules.find((r) => r.id === targetRule.id)?.name).toBe('Updated Name');
});
it('should delete a rule', () => {
const targetId = component.rules[0].id;
const initialCount = component.rules.length;
const mockEvent = { stopPropagation: () => {} } as Event;
component.deleteRule(targetId, mockEvent);
expect(component.rules.length).toBe(initialCount - 1);
expect(component.rules.some((r) => r.id === targetId)).toBe(false);
});
it('should toggle a rule', () => {
const targetRule = component.rules[0];
const initialState = targetRule.enabled;
component.toggleRule(targetRule.id);
expect(component.rules.find((r) => r.id === targetRule.id)?.enabled).toBe(!initialState);
});
});
@@ -0,0 +1,250 @@
import {
Component,
ChangeDetectionStrategy,
inject,
signal,
Injectable,
effect,
} from '@angular/core';
import {
RuleService,
RuleType,
IfThenRule,
TimeWindowRule,
VacationRule,
PlacementStrategy,
} from '../../services/rule-service';
import { SelectableDayType } from '../../services/calendar-service';
import { TranslationService } from '../../services/translation-service';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { FormsModule } from '@angular/forms';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
NativeDateAdapter,
} from '@angular/material/core';
import { CdkDragDrop, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
@Injectable()
class CustomDateAdapter extends NativeDateAdapter {
override format(date: Date, displayFormat: unknown): string {
if (displayFormat === 'input') {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
return super.format(date, displayFormat as object);
}
}
const MY_DATE_FORMATS = {
parse: { dateInput: 'input' },
display: {
dateInput: 'input',
monthYearLabel: { year: 'numeric', month: 'short' },
dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
monthYearA11yLabel: { year: 'numeric', month: 'long' },
},
};
@Component({
selector: 'app-rules-panel',
imports: [
MatExpansionModule,
MatIconModule,
MatButtonModule,
MatSlideToggleModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
FormsModule,
CdkDropList,
CdkDrag,
],
templateUrl: './rules-panel.html',
styleUrl: './rules-panel.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{ provide: DateAdapter, useClass: CustomDateAdapter },
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
{ provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMATS },
],
})
export class RulesPanel {
public ruleService = inject(RuleService);
protected readonly translationService = inject(TranslationService);
private readonly dateAdapter = inject(DateAdapter);
get tr() {
return this.translationService.tr;
}
constructor() {
effect(() => {
this.dateAdapter.setLocale(this.translationService.locale());
});
}
protected readonly RuleType = RuleType;
protected readonly placeDayTypes = Object.values(SelectableDayType);
protected readonly rewardDayTypes = Object.values(SelectableDayType).filter(
(t) => t !== SelectableDayType.ANY,
);
protected get daysOfWeek() {
const tr = this.tr();
return [
{ value: 1, label: tr.rules.daysOfWeek[1] },
{ value: 2, label: tr.rules.daysOfWeek[2] },
{ value: 3, label: tr.rules.daysOfWeek[3] },
{ value: 4, label: tr.rules.daysOfWeek[4] },
{ value: 5, label: tr.rules.daysOfWeek[5] },
{ value: 6, label: tr.rules.daysOfWeek[6] },
{ value: 0, label: tr.rules.daysOfWeek[0] },
];
}
protected get placementOptions(): { value: PlacementStrategy; label: string }[] {
const tr = this.tr();
return [
{ value: 'before', label: tr.rules.placement.before },
{ value: 'after', label: tr.rules.placement.after },
{ value: 'around', label: tr.rules.placement.around },
{ value: 'auto', label: tr.rules.placement.auto },
];
}
protected editingRuleId = signal<string | null>(null);
protected isAddingNew = signal(false);
protected newRuleType = signal<RuleType>(RuleType.IF_THEN);
protected editBuffer: Partial<VacationRule> = {};
public get rules() {
return this.ruleService.rules;
}
public drop(event: CdkDragDrop<VacationRule[]>) {
if (event.previousIndex !== event.currentIndex) {
this.ruleService.reorderRules(event.previousIndex, event.currentIndex);
}
}
public toggleRule(id: string) {
this.ruleService.toggleRule(id);
}
public startEdit(rule: VacationRule, event: Event) {
event.stopPropagation();
this.editingRuleId.set(rule.id);
this.isAddingNew.set(false);
this.editBuffer = { ...rule };
}
public cancelEdit() {
this.editingRuleId.set(null);
this.isAddingNew.set(false);
this.editBuffer = {};
}
public saveEdit() {
if (this.editBuffer.id) {
this.ruleService.updateRule(this.editBuffer as VacationRule);
} else {
this.ruleService.addRule({
...this.editBuffer,
id: crypto.randomUUID(),
enabled: true,
} as VacationRule);
}
this.cancelEdit();
}
public deleteRule(id: string, event: Event) {
event.stopPropagation();
this.ruleService.deleteRule(id);
}
public startAddNew(type: RuleType) {
this.isAddingNew.set(true);
this.editingRuleId.set(null);
this.newRuleType.set(type);
if (type === RuleType.IF_THEN) {
this.editBuffer = {
name: this.tr().rules.defaultNewRuleName,
type: RuleType.IF_THEN,
dayOfWeek: 6,
placeDays: 2,
placeType: SelectableDayType.CP,
placement: 'auto',
rewardDays: 1,
rewardType: SelectableDayType.OTHER,
} as Partial<IfThenRule>;
} else {
this.editBuffer = {
name: this.tr().rules.defaultNewPeriodName,
type: RuleType.TIME_WINDOW,
startDate: new Date(),
endDate: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
placeDays: 5,
placeType: SelectableDayType.CP,
} as Partial<TimeWindowRule>;
}
}
protected getDayTypeLabel(type: string | undefined | null): string {
if (!type) return '';
return this.translationService.getDayTypeLabel(type);
}
public getRuleSummary(rule: VacationRule): string {
const tr = this.tr();
if (rule.type === RuleType.IF_THEN) {
const ifThen = rule as IfThenRule;
const dayLabel = this.daysOfWeek.find((d) => d.value === ifThen.dayOfWeek)?.label ?? '';
const placeTypeLabel = this.getDayTypeLabel(ifThen.placeType);
const placementLabel = (
this.placementOptions.find((p) => p.value === ifThen.placement)?.label ?? ''
)
.trim()
.toLowerCase();
const rewardTypeLabel = this.getDayTypeLabel(ifThen.rewardType);
return tr.rules.ifThenTemplate
.replace('{day}', dayLabel)
.replace('{placeDays}', String(ifThen.placeDays))
.replace('{placeType}', placeTypeLabel)
.replace('{placement}', placementLabel)
.replace('{rewardDays}', String(ifThen.rewardDays))
.replace('{rewardType}', rewardTypeLabel);
} else {
const timeWindow = rule as TimeWindowRule;
const placeTypeLabel = this.getDayTypeLabel(timeWindow.placeType);
const startDateStr = new Date(timeWindow.startDate).toLocaleDateString(
this.translationService.locale(),
);
const endDateStr = new Date(timeWindow.endDate).toLocaleDateString(
this.translationService.locale(),
);
return tr.rules.timeWindowTemplate
.replace('{placeDays}', String(timeWindow.placeDays))
.replace('{placeType}', placeTypeLabel)
.replace('{startDate}', startDateStr)
.replace('{endDate}', endDateStr);
}
}
}
+41 -7
View File
@@ -1,5 +1,31 @@
<div class="user-input">
<p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p>
<p>{{ tr().app.description }}</p>
<mat-card class="period-selection-card mat-elevation-z2">
<mat-card-content>
<h3 class="period-title">
<mat-icon>date_range</mat-icon> {{ tr().userInput.choosePeriod }}
</h3>
<mat-form-field appearance="outline" class="period-field">
<mat-label>{{ tr().userInput.choosePeriod }}</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input
matStartDate
[value]="startDate()"
(dateChange)="onStartDateChange($event.value)"
[placeholder]="tr().userInput.startDate"
/>
<input
matEndDate
[value]="endDate()"
(dateChange)="onEndDateChange($event.value)"
[placeholder]="tr().userInput.endDate"
/>
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
</mat-card-content>
</mat-card>
<div class="listOfDays">
@for (day of daysOff; track day.id) {
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
@@ -11,32 +37,40 @@
</div>
}
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
<button matFab aria-label="Add entry">
<button matFab [aria-label]="tr().userInput.addVacationDay">
<mat-icon>add</mat-icon>
</button>
<h3>Ajouter un jour de vacances</h3>
<h3>{{ tr().userInput.addVacationDay }}</h3>
</mat-card>
</div>
<div class="action-buttons">
<button matFab extended (click)="triggerComputation()">
<mat-icon>calculate</mat-icon>
Calculer
{{ tr().userInput.calculate }}
</button>
<app-calendar-settings></app-calendar-settings>
</div>
<app-rules-panel></app-rules-panel>
@if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card">
<mat-card-content class="warning-content">
<mat-icon color="warn">warning</mat-icon>
<div class="warning">
<span>
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances
non placé{{ remainingDays() > 1 ? 's' : '' }}:
{{
tr()
.userInput.remainingDaysWarning.replace(
'{count}',
remainingDays().toString()
)
.split('{plural}')
.join(remainingDays() > 1 ? 's' : '')
}}
</span>
@for (day of remainingDaysDetails(); track $index) {
<span class="remaining-day-detail">- {{ day }}</span>
}
<span> Veuillez les placer manuellement. </span>
<span> {{ tr().userInput.remainingDaysManualPlace }} </span>
</div>
</mat-card-content>
</mat-card>
@@ -23,6 +23,22 @@
align-items: center;
gap: 0.5rem;
}
.period-selection-card {
margin-bottom: 1rem;
.period-title {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0;
margin-bottom: 1rem;
}
.period-field {
width: 100%;
}
}
}
.listOfDays {
@@ -3,6 +3,7 @@ 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';
import { provideNativeDateAdapter } from '@angular/material/core';
describe('UserInput', () => {
let component: UserInput;
@@ -12,7 +13,11 @@ describe('UserInput', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserInput],
providers: [provideZonelessChangeDetection(), provideAnimationsAsync()],
providers: [
provideZonelessChangeDetection(),
provideAnimationsAsync(),
provideNativeDateAdapter(),
],
}).compileComponents();
fixture = TestBed.createComponent(UserInput);
@@ -43,4 +48,24 @@ describe('UserInput', () => {
component.removeFromList(0);
expect(userInputService.vacationNumberSignal().length).toBe(initialLength - 1);
});
it('should trigger computation', () => {
const initialValue = userInputService.computeTriggerSignal();
component.triggerComputation();
expect(userInputService.computeTriggerSignal()).toBe(initialValue + 1);
});
it('should update start date', () => {
const newDate = new Date();
newDate.setFullYear(newDate.getFullYear() + 2);
component.onStartDateChange(newDate);
expect(userInputService.startDate().getFullYear()).toBe(newDate.getFullYear());
});
it('should update end date', () => {
const newDate = new Date();
newDate.setFullYear(newDate.getFullYear() + 3);
component.onEndDateChange(newDate);
expect(userInputService.endDate().getFullYear()).toBe(newDate.getFullYear());
});
});
+40 -8
View File
@@ -1,18 +1,25 @@
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatInputModule } from '@angular/material/input';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { UserInputService } from '../../services/user-input-service';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
import { RulesPanel } from '../rules-panel/rules-panel';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { DayInput } from '../userInput/day-input/day-input';
import { MatButtonModule } from '@angular/material/button';
import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-service';
import { VacDay } from '../../services/calendar-service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-user-input',
imports: [
MatInputModule,
MatDatepickerModule,
MatFormFieldModule,
CalendarSettings,
RulesPanel,
MatCard,
MatCardContent,
MatIcon,
@@ -25,6 +32,11 @@ import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-ser
})
export class UserInput {
private userInputService: UserInputService = inject(UserInputService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
protected remainingDays = computed(() => {
const remaining = this.userInputService.remainingVacationDaysSignal();
@@ -36,13 +48,17 @@ export class UserInput {
return remaining
.filter((day) => day.numberOfDays > 0)
.map((day) => {
const typeName = tooltipTypeMapping[day.type as unknown as DayType];
const expiryDate = day.expiryDate.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
return `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;
const typeName = this.translationService.getDayTypeLabel(day.type);
const expiryDate = day.expiryDate.toLocaleDateString(
this.translationService.locale(),
{
day: 'numeric',
month: 'short',
year: 'numeric',
},
);
const expiresLabel = this.tr().userInput.expires;
return `${day.numberOfDays} ${typeName} (${expiresLabel} ${expiryDate})`;
});
});
@@ -74,4 +90,20 @@ export class UserInput {
public triggerComputation() {
this.userInputService.requestComputation();
}
protected get startDate() {
return this.userInputService.startDate;
}
protected get endDate() {
return this.userInputService.endDate;
}
public onStartDateChange(date: Date | null) {
this.userInputService.setStartDate(date);
}
public onEndDateChange(date: Date | null) {
this.userInputService.setEndDate(date);
}
}
@@ -13,12 +13,14 @@
<mat-select (valueChange)="onTypeChange($event)" [value]="day().type">
<mat-option></mat-option>
@for (dayType of selectableDayTypes; track $index) {
<mat-option [value]="dayType">{{ tooltipTypeMapping[dayType] }}</mat-option>
<mat-option [value]="dayType">{{
translationService.getDayTypeLabel(dayType)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="dateInput">
<mat-label>Choose a date</mat-label>
<mat-label>{{ tr().userInput.chooseDate }}</mat-label>
<input
matInput
type="datetime"
@@ -1,3 +1,9 @@
:host {
display: block;
container-type: inline-size;
width: 100%;
}
.card {
display: flex;
flex-direction: row;
@@ -12,9 +18,55 @@
}
.numberInput {
width: 80px;
width: 120px;
}
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
@container (max-width: 480px) {
.card {
flex-wrap: wrap;
gap: 12px;
padding: 12px;
align-items: center;
}
.numberInput {
width: 70px;
flex-grow: 0;
}
.dayTypeInput {
flex-grow: 1;
width: 0;
min-width: 120px;
}
.dateInput {
width: 100%;
flex-grow: 1;
flex-basis: 100%;
margin-top: 4px;
}
button[matFab] {
width: 40px;
height: 40px;
min-width: 40px;
line-height: 40px;
::ng-deep .mat-mdc-button-touch-target {
width: 40px;
height: 40px;
}
mat-icon {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
@@ -1,5 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { vi } from 'vitest';
import { DayInput } from './day-input';
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
@@ -30,4 +32,23 @@ describe('DayInput', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit dayChange on input', () => {
const spy = vi.spyOn(component.dayChange, 'emit');
component.onInput({ target: { value: '3' } } as unknown as Event);
expect(spy).toHaveBeenCalledWith({ ...mockVacDay, numberOfDays: 3 });
});
it('should emit dayChange on type change', () => {
const spy = vi.spyOn(component.dayChange, 'emit');
component.onTypeChange(SelectableDayType.RTT);
expect(spy).toHaveBeenCalledWith({ ...mockVacDay, type: SelectableDayType.RTT });
});
it('should emit dayChange on date input', () => {
const spy = vi.spyOn(component.dayChange, 'emit');
const newDate = new Date('2026-01-01');
component.addEvent({ value: newDate } as unknown as MatDatepickerInputEvent<Date>);
expect(spy).toHaveBeenCalledWith({ ...mockVacDay, expiryDate: newDate });
});
});
@@ -1,5 +1,14 @@
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
import { SelectableDayType, tooltipTypeMapping, VacDay } from '../../../services/calendar-service';
import {
Component,
input,
output,
ChangeDetectionStrategy,
Injectable,
inject,
effect,
} from '@angular/core';
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
import { TranslationService } from '../../../services/translation-service';
import { MatCard } from '@angular/material/card';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatInput, MatInputModule, MatLabel } from '@angular/material/input';
@@ -18,6 +27,7 @@ import {
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
@Injectable()
class CustomDateAdapter extends NativeDateAdapter {
override format(date: Date, displayFormat: unknown): string {
if (displayFormat === 'input') {
@@ -85,8 +95,22 @@ export class DayInput {
public dayChange = output<VacDay>();
public deleteEntry = output<void>();
protected readonly selectableDayTypes = Object.values(SelectableDayType);
protected readonly tooltipTypeMapping = tooltipTypeMapping;
protected readonly translationService = inject(TranslationService);
private readonly dateAdapter = inject(DateAdapter);
get tr() {
return this.translationService.tr;
}
constructor() {
effect(() => {
this.dateAdapter.setLocale(this.translationService.locale());
});
}
protected readonly selectableDayTypes = Object.values(SelectableDayType).filter(
(t) => t !== SelectableDayType.ANY,
);
onInput(event: Event) {
const value = Number((event.target as HTMLInputElement).value);
+320 -18
View File
@@ -1,7 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { vi } from 'vitest';
import { provideZonelessChangeDetection } from '@angular/core';
import { CalendarService, DayType, SelectedDate, SelectedDates } from './calendar-service';
import {
CalendarService,
DayType,
SelectedDate,
SelectedDates,
SelectableDayType,
VacDay,
} from './calendar-service';
import { DateRange } from '@angular/material/datepicker';
import { IfThenRule, TimeWindowRule, RuleType } from './rule-service';
describe('CalendarService', () => {
let service: CalendarService;
@@ -17,18 +26,15 @@ describe('CalendarService', () => {
expect(service).toBeTruthy();
});
describe('getHolidays', () => {
describe('getHolidaysForPeriod', () => {
it('should return an array of holidays', () => {
const holidays = service.getHolidays();
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const holidays = service.getHolidaysForPeriod(startDate, endDate);
expect(Array.isArray(holidays)).toBe(true);
expect(holidays.length).toBeGreaterThan(0);
});
it('should cache holidays for the same year', () => {
const holidays1 = service.getHolidays();
const holidays2 = service.getHolidays();
expect(holidays1).toEqual(holidays2);
});
});
describe('static date calculation methods', () => {
@@ -48,9 +54,12 @@ describe('CalendarService', () => {
expect(months.length).toBe(12);
});
it('should calculate months from now', () => {
const months = service.monthsInAYearFromNow();
expect(months.length).toBe(12);
it('should calculate months in period', () => {
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const months = service.monthsInPeriod(startDate, endDate);
expect(months.length).toBeGreaterThanOrEqual(12);
});
it('should return week number for a date', () => {
@@ -61,15 +70,21 @@ describe('CalendarService', () => {
});
});
describe('getSelectedDatesForYear', () => {
it('should return selected dates for the year', () => {
const selectedDates = service.getSelectedDatesForYear();
describe('getSelectedDatesForPeriod', () => {
it('should return selected dates for the period', () => {
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
expect(selectedDates).toBeDefined();
expect(selectedDates._datesSelected.length).toBeGreaterThan(0);
});
it('should include weekends', () => {
const selectedDates = service.getSelectedDatesForYear();
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
const hasWeekend = selectedDates._datesSelected.some(
(date) => date.type === DayType.WEEKEND,
);
@@ -77,7 +92,10 @@ describe('CalendarService', () => {
});
it('should include holidays', () => {
const selectedDates = service.getSelectedDatesForYear();
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
const hasHoliday = selectedDates._datesSelected.some(
(date) => date.type === DayType.CLOSED_DAY,
);
@@ -85,7 +103,10 @@ describe('CalendarService', () => {
});
it('should include today', () => {
const selectedDates = service.getSelectedDatesForYear();
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
const hasToday = selectedDates._datesSelected.some(
(date) => date.type === DayType.TODAY,
);
@@ -202,4 +223,285 @@ describe('SelectedDates', () => {
const tooltip = selectedDates.getTooltip(date);
expect(tooltip).toBeTruthy();
});
describe('optimizeVacations and rules engine', () => {
let calendarService: CalendarService;
let selectedDates: SelectedDates;
beforeEach(() => {
calendarService = TestBed.inject(CalendarService);
selectedDates = new SelectedDates();
});
it('should execute rules sequentially in order', async () => {
const vacationDays: VacDay[] = [];
const rules: (TimeWindowRule | IfThenRule)[] = [
{ id: '1', type: RuleType.IF_THEN, enabled: true } as IfThenRule,
{ id: '2', type: RuleType.TIME_WINDOW, enabled: true } as TimeWindowRule,
{ id: '3', type: RuleType.IF_THEN, enabled: true } as IfThenRule,
];
const applyTimeWindowRulesSpy = vi.spyOn(selectedDates, 'applyTimeWindowRules');
const applyIfThenRulesSpy = vi.spyOn(selectedDates, 'applyIfThenRules');
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 1);
await selectedDates.optimizeVacations(
vacationDays,
rules,
calendarService,
startDate,
endDate,
);
expect(applyIfThenRulesSpy.mock.calls.length).toBe(2);
expect(applyTimeWindowRulesSpy.mock.calls.length).toBe(1);
// First rule is IF_THEN
expect(applyIfThenRulesSpy.mock.calls[0][1][0].id).toBe('1');
// Second rule is TIME_WINDOW
expect(applyTimeWindowRulesSpy.mock.calls[0][1][0].id).toBe('2');
// Third rule is IF_THEN
expect(applyIfThenRulesSpy.mock.calls[1][1][0].id).toBe('3');
});
it('should apply time window rules', () => {
const futureYear = new Date().getFullYear() + 2;
const vacationDays: VacDay[] = [
{
id: '1',
type: SelectableDayType.CP,
numberOfDays: 5,
expiryDate: new Date(futureYear + 1, 11, 31),
},
];
const rules: TimeWindowRule[] = [
{
id: 't1',
name: 'Time Window',
type: RuleType.TIME_WINDOW,
enabled: true,
startDate: new Date(futureYear, 11, 20),
endDate: new Date(futureYear, 11, 25),
placeDays: 3,
placeType: SelectableDayType.CP,
},
];
// Mock monthsInPeriod to return a controlled range of days
vi.spyOn(calendarService, 'monthsInPeriod').mockReturnValue([
[
[
new Date(futureYear, 11, 20),
new Date(futureYear, 11, 21),
new Date(futureYear, 11, 22),
new Date(futureYear, 11, 23),
new Date(futureYear, 11, 24),
],
],
]);
const startDate = new Date(futureYear, 11, 20);
const endDate = new Date(futureYear, 11, 25);
selectedDates.applyTimeWindowRules(
vacationDays,
rules,
calendarService,
startDate,
endDate,
);
// Should have placed 3 days
const placedCount = selectedDates._datesSelected.filter(
(d) => d.type === DayType.CP,
).length;
expect(placedCount).toBe(3);
// Should have consumed 3 days from the pool
expect(vacationDays[0].numberOfDays).toBe(2);
});
it('should apply if-then rules', () => {
const futureYear = new Date().getFullYear() + 2;
const vacationDays: VacDay[] = [
{
id: '1',
type: SelectableDayType.CP,
numberOfDays: 5,
expiryDate: new Date(futureYear + 1, 11, 31),
},
];
const rules: IfThenRule[] = [
{
id: 'i1',
name: 'Samedi Malin',
type: RuleType.IF_THEN,
enabled: true,
dayOfWeek: 6, // Saturday
placeDays: 2,
placeType: SelectableDayType.CP,
placement: 'auto',
rewardDays: 1,
rewardType: SelectableDayType.OTHER,
},
];
// Setup a mock holiday on a Saturday
// Use a date far in the future so it doesn't fail the "past date" check
const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5); // Dec 5 is usually Saturday or close, let's just make sure we get a Saturday
// To guarantee it's a Saturday for the test:
while (saturdayHoliday.getDay() !== 6) {
saturdayHoliday.setDate(saturdayHoliday.getDate() + 1);
}
selectedDates._datesSelected.push(
new SelectedDate(
DayType.CLOSED_DAY,
new DateRange<Date>(saturdayHoliday, saturdayHoliday),
),
);
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 3);
selectedDates.applyIfThenRules(vacationDays, rules, startDate, endDate);
// Should have placed 2 CP days (e.g. Thu/Fri, Mon/Tue, or Fri/Mon)
const placedCP = selectedDates._datesSelected.filter(
(d) => d.type === DayType.CP,
).length;
expect(placedCP).toBe(2);
// Should have consumed 2 CP days
expect(vacationDays[0].numberOfDays).toBe(3);
// Should have added 1 OTHER reward day
const rewardDay = vacationDays.find((vd) => vd.type === SelectableDayType.OTHER);
expect(rewardDay).toBeDefined();
expect(rewardDay?.numberOfDays).toBe(1);
});
it('should apply time window rules with ANY type', () => {
const futureYear = new Date().getFullYear() + 2;
const vacationDays: VacDay[] = [
{
id: '1',
type: SelectableDayType.CP,
numberOfDays: 2,
expiryDate: new Date(futureYear, 11, 31),
},
{
id: '2',
type: SelectableDayType.RTT,
numberOfDays: 2,
expiryDate: new Date(futureYear - 1, 11, 31), // Expires earlier
},
];
const rules: TimeWindowRule[] = [
{
id: 'tw1',
name: 'Vacances',
type: RuleType.TIME_WINDOW,
enabled: true,
startDate: new Date(futureYear, 6, 1),
endDate: new Date(futureYear, 6, 15),
placeDays: 3,
placeType: SelectableDayType.ANY,
},
];
vi.spyOn(calendarService, 'monthsInPeriod').mockReturnValue([
[
[
new Date(futureYear, 6, 1),
new Date(futureYear, 6, 2),
new Date(futureYear, 6, 3),
new Date(futureYear, 6, 4),
new Date(futureYear, 6, 5),
],
],
]);
const startDate = new Date(futureYear, 6, 1);
const endDate = new Date(futureYear, 6, 15);
selectedDates.applyTimeWindowRules(
vacationDays,
rules,
calendarService,
startDate,
endDate,
);
// Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP
const placedRTT = selectedDates._datesSelected.filter(
(d) => d.type === DayType.RTT,
).length;
const placedCP = selectedDates._datesSelected.filter(
(d) => d.type === DayType.CP,
).length;
expect(placedRTT).toBe(2);
expect(placedCP).toBe(1);
// Should have consumed 2 RTT and 1 CP
expect(vacationDays[0].numberOfDays).toBe(1); // CP remaining
expect(vacationDays[1].numberOfDays).toBe(0); // RTT remaining
});
it('should apply if-then rules with ANY type', () => {
const futureYear = new Date().getFullYear() + 2;
const vacationDays: VacDay[] = [
{
id: '1',
type: SelectableDayType.CP,
numberOfDays: 2,
expiryDate: new Date(futureYear + 2, 11, 31),
},
{
id: '2',
type: SelectableDayType.RTT,
numberOfDays: 2,
expiryDate: new Date(futureYear + 1, 11, 31), // Expires earlier
},
];
const rules: IfThenRule[] = [
{
id: 'i1',
name: 'Rule',
type: RuleType.IF_THEN,
enabled: true,
dayOfWeek: 6, // Saturday
placeDays: 3,
placeType: SelectableDayType.ANY,
placement: 'auto',
rewardDays: 0,
rewardType: SelectableDayType.CP,
},
];
const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5);
while (saturdayHoliday.getDay() !== 6) {
saturdayHoliday.setDate(saturdayHoliday.getDate() + 1);
}
selectedDates._datesSelected.push(
new SelectedDate(
DayType.CLOSED_DAY,
new DateRange<Date>(saturdayHoliday, saturdayHoliday),
),
);
const startDate = new Date();
const endDate = new Date();
endDate.setFullYear(endDate.getFullYear() + 3);
selectedDates.applyIfThenRules(vacationDays, rules, startDate, endDate);
// Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP
const placedRTT = selectedDates._datesSelected.filter(
(d) => d.type === DayType.RTT,
).length;
const placedCP = selectedDates._datesSelected.filter(
(d) => d.type === DayType.CP,
).length;
expect(placedRTT).toBe(2);
expect(placedCP).toBe(1);
});
});
});
+349 -242
View File
@@ -4,8 +4,8 @@ import Holidays, { HolidaysTypes } from 'date-holidays';
import dayjs from 'dayjs';
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 { VacationRule, RuleType, IfThenRule, TimeWindowRule } from './rule-service';
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
@@ -22,21 +22,9 @@ const allTypesOptions: HolidaysTypes.Options = {
export class CalendarService {
private dateCache = inject(DateCacheService);
// Cache for holiday timestamps
private holidayTimestampsCache: Set<number> | null = null;
private holidayCacheYear: number | null = null;
// Get selected dates from current date to the next year
getSelectedDatesFromNow(): SelectedDates {
const _holidays = this.getHolidays();
const selectedDates: SelectedDates = new SelectedDates();
// Logic to populate selectedDates from now until the next year
return selectedDates;
}
getSelectedDatesForYear(): SelectedDates {
const holidays = this.getHolidays();
const monthInAYear = this.monthsInAYearFromNow();
getSelectedDatesForPeriod(startDate: Date, endDate: Date): SelectedDates {
const holidays = this.getHolidaysForPeriod(startDate, endDate);
const monthInPeriod = this.monthsInPeriod(startDate, endDate);
const selectedDates: SelectedDates = new SelectedDates();
// Use DateCacheService to get today's date components
@@ -49,7 +37,7 @@ export class CalendarService {
// Create a Set of holiday timestamps for O(1) lookup instead of O(n)
const holidayTimestamps = new Set(holidays.map((h) => h.getTime()));
for (const weeksInAMonth of monthInAYear) {
for (const weeksInAMonth of monthInPeriod) {
for (const daysInAWeek of weeksInAMonth) {
for (const day of daysInAWeek) {
if (day) {
@@ -101,24 +89,16 @@ export class CalendarService {
}
}
getHolidays(): Date[] {
// Cache holidays to avoid recalculation
const currentYear = this.dateCache.getCurrentYear();
if (this.holidayCacheYear === currentYear && this.holidayTimestampsCache) {
// Return timestamps directly for efficient lookup
return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts));
}
// Logic to return holidays for the given year
getHolidaysForPeriod(startDate: Date, endDate: Date): Date[] {
const startYear = startDate.getFullYear();
const endYear = endDate.getFullYear();
const holidaysDate: Date[] = [];
const hd = new Holidays('FR', allTypesOptions);
const holidays = hd.getHolidays(currentYear, lang);
const holidaysPlusOne = hd.getHolidays(currentYear + 1, lang);
holidays.push(...holidaysPlusOne);
const holidaysDate: Date[] = holidays.map((holiday) => new Date(holiday.date));
// Update cache
this.holidayTimestampsCache = new Set(holidaysDate.map((d) => d.getTime()));
this.holidayCacheYear = currentYear;
for (let y = startYear; y <= endYear; y++) {
const yearHolidays = hd.getHolidays(y, lang);
holidaysDate.push(...yearHolidays.map((holiday) => new Date(holiday.date)));
}
return holidaysDate;
}
@@ -167,14 +147,15 @@ export class CalendarService {
return months;
}
monthsInAYearFromNow(): Year {
monthsInPeriod(startDate: Date, endDate: Date): Year {
const months: Year = [];
const currentMonthIndex = this.dateCache.getCurrentMonth();
const currentYear = this.dateCache.getCurrentYear();
for (let i = 0; i < 12; i++) {
const monthIndex = (currentMonthIndex + i) % 12;
const year = currentYear + Math.floor((currentMonthIndex + i) / 12);
months.push(CalendarService.weeksInMonth(monthIndex, year));
const start = dayjs(startDate).startOf('month');
const end = dayjs(endDate).startOf('month');
let current = start;
while (current.isBefore(end) || current.isSame(end, 'month')) {
months.push(CalendarService.weeksInMonth(current.month(), current.year()));
current = current.add(1, 'month');
}
return months;
}
@@ -208,15 +189,17 @@ export enum SelectableDayType {
CP = DayType.CP,
RTT = DayType.RTT,
OTHER = DayType.OTHER,
ANY = 'any',
}
export const tooltipTypeMapping: { [key in DayType]: string } = {
export const tooltipTypeMapping: { [key in DayType | SelectableDayType]: string } = {
[DayType.WEEKEND]: 'Weekend',
[DayType.CLOSED_DAY]: 'Jour Férié',
[DayType.RTT]: 'RTT',
[DayType.CP]: 'Congé Payé',
[DayType.OTHER]: 'Autre',
[DayType.TODAY]: "Aujourd'hui",
[SelectableDayType.ANY]: "N'importe",
};
export const monthNames: string[] = [
@@ -305,7 +288,6 @@ 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();
@@ -545,16 +527,33 @@ export class SelectedDates implements SelectedDateInterface {
async optimizeVacations(
vacationDays: VacDay[],
calendarSettingsService: CalendarSettingsService,
rules: VacationRule[],
calendarService: CalendarService,
startDate: Date,
endDate: Date,
progressCallback?: (progress: number) => void,
): Promise<VacDay[]> {
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationDays);
// Calculate total days to process
const totalDays = vacationDays.reduce((sum, vd) => sum + vd.numberOfDays, 0);
let processedDays = 0;
// Execute custom rules sequentially in the exact order they appear in the list
for (const rule of rules) {
if (!rule.enabled) continue;
if (rule.type === RuleType.TIME_WINDOW) {
this.applyTimeWindowRules(
vacationDays,
[rule as TimeWindowRule],
calendarService,
startDate,
endDate,
);
} else if (rule.type === RuleType.IF_THEN) {
this.applyIfThenRules(vacationDays, [rule as IfThenRule], startDate, endDate);
}
}
// Helper function to update progress, throttled to every 5%
let lastReportedProgress = 0;
const PROGRESS_STEP = 5; // percent
@@ -587,7 +586,7 @@ export class SelectedDates implements SelectedDateInterface {
}
const vacDayToUse = sortedVacDays[0];
const result = this.lookForVacation(vacDayToUse, calendarService);
const result = this.lookForVacation(vacDayToUse, calendarService, startDate, endDate);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
// Mark this VacDay as exhausted and try other vacation days
@@ -617,209 +616,326 @@ export class SelectedDates implements SelectedDateInterface {
return vacationDays;
}
samediMalin(vacationDays: VacDay[]): void {
const now = new Date();
now.setHours(0, 0, 0, 0);
const nowTime = now.getTime();
applyTimeWindowRules(
vacationDays: VacDay[],
rules: TimeWindowRule[],
calendarService: CalendarService,
startDate: Date,
endDate: Date,
): void {
for (const rule of rules) {
let daysToPlace = rule.placeDays;
const samedi_ferie = this._datesSelected.filter(
(date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6,
);
const consumeDays = (count: number): boolean => {
let needed = count;
const availableDays = vacationDays
.filter(
(vd) =>
(rule.placeType === SelectableDayType.ANY ||
vd.type === rule.placeType) &&
vd.numberOfDays > 0,
)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());
// Helper function to get available CP days that don't expire before a given date (ordered by expiry date)
const getAvailableCpDaysForDate = (useByDate: Date): VacDay[] => {
for (const vd of availableDays) {
if (needed === 0) break;
const take = Math.min(vd.numberOfDays, needed);
vd.numberOfDays -= take;
needed -= take;
}
return needed === 0;
};
const totalAvailable = vacationDays
.filter(
(vd) => rule.placeType === SelectableDayType.ANY || vd.type === rule.placeType,
)
.reduce((sum, vd) => sum + vd.numberOfDays, 0);
if (totalAvailable < daysToPlace) {
console.warn(
`Insufficient ${rule.placeType} days for Time Window Rule "${rule.name}". Needs ${daysToPlace}, has ${totalAvailable}. Placing ${totalAvailable} days instead.`,
);
daysToPlace = totalAvailable;
}
if (daysToPlace <= 0) continue;
const getNextAvailableType = (): DayType => {
const sorted = vacationDays
.filter(
(vd) =>
(rule.placeType === SelectableDayType.ANY ||
vd.type === rule.placeType) &&
vd.numberOfDays > 0,
)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());
return sorted.length > 0
? (sorted[0].type as unknown as DayType)
: ((rule.placeType === SelectableDayType.ANY
? SelectableDayType.CP
: rule.placeType) as unknown as DayType);
};
const candidates: Date[] = [];
const daysOfYear = calendarService.monthsInPeriod(startDate, endDate);
const start = new Date(rule.startDate).getTime();
const end = new Date(rule.endDate).getTime();
const startLimit = startDate.getTime();
const endLimit = endDate.getTime();
for (const month of daysOfYear) {
for (const week of month) {
for (const day of week) {
if (day) {
const dayTime = day.getTime();
if (
dayTime >= start &&
dayTime <= end &&
dayTime >= startLimit &&
dayTime <= endLimit &&
!this.isSelected(day)
) {
candidates.push(day);
}
}
}
}
}
for (let i = 0; i < daysToPlace; i++) {
if (candidates.length === 0) break;
const currentPlaceType = getNextAvailableType();
let bestCandidate: Date | null = null;
let bestHeuristic = -Infinity;
let bestCandidateIndex = -1;
for (let j = 0; j < candidates.length; j++) {
const day = candidates[j];
if (this.isSelected(day)) continue;
const tempDatesBackup = [...this._datesSelected];
this._datesSelected.push(
new SelectedDate(currentPlaceType, new DateRange<Date>(day, day)),
);
this.datesSelected = this._datesSelected.slice();
this.grouping();
const heuristic = this.computeHeuristics();
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
if (heuristic > bestHeuristic) {
bestHeuristic = heuristic;
bestCandidate = day;
bestCandidateIndex = j;
}
}
if (bestCandidate) {
this.push(
new SelectedDate(
currentPlaceType,
new DateRange<Date>(bestCandidate, bestCandidate),
),
);
consumeDays(1);
candidates.splice(bestCandidateIndex, 1);
}
}
}
}
applyIfThenRules(
vacationDays: VacDay[],
rules: IfThenRule[],
startDate: Date,
endDate: Date,
): void {
const nowTime = new Date().setHours(0, 0, 0, 0);
const startLimit = Math.max(nowTime, startDate.getTime());
const endLimit = endDate.getTime();
const consumeDays = (count: number, type: SelectableDayType, useByDate: Date): boolean => {
let daysToConsume = count;
const useByTime = useByDate.getTime();
return vacationDays
const availableDays = vacationDays
.filter(
(vd) =>
vd.type === SelectableDayType.CP &&
(type === SelectableDayType.ANY || vd.type === type) &&
vd.numberOfDays > 0 &&
vd.expiryDate.getTime() >= useByTime,
)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());
};
// Helper function to get total available CP days that don't expire before a given date
const getTotalCpDaysForDate = (useByDate: Date): number => {
const useByTime = useByDate.getTime();
return vacationDays
.filter(
(vd) =>
vd.type === SelectableDayType.CP && vd.expiryDate.getTime() >= useByTime,
)
.reduce((sum, vd) => sum + vd.numberOfDays, 0);
};
// Helper function to consume 2 CP days from the available pool for a specific date
const consume2CpDaysForDate = (useByDate: Date): boolean => {
let daysToConsume = 2;
const cpDays = getAvailableCpDaysForDate(useByDate);
for (const cpDay of cpDays) {
for (const vd of availableDays) {
if (daysToConsume === 0) break;
const toConsume = Math.min(cpDay.numberOfDays, daysToConsume);
cpDay.numberOfDays -= toConsume;
daysToConsume -= toConsume;
const take = Math.min(vd.numberOfDays, daysToConsume);
vd.numberOfDays -= take;
daysToConsume -= take;
}
return daysToConsume === 0;
};
// Helper function to add 1 OTHER day as reward
const addOtherDayReward = (): void => {
// Find an existing OTHER VacDay or create a new one
const otherDay = vacationDays.find((vd) => vd.type === SelectableDayType.OTHER);
if (otherDay) {
otherDay.numberOfDays += 1;
const checkAvailableDays = (
count: number,
type: SelectableDayType,
useByDate: Date,
): boolean => {
const useByTime = useByDate.getTime();
const total = vacationDays
.filter(
(vd) =>
(type === SelectableDayType.ANY || vd.type === type) &&
vd.expiryDate.getTime() >= useByTime,
)
.reduce((sum, vd) => sum + vd.numberOfDays, 0);
return total >= count;
};
const addRewardDays = (count: number, type: SelectableDayType): void => {
if (count <= 0) return;
const existing = vacationDays.find((vd) => vd.type === type);
if (existing) {
existing.numberOfDays += count;
} else {
// Create a new OTHER VacDay with a far future expiry date
vacationDays.push({
id: crypto.randomUUID(),
type: SelectableDayType.OTHER,
numberOfDays: 1,
expiryDate: new Date(now.getFullYear() + 10, 11, 31),
type: type,
numberOfDays: count,
expiryDate: new Date(new Date().getFullYear() + 10, 11, 31),
});
}
};
for (const holiday of samedi_ferie) {
if (!holiday.range.start) continue;
// Get the Saturday date (holiday) and the following Sunday
const saturdayDate = new Date(holiday.range.start);
const sundayDate = new Date(saturdayDate);
sundayDate.setDate(saturdayDate.getDate() + 1);
// Calculate dates for all three strategies (2 CP days total each)
// Strategy 1: 2 days before (Thursday + Friday)
const twoDaysBefore1 = new Date(saturdayDate);
twoDaysBefore1.setDate(saturdayDate.getDate() - 2);
const twoDaysBefore2 = new Date(saturdayDate);
twoDaysBefore2.setDate(saturdayDate.getDate() - 1);
// Strategy 2: 2 days after (Monday + Tuesday)
const twoDaysAfter1 = new Date(sundayDate);
twoDaysAfter1.setDate(sundayDate.getDate() + 1);
const twoDaysAfter2 = new Date(sundayDate);
twoDaysAfter2.setDate(sundayDate.getDate() + 2);
// Strategy 3: 1 day before + 1 day after (Friday + Monday)
const oneDayBefore = new Date(saturdayDate);
oneDayBefore.setDate(saturdayDate.getDate() - 1);
const oneDayAfter = new Date(sundayDate);
oneDayAfter.setDate(sundayDate.getDate() + 1);
// Check if we can apply each strategy (not in the past, not already selected, and have non-expired CP days)
const canApplyTwoBeforeStrategy =
twoDaysBefore1.getTime() >= nowTime &&
!this.isSelected(twoDaysBefore1) &&
!this.isSelected(twoDaysBefore2) &&
getTotalCpDaysForDate(twoDaysBefore2) >= 2; // Check expiry against the latest date used
const canApplyTwoAfterStrategy =
twoDaysAfter1.getTime() >= nowTime &&
!this.isSelected(twoDaysAfter1) &&
!this.isSelected(twoDaysAfter2) &&
getTotalCpDaysForDate(twoDaysAfter2) >= 2; // Check expiry against the latest date used
const canApplyMixedStrategy =
oneDayBefore.getTime() >= nowTime &&
!this.isSelected(oneDayBefore) &&
!this.isSelected(oneDayAfter) &&
getTotalCpDaysForDate(oneDayAfter) >= 2; // Check expiry against the latest date used
// Test all applicable strategies and choose the one with the best heuristic score
const strategies: { apply: () => void; heuristic: number }[] = [];
const tempDatesBackup = [...this._datesSelected];
if (canApplyTwoBeforeStrategy) {
this.push(
new SelectedDate(
DayType.CP,
new DateRange<Date>(twoDaysBefore1, twoDaysBefore2),
),
const getWorkingDays = (startDate: Date, direction: 1 | -1, count: number): Date[] => {
const result: Date[] = [];
const current = new Date(startDate);
while (result.length < count) {
current.setDate(current.getDate() + direction);
const isWeekend = current.getDay() === 0 || current.getDay() === 6;
const isHoliday = this._datesSelected.some(
(d) =>
d.type === DayType.CLOSED_DAY &&
d.range.start?.getTime() === current.getTime(),
);
const heuristic = this.computeHeuristics();
strategies.push({
apply: () => {
this._datesSelected = [...tempDatesBackup];
this.push(
new SelectedDate(
DayType.CP,
new DateRange<Date>(twoDaysBefore1, twoDaysBefore2),
),
);
consume2CpDaysForDate(twoDaysBefore2);
addOtherDayReward();
},
heuristic: heuristic,
});
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
if (!isWeekend && !isHoliday) {
result.push(new Date(current));
}
}
return result;
};
if (canApplyTwoAfterStrategy) {
this.push(
new SelectedDate(DayType.CP, new DateRange<Date>(twoDaysAfter1, twoDaysAfter2)),
);
const heuristic = this.computeHeuristics();
strategies.push({
apply: () => {
this._datesSelected = [...tempDatesBackup];
this.push(
new SelectedDate(
DayType.CP,
new DateRange<Date>(twoDaysAfter1, twoDaysAfter2),
),
);
consume2CpDaysForDate(twoDaysAfter2);
addOtherDayReward();
},
heuristic: heuristic,
});
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
}
for (const rule of rules) {
const matchingHolidays = this._datesSelected.filter(
(date) =>
date.type === DayType.CLOSED_DAY &&
date.range.start?.getDay() === rule.dayOfWeek,
);
if (canApplyMixedStrategy) {
this.push(
new SelectedDate(DayType.CP, new DateRange<Date>(oneDayBefore, oneDayBefore)),
);
this.push(
new SelectedDate(DayType.CP, new DateRange<Date>(oneDayAfter, oneDayAfter)),
);
const heuristic = this.computeHeuristics();
strategies.push({
apply: () => {
this._datesSelected = [...tempDatesBackup];
this.push(
new SelectedDate(
DayType.CP,
new DateRange<Date>(oneDayBefore, oneDayBefore),
),
);
this.push(
new SelectedDate(
DayType.CP,
new DateRange<Date>(oneDayAfter, oneDayAfter),
),
);
consume2CpDaysForDate(oneDayAfter);
addOtherDayReward();
},
heuristic: heuristic,
});
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
}
for (const holiday of matchingHolidays) {
if (!holiday.range.start) continue;
const holidayDate = new Date(holiday.range.start);
// Apply the strategy with the best heuristic score
if (strategies.length > 0) {
const bestStrategy = strategies.reduce((best, current) =>
current.heuristic > best.heuristic ? current : best,
);
bestStrategy.apply();
const N = rule.placeDays;
if (N <= 0) continue;
const placementOptions: Date[][] = [];
if (rule.placement === 'before' || rule.placement === 'auto') {
placementOptions.push(getWorkingDays(holidayDate, -1, N));
}
if (rule.placement === 'after' || rule.placement === 'auto') {
placementOptions.push(getWorkingDays(holidayDate, 1, N));
}
if (rule.placement === 'around' || rule.placement === 'auto') {
const beforeCount = Math.ceil(N / 2);
const afterCount = Math.floor(N / 2);
const aroundDates = [
...getWorkingDays(holidayDate, -1, beforeCount),
...getWorkingDays(holidayDate, 1, afterCount),
];
placementOptions.push(aroundDates);
}
const strategies: { apply: () => void; heuristic: number }[] = [];
const tempDatesBackup = [...this._datesSelected];
for (const dates of placementOptions) {
const allValid = dates.every(
(d) =>
d.getTime() >= startLimit &&
d.getTime() <= endLimit &&
!this.isSelected(d),
);
if (!allValid) continue;
const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())));
if (!checkAvailableDays(N, rule.placeType, maxDate)) continue;
const simulatedDays = vacationDays.map((vd) => ({ ...vd }));
const datesWithTypes: { date: Date; type: DayType }[] = [];
for (const d of dates) {
const sorted = simulatedDays
.filter(
(vd) =>
(rule.placeType === SelectableDayType.ANY ||
vd.type === rule.placeType) &&
vd.numberOfDays > 0 &&
vd.expiryDate.getTime() >= maxDate.getTime(),
)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());
let chosenType = (rule.placeType === SelectableDayType.ANY
? SelectableDayType.CP
: rule.placeType) as unknown as DayType;
if (sorted.length > 0) {
sorted[0].numberOfDays--;
chosenType = sorted[0].type as unknown as DayType;
}
datesWithTypes.push({ date: d, type: chosenType });
}
for (const dt of datesWithTypes) {
this.push(new SelectedDate(dt.type, new DateRange<Date>(dt.date, dt.date)));
}
const heuristic = this.computeHeuristics();
strategies.push({
apply: () => {
this._datesSelected = [...tempDatesBackup];
for (const dt of datesWithTypes) {
this.push(
new SelectedDate(
dt.type,
new DateRange<Date>(dt.date, dt.date),
),
);
}
consumeDays(N, rule.placeType, maxDate);
addRewardDays(rule.rewardDays, rule.rewardType);
},
heuristic: heuristic,
});
this._datesSelected = [...tempDatesBackup];
this.datesSelected = this._datesSelected.slice();
this.grouping();
}
if (strategies.length > 0) {
const bestStrategy = strategies.reduce((best, current) =>
current.heuristic > best.heuristic ? current : best,
);
bestStrategy.apply();
} else {
console.warn(
`Could not apply If-Then Rule "${rule.name}" for holiday on ${holidayDate.toLocaleDateString()}. Candidates invalid or insufficient days.`,
);
}
}
}
}
@@ -827,11 +943,15 @@ export class SelectedDates implements SelectedDateInterface {
lookForVacation(
vacDay: VacDay,
calendarService: CalendarService,
startDate: Date,
endDate: Date,
): { apply: () => void; heuristic: number } {
const daysOfYear = calendarService.monthsInAYearFromNow();
const daysOfYear = calendarService.monthsInPeriod(startDate, endDate);
const now = new Date();
now.setHours(0, 0, 0, 0);
const nowTime = now.getTime();
const startLimit = Math.max(nowTime, startDate.getTime());
const endLimit = endDate.getTime();
const expiryTime = vacDay.expiryDate.getTime();
// Preprocess: filter out past days, already selected days, and days after expiry date
@@ -842,7 +962,12 @@ export class SelectedDates implements SelectedDateInterface {
if (day) {
day.setHours(0, 0, 0, 0);
const dayTime = day.getTime();
if (dayTime >= nowTime && dayTime <= expiryTime && !this.isSelected(day)) {
if (
dayTime >= startLimit &&
dayTime <= endLimit &&
dayTime <= expiryTime &&
!this.isSelected(day)
) {
candidateDays.push(day);
}
}
@@ -859,7 +984,6 @@ 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) {
@@ -876,7 +1000,6 @@ export class SelectedDates implements SelectedDateInterface {
// Track only if better than current best
if (heuristic > bestHeuristic) {
bestHeuristic = heuristic;
daysWithBestHeuristic = 1;
const capturedDay = day;
bestStrategy = {
apply: () => {
@@ -891,25 +1014,9 @@ 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();
@@ -18,7 +18,6 @@ describe('CalendarSettingsService', () => {
it('should initialize with default values', () => {
expect(service.get('showWeekNumbers')()).toBe(false);
expect(service.get('samediMalin')()).toBe(true);
});
it('should allow updating showWeekNumbers', () => {
@@ -26,20 +25,13 @@ describe('CalendarSettingsService', () => {
expect(service.get('showWeekNumbers')()).toBe(true);
});
it('should allow updating samediMalin', () => {
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.length).toBe(1);
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');
});
});
@@ -20,13 +20,6 @@ export class CalendarSettingsService {
description: 'Afficher les numéros de semaine',
},
],
[
'samediMalin',
{
defaultValue: true,
description: 'Activer le Samedi Malin',
},
],
]);
private readonly signals = new Map<string, WritableSignal<boolean>>();
+98
View File
@@ -0,0 +1,98 @@
import { TestBed } from '@angular/core/testing';
import { RuleService, RuleType, IfThenRule, TimeWindowRule } from './rule-service';
import { SelectableDayType } from './calendar-service';
describe('RuleService', () => {
let service: RuleService;
beforeEach(() => {
// Clear localStorage before each test to ensure a clean state
localStorage.clear();
TestBed.configureTestingModule({});
service = TestBed.inject(RuleService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should initialize with default rules', () => {
const rules = service.rules;
expect(rules.length).toBe(2);
expect(rules[0].id).toBe('default-samedi-malin');
expect(rules[1].id).toBe('default-christmas');
});
it('should parse dates correctly for TimeWindowRules', () => {
const rules = service.rules;
const christmasRule = rules.find((r) => r.type === RuleType.TIME_WINDOW) as TimeWindowRule;
expect(christmasRule).toBeDefined();
expect(christmasRule.startDate instanceof Date).toBe(true);
expect(christmasRule.endDate instanceof Date).toBe(true);
});
it('should add a new rule', () => {
const newRule: IfThenRule = {
id: 'test-rule',
name: 'Test Rule',
type: RuleType.IF_THEN,
enabled: true,
dayOfWeek: 1, // Monday
placeDays: 1,
placeType: SelectableDayType.RTT,
placement: 'after',
rewardDays: 0,
rewardType: SelectableDayType.OTHER,
};
service.addRule(newRule);
const rules = service.rules;
expect(rules.length).toBe(3);
expect(rules.find((r) => r.id === 'test-rule')).toBeDefined();
});
it('should update an existing rule', () => {
const rules = service.rules;
const targetRule = { ...rules[0] } as IfThenRule;
targetRule.name = 'Updated Name';
service.updateRule(targetRule);
const updatedRules = service.rules;
const updated = updatedRules.find((r) => r.id === targetRule.id);
expect(updated?.name).toBe('Updated Name');
});
it('should delete a rule', () => {
const rules = service.rules;
const idToDelete = rules[0].id;
service.deleteRule(idToDelete);
const updatedRules = service.rules;
expect(updatedRules.length).toBe(1);
expect(updatedRules.find((r) => r.id === idToDelete)).toBeUndefined();
});
it('should toggle rule enabled state', () => {
const targetId = service.rules[0].id;
const initialState = service.rules[0].enabled;
service.toggleRule(targetId);
expect(service.rules.find((r) => r.id === targetId)?.enabled).toBe(!initialState);
});
it('should reorder rules correctly', () => {
const rulesBefore = service.rules;
const firstRuleId = rulesBefore[0].id;
const secondRuleId = rulesBefore[1].id;
// Move first rule to second position
service.reorderRules(0, 1);
const rulesAfter = service.rules;
expect(rulesAfter[0].id).toBe(secondRuleId);
expect(rulesAfter[1].id).toBe(firstRuleId);
});
});
+139
View File
@@ -0,0 +1,139 @@
import { Injectable, WritableSignal, Injector, inject } from '@angular/core';
import { SelectableDayType } from './calendar-service';
import { storedSignal } from '../shared/utils/stored-signal';
export enum RuleType {
IF_THEN = 'if_then',
TIME_WINDOW = 'time_window',
}
export interface BaseRule {
id: string;
name: string;
type: RuleType;
enabled: boolean;
}
export type PlacementStrategy = 'before' | 'after' | 'around' | 'auto';
export interface IfThenRule extends BaseRule {
type: RuleType.IF_THEN;
dayOfWeek: number; // 0-6, where 0 is Sunday, 6 is Saturday
placeDays: number;
placeType: SelectableDayType;
placement: PlacementStrategy;
rewardDays: number;
rewardType: SelectableDayType;
}
export interface TimeWindowRule extends BaseRule {
type: RuleType.TIME_WINDOW;
startDate: Date;
endDate: Date;
placeDays: number;
placeType: SelectableDayType;
}
export type VacationRule = IfThenRule | TimeWindowRule;
interface SerializedRule {
id: string;
name: string;
type: RuleType;
enabled: boolean;
dayOfWeek?: number;
placeDays: number;
placeType: SelectableDayType;
placement?: PlacementStrategy;
rewardDays?: number;
rewardType?: SelectableDayType;
startDate?: string | Date;
endDate?: string | Date;
}
@Injectable({
providedIn: 'root',
})
export class RuleService {
private readonly injector = inject(Injector);
private readonly defaultRules: VacationRule[] = [
{
id: 'default-samedi-malin',
name: 'Samedi Malin',
type: RuleType.IF_THEN,
enabled: true,
dayOfWeek: 6, // Saturday
placeDays: 2,
placeType: SelectableDayType.CP,
placement: 'auto',
rewardDays: 1,
rewardType: SelectableDayType.OTHER,
},
{
id: 'default-christmas',
name: 'Vacances de Noël',
type: RuleType.TIME_WINDOW,
enabled: true,
startDate: new Date(new Date().getFullYear(), 11, 20), // Dec 20
endDate: new Date(new Date().getFullYear() + 1, 0, 3), // Jan 3 next year
placeDays: 5,
placeType: SelectableDayType.ANY,
},
];
private rawRulesSignal: WritableSignal<SerializedRule[]>;
constructor() {
this.rawRulesSignal = storedSignal<SerializedRule[]>(
'vacationRules',
this.defaultRules as unknown as SerializedRule[],
this.injector,
);
}
public get rules(): VacationRule[] {
const raw = this.rawRulesSignal();
return raw.map((rule) => {
if (rule.type === RuleType.TIME_WINDOW) {
return {
...rule,
startDate: new Date(rule.startDate as string),
endDate: new Date(rule.endDate as string),
} as TimeWindowRule;
}
return rule as IfThenRule;
});
}
public addRule(rule: VacationRule): void {
this.rawRulesSignal.update((rules) => [...rules, rule as unknown as SerializedRule]);
}
public updateRule(updatedRule: VacationRule): void {
this.rawRulesSignal.update((rules) =>
rules.map((rule) =>
rule.id === updatedRule.id ? (updatedRule as unknown as SerializedRule) : rule,
),
);
}
public deleteRule(id: string): void {
this.rawRulesSignal.update((rules) => rules.filter((rule) => rule.id !== id));
}
public toggleRule(id: string): void {
this.rawRulesSignal.update((rules) =>
rules.map((rule) => (rule.id === id ? { ...rule, enabled: !rule.enabled } : rule)),
);
}
public reorderRules(previousIndex: number, currentIndex: number): void {
this.rawRulesSignal.update((rules) => {
const updatedRules = [...rules];
const [movedRule] = updatedRules.splice(previousIndex, 1);
updatedRules.splice(currentIndex, 0, movedRule);
return updatedRules;
});
}
}
@@ -0,0 +1,67 @@
import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { TranslationService } from './translation-service';
describe('TranslationService', () => {
let service: TranslationService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()],
});
service = TestBed.inject(TranslationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get correct locale', () => {
expect(service.locale()).toBeDefined();
});
it('should set language if available', () => {
service.setLanguage('en');
expect(service.language()).toBe('en');
service.setLanguage('invalid-lang');
expect(service.language()).toBe('en'); // Should not change
});
it('should get language name', () => {
const enName = service.getLanguageName('en');
expect(enName).toBe('English');
});
it('should get language name fallback', () => {
const origIntl = global.Intl;
Object.defineProperty(global, 'Intl', {
value: {
DisplayNames: class {
of() {
return undefined;
}
},
},
writable: true,
});
expect(service.getLanguageName('test')).toBe('TEST');
Object.defineProperty(global, 'Intl', {
value: origIntl,
writable: true,
});
});
it('should get day type label', () => {
expect(service.getDayTypeLabel('cp')).toBeDefined();
expect(service.getDayTypeLabel('rtt')).toBeDefined();
expect(service.getDayTypeLabel('weekend')).toBeDefined();
expect(service.getDayTypeLabel('closed-day')).toBeDefined();
expect(service.getDayTypeLabel('other')).toBeDefined();
expect(service.getDayTypeLabel('today')).toBeDefined();
expect(service.getDayTypeLabel('any')).toBeDefined();
expect(service.getDayTypeLabel('unknown-type')).toBe('unknown-type');
});
});
+182
View File
@@ -0,0 +1,182 @@
/// <reference types="vite/client" />
import { Injectable, inject, Injector, computed } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
export interface AppTranslations {
locale: string;
app: {
title: string;
description: string;
};
userInput: {
addVacationDay: string;
calculate: string;
remainingDaysWarning: string;
remainingDaysManualPlace: string;
chooseDate: string;
daySuffix: string;
daysSuffix: string;
expires: string;
choosePeriod: string;
startDate: string;
endDate: string;
};
dayTypes: {
weekend: string;
closedDay: string;
rtt: string;
cp: string;
other: string;
today: string;
any: string;
};
settings: {
title: string;
description: string;
showWeekNumbers: string;
close: string;
selectLanguage: string;
};
rules: {
title: string;
subtitle: string;
priorityInfo: string;
ifThenTemplate: string;
timeWindowTemplate: string;
editRule: string;
addRule: string;
place: string;
ruleName: string;
holidayFalls: string;
days: string;
type: string;
position: string;
getBonus: string;
startDate: string;
endDate: string;
cancel: string;
save: string;
add: string;
addRuleButton: string;
newIfThenRule: string;
newTimeWindowRule: string;
defaultNewRuleName: string;
defaultNewPeriodName: string;
daysOfWeek: {
1: string;
2: string;
3: string;
4: string;
5: string;
6: string;
0: string;
};
placement: {
before: string;
after: string;
around: string;
auto: string;
};
};
loading: string;
weekdaysShort: string[];
}
import { translationsMap, availableLanguagesList } from '../i18n-registry';
@Injectable({
providedIn: 'root',
})
export class TranslationService {
private readonly injector = inject(Injector);
public readonly availableLanguages = availableLanguagesList;
// Detect browser language, fallback to the first available language
private getBrowserLanguage(): string {
if (typeof navigator !== 'undefined' && navigator.language) {
const primary = navigator.language.split('-')[0].toLowerCase();
if (this.availableLanguages.includes(primary)) {
return primary;
}
}
return this.availableLanguages[0] || '';
}
public readonly language = storedSignal<string>(
'app.language',
this.getBrowserLanguage(),
this.injector,
);
public readonly tr = computed<AppTranslations>(() => {
const currentLang = this.language();
const fallbackLang = this.availableLanguages[0] || '';
return (
(translationsMap[currentLang] as AppTranslations) ||
(translationsMap[fallbackLang] as AppTranslations) ||
({} as AppTranslations)
);
});
public readonly locale = computed<string>(() => {
const tr = this.tr();
if (tr && tr.locale) {
return tr.locale;
}
const lang = this.language();
if (lang) {
return `${lang}-${lang.toUpperCase()}`;
}
const fallbackLang = this.availableLanguages[0];
if (fallbackLang) {
return `${fallbackLang}-${fallbackLang.toUpperCase()}`;
}
return '';
});
public setLanguage(lang: string): void {
if (this.availableLanguages.includes(lang)) {
this.language.set(lang);
}
}
public getLanguageName(langCode: string): string {
try {
// Using native Intl.DisplayNames to get name of language in its own language
const displayNames = new Intl.DisplayNames([langCode], { type: 'language' });
const name = displayNames.of(langCode);
if (name) {
return name.charAt(0).toUpperCase() + name.slice(1);
}
} catch {
// Fallback to uppercase language code
}
return langCode.toUpperCase();
}
public getDayTypeLabel(type: string): string {
const tr = this.tr();
if (!tr || !tr.dayTypes) {
return type;
}
switch (type) {
case 'weekend':
return tr.dayTypes.weekend || type;
case 'closed-day':
return tr.dayTypes.closedDay || type;
case 'rtt':
return tr.dayTypes.rtt || type;
case 'cp':
return tr.dayTypes.cp || type;
case 'other':
return tr.dayTypes.other || type;
case 'today':
return tr.dayTypes.today || type;
case 'any':
return tr.dayTypes.any || type;
default:
return type;
}
}
}
@@ -54,4 +54,24 @@ describe('UserInputService', () => {
expect(newDay.numberOfDays).toBe(1);
expect(newDay.expiryDate).toBeInstanceOf(Date);
});
it('should set start date', () => {
const newDate = new Date();
newDate.setFullYear(newDate.getFullYear() + 2);
service.setStartDate(newDate);
expect(service.startDate().getFullYear()).toBe(newDate.getFullYear());
});
it('should set end date', () => {
const newDate = new Date();
newDate.setFullYear(newDate.getFullYear() + 3);
service.setEndDate(newDate);
expect(service.endDate().getFullYear()).toBe(newDate.getFullYear());
});
it('should request computation', () => {
const initialValue = service.computeTriggerSignal();
service.requestComputation();
expect(service.computeTriggerSignal()).toBe(initialValue + 1);
});
});
+50 -6
View File
@@ -1,5 +1,6 @@
import { Injectable, signal } from '@angular/core';
import { Injectable, signal, computed, inject, Injector } from '@angular/core';
import { SelectableDayType, VacDay } from './calendar-service';
import { storedSignal } from '../shared/utils/stored-signal';
@Injectable({
providedIn: 'root',
@@ -15,20 +16,19 @@ export class UserInputService {
id: crypto.randomUUID(),
type: SelectableDayType.CP,
numberOfDays: this.getRandomNumber(),
// today + 365 days
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
expiryDate: UserInputService.getOneYearFromTodayMidnight(),
},
{
id: crypto.randomUUID(),
type: SelectableDayType.RTT,
numberOfDays: this.getRandomNumber(),
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
expiryDate: UserInputService.getOneYearFromTodayMidnight(),
},
{
id: crypto.randomUUID(),
type: SelectableDayType.OTHER,
numberOfDays: this.getRandomNumber(),
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
expiryDate: UserInputService.getOneYearFromTodayMidnight(),
},
];
}
@@ -38,7 +38,7 @@ export class UserInputService {
id: crypto.randomUUID(),
type: SelectableDayType.CP,
numberOfDays: 1,
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
expiryDate: this.endDate(),
};
}
@@ -46,6 +46,50 @@ export class UserInputService {
readonly remainingVacationDaysSignal = signal<VacDay[]>([]);
readonly computeTriggerSignal = signal(0);
private readonly injector = inject(Injector);
private static getTodayMidnight(): Date {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d;
}
private static getOneYearFromTodayMidnight(): Date {
const d = new Date();
d.setFullYear(d.getFullYear() + 1);
d.setHours(0, 0, 0, 0);
return d;
}
readonly startDateString = storedSignal<string>(
'timeframe.startDate',
UserInputService.getTodayMidnight().toISOString(),
this.injector,
);
readonly endDateString = storedSignal<string>(
'timeframe.endDate',
UserInputService.getOneYearFromTodayMidnight().toISOString(),
this.injector,
);
readonly startDate = computed(() => new Date(this.startDateString()));
readonly endDate = computed(() => new Date(this.endDateString()));
public setStartDate(date: Date | null): void {
if (!date) return;
const d = new Date(date);
d.setHours(0, 0, 0, 0);
this.startDateString.set(d.toISOString());
}
public setEndDate(date: Date | null): void {
if (!date) return;
const d = new Date(date);
d.setHours(0, 0, 0, 0);
this.endDateString.set(d.toISOString());
}
public requestComputation(): void {
this.computeTriggerSignal.update((count) => count + 1);
}
@@ -0,0 +1,58 @@
import { TestBed } from '@angular/core/testing';
import { Injector, provideZonelessChangeDetection } from '@angular/core';
import { storedSignal } from './stored-signal';
import { vi } from 'vitest';
describe('storedSignal', () => {
let injector: Injector;
const testKey = 'test.key';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()],
});
injector = TestBed.inject(Injector);
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});
it('should return initial value when no local storage value exists', () => {
const signal = storedSignal(testKey, 'initial', injector);
expect(signal()).toBe('initial');
});
it('should read from local storage if available', () => {
localStorage.setItem(testKey, JSON.stringify('stored'));
const signal = storedSignal(testKey, 'initial', injector);
expect(signal()).toBe('stored');
});
it('should fallback to initial value and remove key on invalid JSON', () => {
localStorage.setItem(testKey, 'invalid-json');
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const signal = storedSignal(testKey, 'initial', injector);
expect(signal()).toBe('initial');
expect(consoleSpy).toHaveBeenCalled();
expect(localStorage.getItem(testKey)).toBeNull();
});
it('should handle missing local storage gracefully', () => {
const originalLocalStorage = global.localStorage;
Object.defineProperty(global, 'localStorage', { value: undefined, writable: true });
const signal = storedSignal(testKey, 'initial', injector);
expect(signal()).toBe('initial');
// Restore
Object.defineProperty(global, 'localStorage', {
value: originalLocalStorage,
writable: true,
});
});
});
+79
View File
@@ -0,0 +1,79 @@
{
"locale": "de-DE",
"app": {
"title": "vac-optimizer",
"description": "Optimieren Sie Ihre Urlaubstage basierend auf Feiertagen und Wochenenden!"
},
"userInput": {
"addVacationDay": "Urlaubstag hinzufügen",
"calculate": "Berechnen",
"remainingDaysWarning": "{count} nicht platziert:",
"remainingDaysManualPlace": "Bitte manuell platzieren.",
"chooseDate": "Datum auswählen",
"daySuffix": "Tag",
"daysSuffix": "Tage",
"expires": "abl.",
"choosePeriod": "Optimierungszeitraum",
"startDate": "Startdatum",
"endDate": "Enddatum"
},
"dayTypes": {
"weekend": "Wochenende",
"closedDay": "Feiertag",
"rtt": "RTT",
"cp": "CP",
"other": "Andere",
"today": "Heute",
"any": "Beliebig"
},
"settings": {
"title": "Einstellungen",
"description": "Einstellungen anpassen",
"showWeekNumbers": "Wochennummern anzeigen",
"close": "Schließen",
"selectLanguage": "Sprache auswählen"
},
"rules": {
"title": "Regeln",
"subtitle": "Urlaubsregeln konfigurieren",
"priorityInfo": "Regeln werden von oben nach unten angewendet",
"ifThenTemplate": "Wenn {type} dann {placement}",
"timeWindowTemplate": "Zeitfenster {startDate} bis {endDate}",
"editRule": "Regel bearbeiten",
"addRule": "Regel hinzufügen",
"place": "platzieren",
"ruleName": "Regelname",
"holidayFalls": "Feiertag fällt auf",
"days": "Tage",
"type": "Typ",
"position": "Position",
"getBonus": "Bonus erhalten",
"startDate": "Startdatum",
"endDate": "Enddatum",
"cancel": "Abbrechen",
"save": "Speichern",
"add": "Hinzufügen",
"addRuleButton": "Regel hinzufügen",
"newIfThenRule": "Neue Wenn-Dann-Regel",
"newTimeWindowRule": "Neue Zeitfenster-Regel",
"defaultNewRuleName": "Neue Regel",
"defaultNewPeriodName": "Neuer Zeitraum",
"daysOfWeek": {
"1": "Montag",
"2": "Dienstag",
"3": "Mittwoch",
"4": "Donnerstag",
"5": "Freitag",
"6": "Samstag",
"0": "Sonntag"
},
"placement": {
"before": "davor",
"after": "danach",
"around": "darum herum",
"auto": "automatisch"
}
},
"loading": "Wird geladen...",
"weekdaysShort": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]
}
+79
View File
@@ -0,0 +1,79 @@
{
"locale": "en-GB",
"app": {
"title": "vac-optimizer",
"description": "Optimize your vacation days based on holidays and weekends!"
},
"userInput": {
"addVacationDay": "Add a vacation day",
"calculate": "Calculate",
"remainingDaysWarning": "{count} vacation day{plural} not placed:",
"remainingDaysManualPlace": "Please place them manually.",
"chooseDate": "Choose a date",
"daySuffix": "day",
"daysSuffix": "days",
"expires": "exp.",
"choosePeriod": "Optimization Period",
"startDate": "Start date",
"endDate": "End date"
},
"dayTypes": {
"weekend": "Weekend",
"closedDay": "Public Holiday",
"rtt": "RTT",
"cp": "Paid Leave",
"other": "Other",
"today": "Today",
"any": "Any"
},
"settings": {
"title": "Calendar Settings",
"description": "Configure your calendar settings here.",
"showWeekNumbers": "Show week numbers",
"close": "Close",
"selectLanguage": "Language"
},
"rules": {
"title": "Custom Rules",
"subtitle": "Manage optimization rules",
"priorityInfo": "The order of rules defines their execution priority (from top to bottom). Drag and drop to reorder.",
"ifThenTemplate": "If a holiday falls on a <strong>{day}</strong>, place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> <strong>{placement}</strong> and get <strong>{rewardDays}</strong> day(s) <strong>{rewardType}</strong> as a bonus.",
"timeWindowTemplate": "Place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> between <strong>{startDate}</strong> and <strong>{endDate}</strong>.",
"editRule": "Edit rule",
"addRule": "Add a rule",
"place": "Place",
"ruleName": "Rule name",
"holidayFalls": "Holiday falls on a",
"days": "Days",
"type": "Type",
"position": "Position",
"getBonus": "Get as bonus",
"startDate": "Start date",
"endDate": "End date",
"cancel": "Cancel",
"save": "Save",
"add": "Add",
"addRuleButton": "Add Rule",
"newIfThenRule": "New \"If ... Then\" Rule",
"newTimeWindowRule": "New Imposed Period",
"defaultNewRuleName": "New Rule",
"defaultNewPeriodName": "New Period",
"daysOfWeek": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"0": "Sunday"
},
"placement": {
"before": "Before",
"after": "After",
"around": "Around",
"auto": "Auto (Best choice)"
}
},
"loading": "Calculating...",
"weekdaysShort": ["M", "T", "W", "T", "F", "S", "S"]
}
+79
View File
@@ -0,0 +1,79 @@
{
"locale": "es-ES",
"app": {
"title": "vac-optimizer",
"description": "¡Optimice sus días de vacaciones según los días festivos y los fines de semana!"
},
"userInput": {
"addVacationDay": "Añadir un día de vacaciones",
"calculate": "Calcular",
"remainingDaysWarning": "{count} día{plural} de vacaciones no colocado{plural}:",
"remainingDaysManualPlace": "Por favor, colóquelos manualmente.",
"chooseDate": "Elija una fecha",
"daySuffix": "día",
"daysSuffix": "días",
"expires": "exp.",
"choosePeriod": "Período de optimización",
"startDate": "Fecha de inicio",
"endDate": "Fecha de fin"
},
"dayTypes": {
"weekend": "Fin de semana",
"closedDay": "Día festivo",
"rtt": "RTT",
"cp": "Vacaciones pagadas",
"other": "Otro",
"today": "Hoy",
"any": "Cualquier"
},
"settings": {
"title": "Ajustes del calendario",
"description": "Configure sus ajustes del calendario aquí.",
"showWeekNumbers": "Mostrar números de semana",
"close": "Cerrar",
"selectLanguage": "Idioma"
},
"rules": {
"title": "Reglas personalizadas",
"subtitle": "Gestionar reglas de optimización",
"priorityInfo": "El orden de las reglas define su prioridad de ejecución (de arriba a abajo). Arrastre y suelte para reorganizar.",
"ifThenTemplate": "Si un día festivo cae en <strong>{day}</strong>, colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> <strong>{placement}</strong> y obtener <strong>{rewardDays}</strong> día(s) <strong>{rewardType}</strong> de bonificación.",
"timeWindowTemplate": "Colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> entre el <strong>{startDate}</strong> y el <strong>{endDate}</strong>.",
"editRule": "Editar regla",
"addRule": "Añadir una regla",
"place": "Colocar",
"ruleName": "Nombre de la regla",
"holidayFalls": "Día festivo cae en",
"days": "Días",
"type": "Tipo",
"position": "Posición",
"getBonus": "Obtener en bonificación",
"startDate": "Fecha de inicio",
"endDate": "Fecha de fin",
"cancel": "Cancelar",
"save": "Guardar",
"add": "Añadir",
"addRuleButton": "Añadir",
"newIfThenRule": "Nueva regla \"Si ... Entonces\"",
"newTimeWindowRule": "Nuevo período impuesto",
"defaultNewRuleName": "Nueva regla",
"defaultNewPeriodName": "Nuevo período",
"daysOfWeek": {
"1": "Lunes",
"2": "Martes",
"3": "Miércoles",
"4": "Jueves",
"5": "Viernes",
"6": "Sábado",
"0": "Domingo"
},
"placement": {
"before": "Antes",
"after": "Después",
"around": "Alrededor",
"auto": "Auto (Mejor opción)"
}
},
"loading": "Calculando...",
"weekdaysShort": ["L", "M", "M", "J", "V", "S", "D"]
}
+79
View File
@@ -0,0 +1,79 @@
{
"locale": "fr-FR",
"app": {
"title": "vac-optimizer",
"description": "Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !"
},
"userInput": {
"addVacationDay": "Ajouter un jour de vacances",
"calculate": "Calculer",
"remainingDaysWarning": "{count} jour{plural} de vacances non placé{plural}:",
"remainingDaysManualPlace": "Veuillez les placer manuellement.",
"chooseDate": "Choisissez une date",
"daySuffix": "jour",
"daysSuffix": "jours",
"expires": "exp.",
"choosePeriod": "Période d'optimisation",
"startDate": "Date de début",
"endDate": "Date de fin"
},
"dayTypes": {
"weekend": "Weekend",
"closedDay": "Jour Férié",
"rtt": "RTT",
"cp": "Congé Payé",
"other": "Autre",
"today": "Aujourd'hui",
"any": "N'importe"
},
"settings": {
"title": "Paramètres du calendrier",
"description": "Configurez vos paramètres de calendrier ici.",
"showWeekNumbers": "Afficher les numéros de semaine",
"close": "Fermer",
"selectLanguage": "Langue"
},
"rules": {
"title": "Règles Personnalisées",
"subtitle": "Gérer les règles d'optimisation",
"priorityInfo": "L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez pour réorganiser.",
"ifThenTemplate": "Si un jour férié tombe un <strong>{day}</strong>, placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> <strong>{placement}</strong> et obtenir <strong>{rewardDays}</strong> jour(s) <strong>{rewardType}</strong> en bonus.",
"timeWindowTemplate": "Placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> entre le <strong>{startDate}</strong> et le <strong>{endDate}</strong>.",
"editRule": "Modifier la règle",
"addRule": "Ajouter une règle",
"place": "Placer",
"ruleName": "Nom de la règle",
"holidayFalls": "Jour férié tombe un",
"days": "Jours",
"type": "Type",
"position": "Position",
"getBonus": "Obtenir en bonus",
"startDate": "Date de début",
"endDate": "Date de fin",
"cancel": "Annuler",
"save": "Enregistrer",
"add": "Ajouter",
"addRuleButton": "Ajouter",
"newIfThenRule": "Nouvelle Règle \"Si ... Alors\"",
"newTimeWindowRule": "Nouvelle Période Imposée",
"defaultNewRuleName": "Nouvelle Règle",
"defaultNewPeriodName": "Nouvelle Période",
"daysOfWeek": {
"1": "Lundi",
"2": "Mardi",
"3": "Mercredi",
"4": "Jeudi",
"5": "Vendredi",
"6": "Samedi",
"0": "Dimanche"
},
"placement": {
"before": "Avant",
"after": "Après",
"around": "Autour",
"auto": "Auto (Meilleur choix)"
}
},
"loading": "Calcul en cours...",
"weekdaysShort": ["L", "M", "M", "J", "V", "S", "D"]
}
+21
View File
@@ -0,0 +1,21 @@
import '@angular/compiler';
import { vi } from 'vitest';
import '@analogjs/vitest-angular/setup-snapshots';
import '@analogjs/vitest-angular/setup-serializers';
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
setupTestBed();
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
+4 -4
View File
@@ -1,10 +1,10 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
"types": ["vitest/globals"],
"target": "es2022"
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts"],
"files": ["src/test-setup.ts"]
}
+27
View File
@@ -0,0 +1,27 @@
/// <reference types="vitest" />
import angular from '@analogjs/vite-plugin-angular';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['**/*.spec.ts'],
reporters: ['default'],
pool: 'forks',
coverage: {
provider: 'istanbul',
reporter: ['text', 'html', 'cobertura'],
},
},
define: {
'import.meta.vitest': mode !== 'production',
},
};
});