Compare commits

..
Author SHA1 Message Date
dependabot[bot] 3361642b26 Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 20:10:57 +00:00
dependabot[bot] fc829ad0af Bump the angular group across 1 directory with 10 updates (#113)
Bumps the angular group with 10 updates in the / directory:

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



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

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

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

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

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/platform-server"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  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-01 20:15:55 +00:00
dependabot[bot] b0266e31ea Bump @angular/ssr from 21.1.4 to 21.2.0 (#106)
Bumps [@angular/ssr](https://github.com/angular/angular-cli) from 21.1.4 to 21.2.0.
- [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.1.4...v21.2.0)

---
updated-dependencies:
- dependency-name: "@angular/ssr"
  dependency-version: 21.2.0
  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-01 20:13:03 +00:00
dependabot[bot] 492cd3a34c Bump hono from 4.12.1 to 4.12.3 (#107)
Bumps [hono](https://github.com/honojs/hono) from 4.12.1 to 4.12.3.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.1...v4.12.3)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.3
  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-01 20:13:01 +00:00
dependabot[bot] 96053b7f5d Bump aquasecurity/trivy-action from 0.34.0 to 0.34.1 (#102)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.34.0 to 0.34.1.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.0...0.34.1)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.1
  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-01 20:12:42 +00:00
dependabot[bot] 2013ac5256 Bump minimatch from 3.1.2 to 3.1.5 (#109)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  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-01 20:12:08 +00:00
CopilotandTHoguet 9142cf56cf Add Dependabot auto-merge workflow for minor and patch updates (#112)
* Initial plan

* add Dependabot auto-merge workflow for minor and patch updates

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2026-03-01 21:08:03 +01:00
dependabot[bot] c05c08f994 Bump rollup from 4.58.0 to 4.59.0 (#110)
Bumps [rollup](https://github.com/rollup/rollup) from 4.58.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.58.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.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-01 21:00:20 +01:00
dependabot[bot] 0aab8e1bb9 Bump the development-dependencies group with 4 updates (#104)
Bumps the development-dependencies group with 4 updates: [@angular/build](https://github.com/angular/angular-cli), [@angular/cli](https://github.com/angular/angular-cli), [jasmine-core](https://github.com/jasmine/jasmine) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@angular/build` from 21.1.4 to 21.1.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.1.4...v21.1.5)

Updates `@angular/cli` from 21.1.4 to 21.1.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.1.4...v21.1.5)

Updates `jasmine-core` from 6.0.1 to 6.1.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.0.1...v6.1.0)

Updates `typescript-eslint` from 8.56.0 to 8.56.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.56.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@angular/build"
  dependency-version: 21.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 21.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: jasmine-core
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.56.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-02-27 21:45:43 +01:00
nessar 7127744454 fix: correct string concatenation in getClasses method 2026-02-21 17:36:50 +01:00
nessar 26d8bf5184 chore(deps): update Angular and related dependencies to version 21.1.x
- Updated Angular packages to version 21.1.5 for core modules, forms, and router.
- Updated Angular CDK, build, CLI, and compiler-cli to version 21.1.4.
- Updated express to version 5.2.1 and rxjs to version 7.8.2.
- Updated ESLint and related packages to the latest versions.
- Updated types for express and jasmine to the latest versions.
- Updated prettier and typescript to the latest versions.
- Updated karma and its plugins to the latest versions.
2026-02-21 17:29:20 +01:00
dependabot[bot] 9ff7cb503a Bump @types/node from 24.10.4 to 25.0.6 (#81)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.4 to 25.0.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 16:34:29 +01:00
dependabot[bot] 420dacf379 Bump lodash from 4.17.21 to 4.17.23 (#87)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 16:34:20 +01:00
dependabot[bot] a213caaf92 Bump date-holidays from 3.26.6 to 3.26.8 (#92)
Bumps [date-holidays](https://github.com/commenthol/date-holidays) from 3.26.6 to 3.26.8.
- [Changelog](https://github.com/commenthol/date-holidays/blob/master/CHANGELOG.md)
- [Commits](https://github.com/commenthol/date-holidays/compare/v3.26.6...v3.26.8)

---
updated-dependencies:
- dependency-name: date-holidays
  dependency-version: 3.26.8
  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-02-21 16:34:09 +01:00
dependabot[bot] cbdaa8991c Bump qs from 6.14.1 to 6.14.2 (#95)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 16:33:59 +01:00
dependabot[bot] 2caab553aa Bump aquasecurity/trivy-action from 0.33.1 to 0.34.0 (#96)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.33.1 to 0.34.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.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-02-21 16:33:55 +01:00
dependabot[bot] 7c1538ee66 Bump aquasecurity/trivy-action (#97)
Bumps the github_actions group with 1 update in the /.github/workflows directory: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).


Updates `aquasecurity/trivy-action` from 0.33.1 to 0.34.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.0
  dependency-type: direct:production
  dependency-group: github_actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-21 16:33:51 +01:00
dependabot[bot] 1ff20d4fcd Bump tar from 7.5.2 to 7.5.9 (#98)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.2 to 7.5.9.
- [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.2...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.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-02-21 16:33:47 +01:00
dependabot[bot] 2435f3f01b Bump @angular/core from 21.0.6 to 21.1.5 (#99)
Bumps [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) from 21.0.6 to 21.1.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.1.5/packages/core)

---
updated-dependencies:
- dependency-name: "@angular/core"
  dependency-version: 21.1.5
  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-02-21 16:33:44 +01:00
dependabot[bot] 6078b552dd Bump actions/upload-artifact from 5 to 6 (#72)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 18:46:29 +01:00
nessar d07375b10f npm update 2026-01-07 18:44:13 +01:00
nessar 5769d2e653 remove loading for each change in "form" 2025-12-06 22:52:20 +01:00
nessar d6f5b7a7ed remove wrap useless 2025-12-06 22:28:34 +01:00
nessar d70489b111 chore: add peer dependencies to various packages in package-lock.json 2025-12-06 22:26:50 +01:00
dependabot[bot] d80749489f Bump jasmine-core (#70)
Bumps the development-dependencies group with 1 update in the / directory: [jasmine-core](https://github.com/jasmine/jasmine).


Updates `jasmine-core` from 5.12.1 to 5.13.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/v5.12.1...v5.13.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-version: 5.13.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>
2025-12-06 22:25:54 +01:00
dependabot[bot] 8ce6582123 Bump actions/checkout from 4 to 6 (#58) 2025-12-06 21:21:27 +00:00
nessar 2b5468090a prettier 2025-12-06 22:19:44 +01:00
nessar 327255b466 chore: update dependencies and devDependencies in package.json
- Updated jest-preset-angular from ^15.0.3 to ^16.0.0
2025-12-06 22:18:12 +01:00
Théophyl Hoguetandcopilot-swe-agent[bot] d3515f89e5 T hoguet/issue16 (#61)
* feat: enhance loading spinner with progress indication and add warning for unplaced vacation days

* feat: upgrade Angular dependencies to version 21 and implement new animation features

- Updated Angular dependencies in package.json to version 21.x.x.
- Added `provideAnimationsAsync` to app configuration for improved animation handling.
- Refactored user input component to utilize a dynamic list of vacation days with animations.
- Introduced a new `DayInput` component for managing individual vacation days.
- Enhanced styling and animations for user input and day input components.
- Updated calendar service to support new vacation day structure.
- Adjusted tests to accommodate changes in component structure and dependencies.

* feat: enhance vacation optimization logic and UI components

* Fix signal immutability violations and cleanup unused code (#62)

* Initial plan

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

* fix: add --legacy-peer-deps for Angular 21 compatibility (#63)

* Initial plan

* fix: add --legacy-peer-deps to npm ci for Angular 21 compatibility

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

* Fix unit tests for VacDay[] type migration (#64)

* Initial plan

* fix: update unit tests to match VacDay[] type structure

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-30 03:17:57 +01:00
3272622abf feat: enhance loading spinner with progress indication and add warnin… (#49)
* feat: enhance loading spinner with progress indication and add warning for unplaced vacation days

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

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

* Update src/app/components/calendar-year/calendar-year.scss

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

* Update src/app/components/user-input/user-input.html

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

* Apply suggestions from code review

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

* Fix setTimeout async anti-pattern in calendar-year component (#50)

* Initial plan

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-18 00:45:05 +01:00
23 changed files with 3052 additions and 2960 deletions

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v4
@@ -0,0 +1,28 @@
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 }}
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -52,7 +52,7 @@ jobs:
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scanner - name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.33.1 uses: aquasecurity/trivy-action@0.34.1
with: with:
image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }} image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }}
format: 'sarif' format: 'sarif'
+9 -9
View File
@@ -11,7 +11,7 @@ jobs:
name: Lint Check name: Lint Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -20,7 +20,7 @@ jobs:
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --legacy-peer-deps
- name: Check code formatting with Prettier - name: Check code formatting with Prettier
run: npx prettier --check "src/**/*.{ts,html,scss,css,json}" run: npx prettier --check "src/**/*.{ts,html,scss,css,json}"
@@ -32,7 +32,7 @@ jobs:
name: TypeScript Type Check name: TypeScript Type Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -41,7 +41,7 @@ jobs:
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --legacy-peer-deps
- name: Run TypeScript compiler - name: Run TypeScript compiler
run: npx tsc --noEmit run: npx tsc --noEmit
@@ -50,7 +50,7 @@ jobs:
name: Unit Tests name: Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -59,7 +59,7 @@ jobs:
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --legacy-peer-deps
- name: Run unit tests - name: Run unit tests
run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
@@ -76,7 +76,7 @@ jobs:
name: Build Application name: Build Application
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
@@ -85,7 +85,7 @@ jobs:
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --legacy-peer-deps
- name: Build production bundle - name: Build production bundle
run: npm run build -- --configuration production run: npm run build -- --configuration production
@@ -98,7 +98,7 @@ jobs:
echo "Build completed successfully" echo "Build completed successfully"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v7
with: with:
name: build-artifacts name: build-artifacts
path: dist/ path: dist/
+2418 -2639
View File
File diff suppressed because it is too large. Load diff
+32 -31
View File
@@ -27,44 +27,45 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^20.2.12", "@angular/animations": "21.2.0",
"@angular/common": "^20.3.10", "@angular/cdk": "^21.2.0",
"@angular/compiler": "^20.3.10", "@angular/common": "21.2.0",
"@angular/core": "^20.3.10", "@angular/compiler": "21.2.0",
"@angular/forms": "^20.3.10", "@angular/core": "21.2.0",
"@angular/material": "^20.2.12", "@angular/forms": "21.2.0",
"@angular/platform-browser": "^20.3.10", "@angular/material": "^21.2.0",
"@angular/platform-server": "^20.3.10", "@angular/platform-browser": "21.2.0",
"@angular/router": "^20.3.10", "@angular/platform-server": "21.2.0",
"@angular/ssr": "^20.3.10", "@angular/router": "21.2.0",
"date-holidays": "^3.26.2", "@angular/ssr": "21.2.0",
"date-holidays": "^3.26.8",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"express": "^5.1.0", "express": "^5.2.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.2",
"tslib": "^2.3.0" "tslib": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^20.3.10", "@angular/build": "21.1.5",
"@angular/cli": "^20.3.10", "@angular/cli": "21.1.5",
"@angular/compiler-cli": "^20.3.10", "@angular/compiler-cli": "21.1.5",
"@eslint/js": "^9.39.1", "@eslint/js": "^10.0.1",
"@types/express": "^5.0.1", "@types/express": "^5.0.6",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~6.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.10.0", "@types/node": "^25.3.0",
"eslint": "^9.39.1", "eslint": "^10.0.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.5",
"jasmine-core": "~5.12.1", "jasmine-core": "~6.1.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-preset-angular": "^15.0.3", "jest-preset-angular": "^16.0.0",
"karma": "~6.4.0", "karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.2.0",
"prettier": "^3.6.2", "prettier": "^3.8.1",
"typescript": "~5.9.2", "typescript": "~5.9.3",
"typescript-eslint": "^8.46.4" "typescript-eslint": "^8.56.1"
} }
} }
+2
View File
@@ -11,6 +11,7 @@ import {
withEventReplay, withEventReplay,
withIncrementalHydration, withIncrementalHydration,
} from '@angular/platform-browser'; } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -18,5 +19,6 @@ export const appConfig: ApplicationConfig = {
provideZonelessChangeDetection(), provideZonelessChangeDetection(),
provideRouter(routes), provideRouter(routes),
provideClientHydration(withEventReplay(), withIncrementalHydration()), provideClientHydration(withEventReplay(), withIncrementalHydration()),
provideAnimationsAsync(),
], ],
}; };
@@ -1,17 +1,16 @@
import { Component, effect, signal, inject } from '@angular/core'; import { Component, effect, signal, inject, untracked } from '@angular/core';
import { UserInputService } from '../../services/user-input-service'; import { UserInputService } from '../../services/user-input-service';
import { MatCard, MatCardContent } from '@angular/material/card'; import { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month'; import { CalendarMonth } from '../calendar-month/calendar-month';
import { CalendarService, SelectedDates } from '../../services/calendar-service'; import { CalendarService, SelectedDates } from '../../services/calendar-service';
import { CommonModule } from '@angular/common';
import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service'; import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service'; import { DateCacheService } from '../../services/date-cache.service';
import { LoggerService } from '../../services/logger.service';
@Component({ @Component({
selector: 'app-calendar-year', selector: 'app-calendar-year',
imports: [MatCard, CalendarMonth, MatCardContent, CommonModule, MatProgressSpinner], imports: [MatCard, CalendarMonth, MatCardContent, MatProgressSpinner],
templateUrl: './calendar-year.html', templateUrl: './calendar-year.html',
styleUrl: './calendar-year.scss', styleUrl: './calendar-year.scss',
}) })
@@ -20,7 +19,6 @@ export class CalendarYear {
private userInput = inject(UserInputService); private userInput = inject(UserInputService);
private calendarService = inject(CalendarService); private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService); private calendarSettingsService = inject(CalendarSettingsService);
private logger = inject(LoggerService);
protected isLoading = signal(true); protected isLoading = signal(true);
protected loadingProgress = signal(0); protected loadingProgress = signal(0);
@@ -36,9 +34,11 @@ export class CalendarYear {
constructor() { constructor() {
effect(() => { effect(() => {
this.userInput.computeTriggerSignal();
this.selectedDates = this.calendarService.getSelectedDatesForYear(); this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = this.userInput.vacationNumberSignal(); const vacationData = untracked(() => this.userInput.vacationNumberSignal());
const c = this.calendarSettingsService.get('samediMalin')(); const c = untracked(() => this.calendarSettingsService.get('samediMalin')());
console.log('Samedi Malin setting:', c); console.log('Samedi Malin setting:', c);
console.log('Optimizing vacations with data:', vacationData); console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true); this.isLoading.set(true);
@@ -47,15 +47,15 @@ export class CalendarYear {
queueMicrotask(() => { queueMicrotask(() => {
void (async () => { void (async () => {
if (this.selectedDates) { if (this.selectedDates) {
// Create a copy to avoid mutating the original signal data // Create a deep copy to avoid mutating the original signal data
const vacationDaysCopy = vacationData.map((vd) => ({ ...vd }));
const remainingDays = await this.selectedDates.optimizeVacations( const remainingDays = await this.selectedDates.optimizeVacations(
{ ...vacationData }, vacationDaysCopy,
this.calendarSettingsService, this.calendarSettingsService,
this.calendarService, this.calendarService,
(progress: number) => { (progress: number) => {
this.loadingProgress.set(progress); this.loadingProgress.set(progress);
}, },
this.logger,
); );
// Update the remaining vacation days signal // Update the remaining vacation days signal
this.userInput.remainingVacationDaysSignal.set(remainingDays); this.userInput.remainingVacationDaysSignal.set(remainingDays);
+33 -18
View File
@@ -1,29 +1,44 @@
<div class="user-input"> <div class="user-input">
<p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p> <p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p>
<form [formGroup]="userInputForm"> <div class="listOfDays">
<mat-form-field appearance="fill"> @for (day of daysOff; track day.id) {
<mat-label>Combien de CP par an ?</mat-label> <div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
<input matInput type="number" max="365" min="0" formControlName="CP" /> <app-day-input
</mat-form-field> [day]="day"
<mat-form-field appearance="fill"> (dayChange)="updateDay($index, $event)"
<mat-label>Combien de jours de RTT par an ?</mat-label> (deleteEntry)="removeFromList($index)"
<input matInput type="number" max="365" min="0" formControlName="RTT" /> ></app-day-input>
</mat-form-field> </div>
<mat-form-field appearance="fill"> }
<mat-label>Autres jours</mat-label> <mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
<input matInput type="number" max="365" min="0" formControlName="Others" /> <button matFab aria-label="Add entry">
</mat-form-field> <mat-icon>add</mat-icon>
</form> </button>
<h3>Ajouter un jour de vacances</h3>
</mat-card>
</div>
<div class="action-buttons">
<button matFab extended (click)="triggerComputation()">
<mat-icon>calculate</mat-icon>
Calculer
</button>
<app-calendar-settings></app-calendar-settings>
</div>
@if (hasRemainingDays()) { @if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card"> <mat-card class="mat-elevation-z2 warning-card">
<mat-card-content class="warning-content"> <mat-card-content class="warning-content">
<mat-icon color="warn" aria-hidden="true">warning</mat-icon> <mat-icon color="warn">warning</mat-icon>
<div class="warning">
<span> <span>
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances
placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement. non placé{{ remainingDays() > 1 ? 's' : '' }}:
</span> </span>
@for (day of remainingDaysDetails(); track $index) {
<span class="remaining-day-detail">- {{ day }}</span>
}
<span> Veuillez les placer manuellement. </span>
</div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
} }
<app-calendar-settings></app-calendar-settings>
</div> </div>
@@ -25,7 +25,79 @@
} }
} }
.listOfDays {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.day-input-wrapper {
animation: slide-fade-in 200ms ease-out;
@starting-style {
opacity: 0;
transform: translateX(-20px);
}
}
.day-enter {
animation: slide-fade-in 200ms ease-out;
}
.day-leave {
opacity: 0;
transform: translateX(20px);
transition:
opacity 200ms ease-in,
transform 200ms ease-in;
}
@keyframes slide-fade-in {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.add-day-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
gap: 1rem;
margin-bottom: 1rem;
cursor: pointer;
}
.add-day-card h3 {
flex-grow: 1;
margin: 0;
font-weight: normal;
}
.action-buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
}
.warning {
display: flex;
flex-direction: column;
}
:host {
max-width: 100%;
}
@@ -1,19 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core'; import { provideZonelessChangeDetection } from '@angular/core';
import { UserInput } from './user-input'; import { UserInput } from './user-input';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { UserInputService } from '../../services/user-input-service';
describe('UserInput', () => { describe('UserInput', () => {
let component: UserInput; let component: UserInput;
let fixture: ComponentFixture<UserInput>; let fixture: ComponentFixture<UserInput>;
let userInputService: UserInputService;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserInput], imports: [UserInput],
providers: [provideZonelessChangeDetection()], providers: [provideZonelessChangeDetection(), provideAnimationsAsync()],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(UserInput); fixture = TestBed.createComponent(UserInput);
component = fixture.componentInstance; component = fixture.componentInstance;
userInputService = TestBed.inject(UserInputService);
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -21,20 +25,22 @@ describe('UserInput', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should initialize form with values from service', () => { it('should initialize with vacation days from service', () => {
expect(component.userInputForm).toBeDefined(); const vacationDays = userInputService.vacationNumberSignal();
expect(component.userInputForm.get('CP')).toBeDefined(); expect(vacationDays).toBeDefined();
expect(component.userInputForm.get('RTT')).toBeDefined(); expect(Array.isArray(vacationDays)).toBe(true);
expect(component.userInputForm.get('Others')).toBeDefined(); expect(vacationDays.length).toBeGreaterThan(0);
}); });
it('should have valid form controls', () => { it('should add a new vacation day', () => {
const cpControl = component.userInputForm.get('CP'); const initialLength = userInputService.vacationNumberSignal().length;
const rttControl = component.userInputForm.get('RTT'); component.addDay();
const othersControl = component.userInputForm.get('Others'); expect(userInputService.vacationNumberSignal().length).toBe(initialLength + 1);
});
expect(cpControl).toBeTruthy(); it('should remove a vacation day', () => {
expect(rttControl).toBeTruthy(); const initialLength = userInputService.vacationNumberSignal().length;
expect(othersControl).toBeTruthy(); component.removeFromList(0);
expect(userInputService.vacationNumberSignal().length).toBe(initialLength - 1);
}); });
}); });
+45 -47
View File
@@ -1,79 +1,77 @@
import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core'; import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { UserInputService } from '../../services/user-input-service'; import { UserInputService } from '../../services/user-input-service';
import { VacationsNumber } from '../../services/calendar-service';
import { debounceTime, takeUntil, Subject } from 'rxjs';
import { CalendarSettings } from '../calendar-settings/calendar-settings'; import { CalendarSettings } from '../calendar-settings/calendar-settings';
import { MatCard, MatCardContent } from '@angular/material/card'; import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon'; 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';
@Component({ @Component({
selector: 'app-user-input', selector: 'app-user-input',
imports: [ imports: [
MatFormField,
MatLabel,
MatInputModule, MatInputModule,
ReactiveFormsModule,
CalendarSettings, CalendarSettings,
MatCard, MatCard,
MatCardContent, MatCardContent,
MatIcon, MatIcon,
MatButtonModule,
DayInput,
], ],
templateUrl: './user-input.html', templateUrl: './user-input.html',
styleUrl: './user-input.scss', styleUrl: './user-input.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class UserInput implements OnInit, OnDestroy { export class UserInput {
constructor(private userInputService: UserInputService) { private userInputService: UserInputService = inject(UserInputService);
// Watch for signal changes and update form
effect(() => {
const vacationData = this.userInputService.vacationNumberSignal();
this.userInputForm.patchValue(
{
CP: vacationData.cp,
RTT: vacationData.rtt,
Others: vacationData.other,
},
{ emitEvent: false },
);
});
}
private destroy$ = new Subject<void>();
userInputForm: FormGroup = new FormGroup({
CP: new FormControl<number | null>(null),
RTT: new FormControl<number | null>(null),
Others: new FormControl<number | null>(null),
});
protected remainingDays = computed(() => { protected remainingDays = computed(() => {
const remaining = this.userInputService.remainingVacationDaysSignal(); const remaining = this.userInputService.remainingVacationDaysSignal();
return remaining.cp + remaining.rtt + remaining.other; return remaining.reduce((acc, day) => acc + day.numberOfDays, 0);
});
protected remainingDaysDetails = computed(() => {
const remaining = this.userInputService.remainingVacationDaysSignal();
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})`;
});
}); });
protected hasRemainingDays = computed(() => this.remainingDays() > 0); protected hasRemainingDays = computed(() => this.remainingDays() > 0);
ngOnInit() { protected get daysOff() {
this.userInputForm.valueChanges return this.userInputService.vacationNumberSignal();
.pipe(debounceTime(1500), takeUntil(this.destroy$))
.subscribe(() => this.submitForm());
} }
ngOnDestroy() { public addDay() {
this.destroy$.next(); this.userInputService.vacationNumberSignal.update((days) => [
this.destroy$.complete(); ...days,
this.userInputService.createVacationDay(),
]);
} }
private submitForm() { public removeFromList(index: number) {
if (this.userInputForm.valid) { this.userInputService.vacationNumberSignal.update((days) =>
const vacationData: VacationsNumber = { days.filter((_, i) => i !== index),
cp: this.userInputForm.get('CP')?.value || 0, );
rtt: this.userInputForm.get('RTT')?.value || 0,
other: this.userInputForm.get('Others')?.value || 0,
};
this.userInputService.vacationNumberSignal.set(vacationData);
} }
public updateDay(index: number, updatedDay: VacDay) {
this.userInputService.vacationNumberSignal.update((days) =>
days.map((day, i) => (i === index ? updatedDay : day)),
);
}
public triggerComputation() {
this.userInputService.requestComputation();
} }
} }
@@ -0,0 +1,36 @@
<!-- <mat-list-item role="listitem"> -->
<mat-card class="card">
<mat-form-field class="numberInput">
<input
matInput
type="number"
min="0"
[value]="day().numberOfDays"
(input)="onInput($event)"
/>
</mat-form-field>
<mat-form-field class="dayTypeInput">
<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-select>
</mat-form-field>
<mat-form-field class="dateInput">
<mat-label>Choose a date</mat-label>
<input
matInput
type="datetime"
[matDatepicker]="picker"
[value]="day().expiryDate"
(dateChange)="addEvent($event)"
/>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<button matFab aria-label="Delete entry" (click)="deleteEntry.emit()">
<mat-icon>close</mat-icon>
</button>
</mat-card>
<!-- </mat-list-item> -->
@@ -0,0 +1,20 @@
.card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px;
gap: 16px;
}
.dateInput {
flex-grow: 1;
}
.numberInput {
width: 80px;
}
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { DayInput } from './day-input';
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
describe('DayInput', () => {
let component: DayInput;
let fixture: ComponentFixture<DayInput>;
const mockVacDay: VacDay = {
id: 'test-id',
type: SelectableDayType.CP,
numberOfDays: 5,
expiryDate: new Date('2025-12-31'),
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DayInput],
providers: [provideAnimationsAsync()],
}).compileComponents();
fixture = TestBed.createComponent(DayInput);
component = fixture.componentInstance;
fixture.componentRef.setInput('day', mockVacDay);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,103 @@
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
import { SelectableDayType, tooltipTypeMapping, VacDay } from '../../../services/calendar-service';
import { MatCard } from '@angular/material/card';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatInput, MatInputModule, MatLabel } from '@angular/material/input';
import {
MatDatepicker,
MatDatepickerInput,
MatDatepickerInputEvent,
MatDatepickerToggle,
} from '@angular/material/datepicker';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
NativeDateAdapter,
} from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
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);
}
override parse(value: string): Date | null {
if (!value) return null;
const parts = value.split('/');
if (parts.length === 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1;
const year = parseInt(parts[2], 10);
if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
return new Date(year, month, day);
}
}
return null;
}
}
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-day-input',
imports: [
MatIconModule,
MatCard,
MatSelect,
MatOption,
MatInput,
MatDatepicker,
MatLabel,
MatDatepickerToggle,
MatDatepickerInput,
MatInputModule,
MatButtonModule,
],
templateUrl: './day-input.html',
styleUrl: './day-input.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 DayInput {
public day = input.required<VacDay>();
public dayChange = output<VacDay>();
public deleteEntry = output<void>();
protected readonly selectableDayTypes = Object.values(SelectableDayType);
protected readonly tooltipTypeMapping = tooltipTypeMapping;
onInput(event: Event) {
const value = Number((event.target as HTMLInputElement).value);
this.dayChange.emit({ ...this.day(), numberOfDays: value });
}
onTypeChange(type: SelectableDayType) {
this.dayChange.emit({ ...this.day(), type });
}
addEvent(event: MatDatepickerInputEvent<Date>) {
this.dayChange.emit({ ...this.day(), expiryDate: event.value! });
}
}
+115 -56
View File
@@ -6,7 +6,6 @@ import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek'; import isoWeek from 'dayjs/plugin/isoWeek';
import { CalendarSettingsService } from './calendar-settings-service'; import { CalendarSettingsService } from './calendar-settings-service';
import { DateCacheService } from './date-cache.service'; import { DateCacheService } from './date-cache.service';
import { LoggerService } from './logger.service';
dayjs.extend(weekOfYear); dayjs.extend(weekOfYear);
dayjs.extend(isoWeek); dayjs.extend(isoWeek);
@@ -22,7 +21,6 @@ const allTypesOptions: HolidaysTypes.Options = {
}) })
export class CalendarService { export class CalendarService {
private dateCache = inject(DateCacheService); private dateCache = inject(DateCacheService);
private logger = inject(LoggerService);
// Cache for holiday timestamps // Cache for holiday timestamps
private holidayTimestampsCache: Set<number> | null = null; private holidayTimestampsCache: Set<number> | null = null;
@@ -186,10 +184,11 @@ export class CalendarService {
} }
} }
export type VacationsNumber = { export type VacDay = {
rtt: number; id: string;
cp: number; type: SelectableDayType;
other: number; numberOfDays: number;
expiryDate: Date;
}; };
export type Week = (Date | null)[]; export type Week = (Date | null)[];
@@ -205,6 +204,12 @@ export enum DayType {
TODAY = 'today', TODAY = 'today',
} }
export enum SelectableDayType {
CP = DayType.CP,
RTT = DayType.RTT,
OTHER = DayType.OTHER,
}
export const tooltipTypeMapping: { [key in DayType]: string } = { export const tooltipTypeMapping: { [key in DayType]: string } = {
[DayType.WEEKEND]: 'Weekend', [DayType.WEEKEND]: 'Weekend',
[DayType.CLOSED_DAY]: 'Jour Férié', [DayType.CLOSED_DAY]: 'Jour Férié',
@@ -268,7 +273,7 @@ export class SelectedDate implements SelectedDateInterface {
getClasses(date: Date): string { getClasses(date: Date): string {
let returnType: DayType | string = ''; let returnType: DayType | string = '';
if (this.type && date >= this.range.start! && date <= this.range.end!) { if (this.type && date >= this.range.start! && date <= this.range.end!) {
returnType = returnType += this.type + ' '; returnType += this.type + ' ';
} }
return returnType; return returnType;
} }
@@ -539,16 +544,15 @@ export class SelectedDates implements SelectedDateInterface {
} }
async optimizeVacations( async optimizeVacations(
vacationsNumber: VacationsNumber, vacationDays: VacDay[],
calendarSettingsService: CalendarSettingsService, calendarSettingsService: CalendarSettingsService,
calendarService: CalendarService, calendarService: CalendarService,
progressCallback?: (progress: number) => void, progressCallback?: (progress: number) => void,
logger?: LoggerService, ): Promise<VacDay[]> {
): Promise<VacationsNumber> { if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationDays);
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
const initialVacations = { ...vacationsNumber }; // Calculate total days to process
const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other; const totalDays = vacationDays.reduce((sum, vd) => sum + vd.numberOfDays, 0);
let processedDays = 0; let processedDays = 0;
// Helper function to update progress, throttled to every 5% // Helper function to update progress, throttled to every 5%
@@ -568,30 +572,30 @@ export class SelectedDates implements SelectedDateInterface {
await updateProgress(); await updateProgress();
while (vacationsNumber.cp > 0) { // Track VacDays that can't find valid placements
const result = this.lookForVacation(DayType.CP, calendarService); const exhaustedVacDayIds = new Set<string>();
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
// Process vacation days ordered by expiration date (earliest first)
while (vacationDays.some((vd) => vd.numberOfDays > 0 && !exhaustedVacDayIds.has(vd.id))) {
// Sort by expiration date to use days that expire first, excluding exhausted ones
const sortedVacDays = [...vacationDays]
.filter((vd) => vd.numberOfDays > 0 && !exhaustedVacDayIds.has(vd.id))
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime());
if (sortedVacDays.length === 0) {
break; break;
} }
vacationsNumber.cp -= 1;
processedDays++; const vacDayToUse = sortedVacDays[0];
await updateProgress(); const result = this.lookForVacation(vacDayToUse, calendarService);
}
while (vacationsNumber.rtt > 0) {
const result = this.lookForVacation(DayType.RTT, calendarService);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) { if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
break; // Mark this VacDay as exhausted and try other vacation days
exhaustedVacDayIds.add(vacDayToUse.id);
continue;
} }
vacationsNumber.rtt -= 1;
processedDays++; vacDayToUse.numberOfDays -= 1;
await updateProgress();
}
while (vacationsNumber.other > 0) {
const result = this.lookForVacation(DayType.OTHER, calendarService);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
break;
}
vacationsNumber.other -= 1;
processedDays++; processedDays++;
await updateProgress(); await updateProgress();
} }
@@ -602,18 +606,18 @@ export class SelectedDates implements SelectedDateInterface {
} }
// Inform user if not all days were placed // Inform user if not all days were placed
const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other; const remainingDays = vacationDays.reduce((sum, vd) => sum + vd.numberOfDays, 0);
if (remainingDays > 0 && logger) { if (remainingDays > 0) {
logger.warn( console.warn(
`Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` + `Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` +
`Too many days have equivalent placement quality - please place remaining days manually.`, `Too many days have equivalent placement quality - please place remaining days manually.`,
); );
} }
return vacationsNumber; return vacationDays;
} }
samediMalin(vacationsNumber: VacationsNumber): void { samediMalin(vacationDays: VacDay[]): void {
const now = new Date(); const now = new Date();
now.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0);
const nowTime = now.getTime(); const nowTime = now.getTime();
@@ -622,14 +626,63 @@ export class SelectedDates implements SelectedDateInterface {
(date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6, (date) => date.type === DayType.CLOSED_DAY && date.range.start?.getDay() === 6,
); );
// Helper function to get available CP days that don't expire before a given date (ordered by expiry date)
const getAvailableCpDaysForDate = (useByDate: Date): VacDay[] => {
const useByTime = useByDate.getTime();
return vacationDays
.filter(
(vd) =>
vd.type === SelectableDayType.CP &&
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) {
if (daysToConsume === 0) break;
const toConsume = Math.min(cpDay.numberOfDays, daysToConsume);
cpDay.numberOfDays -= toConsume;
daysToConsume -= toConsume;
}
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;
} 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),
});
}
};
for (const holiday of samedi_ferie) { for (const holiday of samedi_ferie) {
if (!holiday.range.start) continue; if (!holiday.range.start) continue;
// Skip if no CP days available
if (vacationsNumber.cp < 2) {
continue;
}
// Get the Saturday date (holiday) and the following Sunday // Get the Saturday date (holiday) and the following Sunday
const saturdayDate = new Date(holiday.range.start); const saturdayDate = new Date(holiday.range.start);
const sundayDate = new Date(saturdayDate); const sundayDate = new Date(saturdayDate);
@@ -654,21 +707,24 @@ export class SelectedDates implements SelectedDateInterface {
const oneDayAfter = new Date(sundayDate); const oneDayAfter = new Date(sundayDate);
oneDayAfter.setDate(sundayDate.getDate() + 1); oneDayAfter.setDate(sundayDate.getDate() + 1);
// Check if we can apply each strategy (and not in the past) // Check if we can apply each strategy (not in the past, not already selected, and have non-expired CP days)
const canApplyTwoBeforeStrategy = const canApplyTwoBeforeStrategy =
twoDaysBefore1.getTime() >= nowTime && twoDaysBefore1.getTime() >= nowTime &&
!this.isSelected(twoDaysBefore1) && !this.isSelected(twoDaysBefore1) &&
!this.isSelected(twoDaysBefore2); !this.isSelected(twoDaysBefore2) &&
getTotalCpDaysForDate(twoDaysBefore2) >= 2; // Check expiry against the latest date used
const canApplyTwoAfterStrategy = const canApplyTwoAfterStrategy =
twoDaysAfter1.getTime() >= nowTime && twoDaysAfter1.getTime() >= nowTime &&
!this.isSelected(twoDaysAfter1) && !this.isSelected(twoDaysAfter1) &&
!this.isSelected(twoDaysAfter2); !this.isSelected(twoDaysAfter2) &&
getTotalCpDaysForDate(twoDaysAfter2) >= 2; // Check expiry against the latest date used
const canApplyMixedStrategy = const canApplyMixedStrategy =
oneDayBefore.getTime() >= nowTime && oneDayBefore.getTime() >= nowTime &&
!this.isSelected(oneDayBefore) && !this.isSelected(oneDayBefore) &&
!this.isSelected(oneDayAfter); !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 // Test all applicable strategies and choose the one with the best heuristic score
const strategies: { apply: () => void; heuristic: number }[] = []; const strategies: { apply: () => void; heuristic: number }[] = [];
@@ -691,8 +747,8 @@ export class SelectedDates implements SelectedDateInterface {
new DateRange<Date>(twoDaysBefore1, twoDaysBefore2), new DateRange<Date>(twoDaysBefore1, twoDaysBefore2),
), ),
); );
vacationsNumber.cp -= 2; consume2CpDaysForDate(twoDaysBefore2);
vacationsNumber.other += 1; // Reward: gain 1 other day addOtherDayReward();
}, },
heuristic: heuristic, heuristic: heuristic,
}); });
@@ -715,8 +771,8 @@ export class SelectedDates implements SelectedDateInterface {
new DateRange<Date>(twoDaysAfter1, twoDaysAfter2), new DateRange<Date>(twoDaysAfter1, twoDaysAfter2),
), ),
); );
vacationsNumber.cp -= 2; consume2CpDaysForDate(twoDaysAfter2);
vacationsNumber.other += 1; // Reward: gain 1 other day addOtherDayReward();
}, },
heuristic: heuristic, heuristic: heuristic,
}); });
@@ -748,8 +804,8 @@ export class SelectedDates implements SelectedDateInterface {
new DateRange<Date>(oneDayAfter, oneDayAfter), new DateRange<Date>(oneDayAfter, oneDayAfter),
), ),
); );
vacationsNumber.cp -= 2; consume2CpDaysForDate(oneDayAfter);
vacationsNumber.other += 1; // Reward: gain 1 other day addOtherDayReward();
}, },
heuristic: heuristic, heuristic: heuristic,
}); });
@@ -769,22 +825,24 @@ export class SelectedDates implements SelectedDateInterface {
} }
lookForVacation( lookForVacation(
vacType: DayType, vacDay: VacDay,
calendarService: CalendarService, calendarService: CalendarService,
): { apply: () => void; heuristic: number } { ): { apply: () => void; heuristic: number } {
const daysOfYear = calendarService.monthsInAYearFromNow(); const daysOfYear = calendarService.monthsInAYearFromNow();
const now = new Date(); const now = new Date();
now.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0);
const nowTime = now.getTime(); const nowTime = now.getTime();
const expiryTime = vacDay.expiryDate.getTime();
// Preprocess: filter out past days and already selected days in one pass // Preprocess: filter out past days, already selected days, and days after expiry date
const candidateDays: Date[] = []; const candidateDays: Date[] = [];
for (const month of daysOfYear) { for (const month of daysOfYear) {
for (const week of month) { for (const week of month) {
for (const day of week) { for (const day of week) {
if (day) { if (day) {
day.setHours(0, 0, 0, 0); day.setHours(0, 0, 0, 0);
if (day.getTime() >= nowTime && !this.isSelected(day)) { const dayTime = day.getTime();
if (dayTime >= nowTime && dayTime <= expiryTime && !this.isSelected(day)) {
candidateDays.push(day); candidateDays.push(day);
} }
} }
@@ -797,6 +855,7 @@ export class SelectedDates implements SelectedDateInterface {
return { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC }; return { apply: () => {}, heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC };
} }
const vacType = vacDay.type as unknown as DayType;
const tempDatesBackup = [...this._datesSelected]; const tempDatesBackup = [...this._datesSelected];
let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null; let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null;
let bestHeuristic = -Infinity; let bestHeuristic = -Infinity;
-52
View File
@@ -1,52 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { LoggerService } from './logger.service';
describe('LoggerService', () => {
let service: LoggerService;
let consoleLogSpy: jasmine.Spy;
let consoleWarnSpy: jasmine.Spy;
let consoleErrorSpy: jasmine.Spy;
let consoleDebugSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LoggerService);
// Spy on console methods
consoleLogSpy = spyOn(console, 'log');
consoleWarnSpy = spyOn(console, 'warn');
consoleErrorSpy = spyOn(console, 'error');
consoleDebugSpy = spyOn(console, 'debug');
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should log messages', () => {
service.log('test message');
expect(consoleLogSpy).toHaveBeenCalledWith('test message');
});
it('should log warnings', () => {
service.warn('test warning');
expect(consoleWarnSpy).toHaveBeenCalledWith('test warning');
});
it('should log errors', () => {
service.error('test error');
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
});
it('should log debug messages', () => {
service.debug('test debug');
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug');
});
it('should pass additional arguments to console methods', () => {
const arg1 = { key: 'value' };
const arg2 = [1, 2, 3];
service.log('test', arg1, arg2);
expect(consoleLogSpy).toHaveBeenCalledWith('test', arg1, arg2);
});
});
-47
View File
@@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
/**
* Logger service that wraps console methods.
* Can be configured to disable logging in production environments.
*/
@Injectable({
providedIn: 'root',
})
export class LoggerService {
private isProduction = false; // Can be configured based on environment
/**
* Log an informational message
*/
log(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.log(message, ...args);
}
}
/**
* Log a warning message
*/
warn(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.warn(message, ...args);
}
}
/**
* Log an error message
*/
error(message: string, ...args: unknown[]): void {
// Always log errors, even in production
console.error(message, ...args);
}
/**
* Log a debug message
*/
debug(message: string, ...args: unknown[]): void {
if (!this.isProduction) {
console.debug(message, ...args);
}
}
}
+32 -9
View File
@@ -1,8 +1,9 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core'; import { provideZonelessChangeDetection } from '@angular/core';
import { UserInputService } from './user-input-service'; import { UserInputService } from './user-input-service';
import { SelectableDayType, VacDay } from './calendar-service';
describe('UserInput', () => { describe('UserInputService', () => {
let service: UserInputService; let service: UserInputService;
beforeEach(() => { beforeEach(() => {
@@ -16,19 +17,41 @@ describe('UserInput', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should initialize with random vacation numbers', () => { it('should initialize with vacation days array', () => {
const vacations = service.vacationNumberSignal(); const vacations = service.vacationNumberSignal();
expect(vacations.cp).toBeGreaterThanOrEqual(1); expect(Array.isArray(vacations)).toBe(true);
expect(vacations.cp).toBeLessThanOrEqual(9); expect(vacations.length).toBeGreaterThan(0);
expect(vacations.rtt).toBeGreaterThanOrEqual(1); });
expect(vacations.rtt).toBeLessThanOrEqual(9);
expect(vacations.other).toBeGreaterThanOrEqual(1); it('should have vacation days with valid properties', () => {
expect(vacations.other).toBeLessThanOrEqual(9); const vacations = service.vacationNumberSignal();
vacations.forEach((day) => {
expect(day.id).toBeDefined();
expect(day.type).toBeDefined();
expect(day.numberOfDays).toBeGreaterThanOrEqual(1);
expect(day.numberOfDays).toBeLessThanOrEqual(9);
expect(day.expiryDate).toBeInstanceOf(Date);
});
}); });
it('should update vacation number signal', () => { it('should update vacation number signal', () => {
const newVacations = { cp: 5, rtt: 3, other: 2 }; const newVacations: VacDay[] = [
{
id: 'test-id',
type: SelectableDayType.CP,
numberOfDays: 5,
expiryDate: new Date(),
},
];
service.vacationNumberSignal.set(newVacations); service.vacationNumberSignal.set(newVacations);
expect(service.vacationNumberSignal()).toEqual(newVacations); expect(service.vacationNumberSignal()).toEqual(newVacations);
}); });
it('should create a new vacation day', () => {
const newDay = service.createVacationDay();
expect(newDay.id).toBeDefined();
expect(newDay.type).toBe(SelectableDayType.CP);
expect(newDay.numberOfDays).toBe(1);
expect(newDay.expiryDate).toBeInstanceOf(Date);
});
}); });
+37 -11
View File
@@ -1,5 +1,5 @@
import { Injectable, signal } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { VacationsNumber } from './calendar-service'; import { SelectableDayType, VacDay } from './calendar-service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -9,18 +9,44 @@ export class UserInputService {
return Math.floor(Math.random() * 9) + 1; return Math.floor(Math.random() * 9) + 1;
} }
private getInitialValues(): VacationsNumber { private getInitialValues(): VacDay[] {
return [
{
id: crypto.randomUUID(),
type: SelectableDayType.CP,
numberOfDays: this.getRandomNumber(),
// today + 365 days
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
},
{
id: crypto.randomUUID(),
type: SelectableDayType.RTT,
numberOfDays: this.getRandomNumber(),
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
},
{
id: crypto.randomUUID(),
type: SelectableDayType.OTHER,
numberOfDays: this.getRandomNumber(),
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
},
];
}
public createVacationDay(): VacDay {
return { return {
cp: this.getRandomNumber(), id: crypto.randomUUID(),
rtt: this.getRandomNumber(), type: SelectableDayType.CP,
other: this.getRandomNumber(), numberOfDays: 1,
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
}; };
} }
readonly vacationNumberSignal = signal<VacationsNumber>(this.getInitialValues()); readonly vacationNumberSignal = signal<VacDay[]>(this.getInitialValues());
readonly remainingVacationDaysSignal = signal<VacationsNumber>({ readonly remainingVacationDaysSignal = signal<VacDay[]>([]);
cp: 0, readonly computeTriggerSignal = signal(0);
rtt: 0,
other: 0, public requestComputation(): void {
}); this.computeTriggerSignal.update((count) => count + 1);
}
} }
+3 -9
View File
@@ -4,14 +4,8 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [ "types": ["node"]
"node"
]
}, },
"include": [ "include": ["src/**/*.ts"],
"src/**/*.ts" "exclude": ["src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }
+2 -6
View File
@@ -4,11 +4,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jasmine"]
"jasmine"
]
}, },
"include": [ "include": ["src/**/*.ts"]
"src/**/*.ts"
]
} }