Compare commits

...
Author SHA1 Message Date
Théophyl Hoguet 7cd67dce01 Merge branch 'main' into copilot/update-ci-cd-pipeline 2025-11-03 20:42:57 +01:00
copilot-swe-agent[bot]andTHoguet b7970a68c9 Update README and fix build configuration
- Add CI/CD documentation to README
- Document code quality tools and scripts
- Disable font optimization in production build to avoid network dependency
- Add build cache configuration for GitHub Actions

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-03 19:15:56 +00:00
copilot-swe-agent[bot]andTHoguet 35b31b3188 Add comprehensive CI/CD workflows and tooling
- Add PR validation workflow with lint, format, type check, test, and build steps
- Improve Docker build workflow with caching, security scanning, and manual trigger
- Add CodeQL security analysis workflow
- Add Dependabot configuration for automated dependency updates
- Configure ESLint with TypeScript support
- Add npm scripts for linting and formatting
- Format code with Prettier
- Fix linting issues in source code

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-03 19:13:52 +00:00
copilot-swe-agent[bot] f0daa354bf Initial plan 2025-11-03 19:03:52 +00:00
32 changed files with 935 additions and 340 deletions

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
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
+32 -92
View File
@@ -1,101 +1,41 @@
# For most projects, this workflow file will not need changing; you simply need name: CodeQL Security Analysis
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on: on:
push: push:
branches: [ "main" ] branches: [main]
pull_request: pull_request:
branches: [ "main" ] branches: [main]
schedule: schedule:
- cron: '27 7 * * 2' - cron: '0 0 * * 1' # Run every Monday at midnight UTC
jobs: jobs:
analyze: analyze:
name: Analyze (${{ matrix.language }}) name: Analyze Code
# Runner size impacts CodeQL analysis time. To learn more, please see: runs-on: ubuntu-latest
# - https://gh.io/recommended-hardware-resources-for-running-codeql permissions:
# - https://gh.io/supported-runners-and-hardware-resources security-events: write
# - https://gh.io/using-larger-runners (GitHub.com only) actions: read
# Consider using larger runners or machines with greater resources for possible analysis time improvements. contents: read
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs strategy:
packages: read fail-fast: false
matrix:
language: ['javascript-typescript']
# only required for workflows in private repositories steps:
actions: read - name: Checkout repository
contents: read uses: actions/checkout@v4
strategy: - name: Initialize CodeQL
fail-fast: false uses: github/codeql-action/init@v3
matrix: with:
include: languages: ${{ matrix.language }}
- language: actions queries: security-extended,security-and-quality
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action. - name: Autobuild
# This includes steps like installing compilers or runtimes (`actions/setup-node` uses: github/codeql-action/autobuild@v3
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning. - name: Perform CodeQL Analysis
- name: Initialize CodeQL uses: github/codeql-action/analyze@v3
uses: github/codeql-action/init@v4 with:
with: category: '/language:${{matrix.language}}'
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
+22 -1
View File
@@ -5,6 +5,7 @@ on:
branches: [main] branches: [main]
release: release:
types: [published] types: [published]
workflow_dispatch:
jobs: jobs:
build-and-push: build-and-push:
@@ -12,10 +13,14 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
security-events: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -33,11 +38,27 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=sha type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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.27.0
with:
image-ref: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
+105
View File
@@ -0,0 +1,105 @@
name: PR Validation
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- 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@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run TypeScript compiler
run: npx tsc --noEmit
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v4
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@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- 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@v4
with:
name: build-artifacts
path: dist/
retention-days: 7
+41
View File
@@ -12,6 +12,38 @@ ng serve
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. 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.
## Code Quality
This project uses ESLint and Prettier to maintain code quality and consistency.
### Linting
Check for linting errors:
```bash
npm run lint
```
Auto-fix linting issues:
```bash
npm run lint:fix
```
### Formatting
Check code formatting:
```bash
npm run format:check
```
Format code:
```bash
npm run format
```
## Code scaffolding ## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run: Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
@@ -54,6 +86,15 @@ ng e2e
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## CI/CD
This project uses GitHub Actions for continuous integration and deployment:
- **PR Validation**: Automatically runs on pull requests to validate code quality, run tests, and build the application
- **Docker Build & Push**: Builds and publishes Docker images to GitHub Container Registry on main branch pushes and releases
- **CodeQL Analysis**: Runs security analysis weekly and on pull requests
- **Dependabot**: Automatically updates dependencies weekly
## Additional Resources ## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
+4 -1
View File
@@ -52,7 +52,10 @@
"maximumError": "16kB" "maximumError": "16kB"
} }
], ],
"outputHashing": "all" "outputHashing": "all",
"optimization": {
"fonts": false
}
}, },
"development": { "development": {
"optimization": false, "optimization": false,
+34
View File
@@ -0,0 +1,34 @@
import eslintJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
import prettierPlugin from 'eslint-plugin-prettier';
export default [
eslintJs.configs.recommended,
...tseslint.configs.recommended,
prettier,
{
files: ['**/*.ts'],
plugins: {
prettier: prettierPlugin,
},
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'prettier/prettier': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'],
},
];
+449 -1
View File
@@ -28,6 +28,7 @@
"@angular/build": "^20.3.7", "@angular/build": "^20.3.7",
"@angular/cli": "^20.3.7", "@angular/cli": "^20.3.7",
"@angular/compiler-cli": "^20.3.0", "@angular/compiler-cli": "^20.3.0",
"@eslint/js": "^9.17.0",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
@@ -44,7 +45,8 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "~5.9.2" "typescript": "~5.9.2",
"typescript-eslint": "^8.19.1"
} }
}, },
"node_modules/@acemir/cssom": { "node_modules/@acemir/cssom": {
@@ -3718,6 +3720,44 @@
"@tybys/wasm-util": "^0.10.0" "@tybys/wasm-util": "^0.10.0"
} }
}, },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@npmcli/agent": { "node_modules/@npmcli/agent": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
@@ -5123,6 +5163,264 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz",
"integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/type-utils": "8.46.3",
"@typescript-eslint/utils": "8.46.3",
"@typescript-eslint/visitor-keys": "8.46.3",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.46.3",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz",
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
"@typescript-eslint/typescript-estree": "8.46.3",
"@typescript-eslint/visitor-keys": "8.46.3",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz",
"integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.46.3",
"@typescript-eslint/types": "^8.46.3",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz",
"integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.3",
"@typescript-eslint/visitor-keys": "8.46.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz",
"integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz",
"integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.3",
"@typescript-eslint/typescript-estree": "8.46.3",
"@typescript-eslint/utils": "8.46.3",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz",
"integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz",
"integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.46.3",
"@typescript-eslint/tsconfig-utils": "8.46.3",
"@typescript-eslint/types": "8.46.3",
"@typescript-eslint/visitor-keys": "8.46.3",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz",
"integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
"@typescript-eslint/typescript-estree": "8.46.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz",
"integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.3",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": { "node_modules/@ungap/structured-clone": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -7680,6 +7978,36 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -7711,6 +8039,16 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fb-watchman": { "node_modules/fb-watchman": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
@@ -8127,6 +8465,13 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/handlebars": { "node_modules/handlebars": {
"version": "4.7.8", "version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@@ -10696,6 +11041,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -12251,6 +12606,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -12427,6 +12803,17 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@@ -12531,6 +12918,30 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": { "node_modules/rxjs": {
"version": "7.8.2", "version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -13695,6 +14106,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/ts-jest": { "node_modules/ts-jest": {
"version": "29.4.5", "version": "29.4.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
@@ -13859,6 +14283,30 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/typescript-eslint": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz",
"integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.46.3",
"@typescript-eslint/parser": "8.46.3",
"@typescript-eslint/typescript-estree": "8.46.3",
"@typescript-eslint/utils": "8.46.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "0.7.41", "version": "0.7.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
+8 -2
View File
@@ -7,7 +7,11 @@
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs" "serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\""
}, },
"prettier": { "prettier": {
"printWidth": 100, "printWidth": 100,
@@ -43,6 +47,7 @@
"@angular/build": "^20.3.7", "@angular/build": "^20.3.7",
"@angular/cli": "^20.3.7", "@angular/cli": "^20.3.7",
"@angular/compiler-cli": "^20.3.0", "@angular/compiler-cli": "^20.3.0",
"@eslint/js": "^9.17.0",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
@@ -59,6 +64,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "~5.9.2" "typescript": "~5.9.2",
"typescript-eslint": "^8.19.1"
} }
} }
+1 -3
View File
@@ -4,9 +4,7 @@ import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server'; import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = { const serverConfig: ApplicationConfig = {
providers: [ providers: [provideServerRendering(withRoutes(serverRoutes))],
provideServerRendering(withRoutes(serverRoutes))
]
}; };
export const config = mergeApplicationConfig(appConfig, serverConfig); export const config = mergeApplicationConfig(appConfig, serverConfig);
+2 -2
View File
@@ -3,6 +3,6 @@ import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [ export const serverRoutes: ServerRoute[] = [
{ {
path: '**', path: '**',
renderMode: RenderMode.Client renderMode: RenderMode.Client,
} },
]; ];
+1 -1
View File
@@ -6,7 +6,7 @@ describe('App', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [App], imports: [App],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}).compileComponents(); }).compileComponents();
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, model, signal } from '@angular/core'; import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { provideNativeDateAdapter } from '@angular/material/core'; import { provideNativeDateAdapter } from '@angular/material/core';
import { MatDivider } from '@angular/material/divider'; import { MatDivider } from '@angular/material/divider';
@@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { CalendarMonth } from './calendar-month'; import { CalendarMonth } from './calendar-month';
describe('CalendarMonth', () => { describe('CalendarMonth', () => {
let component: CalendarMonth; let component: CalendarMonth;
let fixture: ComponentFixture<CalendarMonth>; let fixture: ComponentFixture<CalendarMonth>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarMonth], imports: [CalendarMonth],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(CalendarMonth); fixture = TestBed.createComponent(CalendarMonth);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });
@@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { CalendarSettings } from './calendar-settings'; import { CalendarSettings } from './calendar-settings';
describe('CalendarSettings', () => { describe('CalendarSettings', () => {
let component: CalendarSettings; let component: CalendarSettings;
let fixture: ComponentFixture<CalendarSettings>; let fixture: ComponentFixture<CalendarSettings>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarSettings], imports: [CalendarSettings],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(CalendarSettings); fixture = TestBed.createComponent(CalendarSettings);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });
@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButton, MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatIcon, MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { CalendarSettingsDialog } from '../dialog/calendar-settings-dialog/calendar-settings-dialog'; import { CalendarSettingsDialog } from '../dialog/calendar-settings-dialog/calendar-settings-dialog';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
@@ -1,17 +1,21 @@
<div class="calendar-year"> <div class="calendar-year">
@if (isLoading()) { @if (isLoading()) {
<div class="loading-overlay"> <div class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
<mat-spinner></mat-spinner> <mat-spinner></mat-spinner>
<p>Calcul en cours...</p> <p>Calcul en cours...</p>
</div>
</div> </div>
</div>
} }
@for (month of months; track $index) { @for (month of months; track $index) {
<mat-card class="card"> <mat-card class="card">
<mat-card-content> <mat-card-content>
<app-calendar-month [monthIndex]="month" [year]="getYear(month)" [selectedDates]="selectedDates"></app-calendar-month> <app-calendar-month
</mat-card-content> [monthIndex]="month"
</mat-card> [year]="getYear(month)"
[selectedDates]="selectedDates"
></app-calendar-month>
</mat-card-content>
</mat-card>
} }
</div> </div>
@@ -66,5 +66,10 @@
} }
:root { :root {
@include mat.progress-spinner-overrides((size: 0.5rem, active-indicator-width: 0.25rem)); @include mat.progress-spinner-overrides(
(
size: 0.5rem,
active-indicator-width: 0.25rem,
)
);
} }
@@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { CalendarYear } from './calendar-year'; import { CalendarYear } from './calendar-year';
describe('CalendarYear', () => { describe('CalendarYear', () => {
let component: CalendarYear; let component: CalendarYear;
let fixture: ComponentFixture<CalendarYear>; let fixture: ComponentFixture<CalendarYear>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarYear], imports: [CalendarYear],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(CalendarYear); fixture = TestBed.createComponent(CalendarYear);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });
@@ -4,39 +4,38 @@ import { MatDialogRef } from '@angular/material/dialog';
import { CalendarSettingsDialog } from './calendar-settings-dialog'; import { CalendarSettingsDialog } from './calendar-settings-dialog';
describe('CalendarSettingsDialog', () => { describe('CalendarSettingsDialog', () => {
let component: CalendarSettingsDialog; let component: CalendarSettingsDialog;
let fixture: ComponentFixture<CalendarSettingsDialog>; let fixture: ComponentFixture<CalendarSettingsDialog>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarSettingsDialog], imports: [CalendarSettingsDialog],
providers: [ providers: [
provideZonelessChangeDetection(), provideZonelessChangeDetection(),
{ {
provide: MatDialogRef, provide: MatDialogRef,
useValue: { useValue: {
close: jasmine.createSpy('close') close: jasmine.createSpy('close'),
} },
} },
] ],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(CalendarSettingsDialog); fixture = TestBed.createComponent(CalendarSettingsDialog);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should have calendar settings service', () => { it('should have calendar settings service', () => {
expect(component['calendarSettingsService']).toBeDefined(); expect(component['calendarSettingsService']).toBeDefined();
}); });
it('should close dialog when closeDialog is called', () => { it('should close dialog when closeDialog is called', () => {
component.closeDialog(); component.closeDialog();
expect(component.dialogRef.close).toHaveBeenCalled(); expect(component.dialogRef.close).toHaveBeenCalled();
}); });
}); });
@@ -1,10 +1,8 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { CalendarSettingsService } from '../../../services/calendar-settings-service'; import { CalendarSettingsService } from '../../../services/calendar-settings-service';
import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatSlideToggle } from '@angular/material/slide-toggle';
import { CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling';
@Component({ @Component({
selector: 'app-calendar-settings-dialog', selector: 'app-calendar-settings-dialog',
@@ -3,39 +3,38 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { UserInput } from './user-input'; import { UserInput } from './user-input';
describe('UserInput', () => { describe('UserInput', () => {
let component: UserInput; let component: UserInput;
let fixture: ComponentFixture<UserInput>; let fixture: ComponentFixture<UserInput>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserInput], imports: [UserInput],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(UserInput); fixture = TestBed.createComponent(UserInput);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should initialize form with values from service', () => { it('should initialize form with values from service', () => {
expect(component.userInputForm).toBeDefined(); expect(component.userInputForm).toBeDefined();
expect(component.userInputForm.get('CP')).toBeDefined(); expect(component.userInputForm.get('CP')).toBeDefined();
expect(component.userInputForm.get('RTT')).toBeDefined(); expect(component.userInputForm.get('RTT')).toBeDefined();
expect(component.userInputForm.get('Others')).toBeDefined(); expect(component.userInputForm.get('Others')).toBeDefined();
}); });
it('should have valid form controls', () => { it('should have valid form controls', () => {
const cpControl = component.userInputForm.get('CP'); const cpControl = component.userInputForm.get('CP');
const rttControl = component.userInputForm.get('RTT'); const rttControl = component.userInputForm.get('RTT');
const othersControl = component.userInputForm.get('Others'); const othersControl = component.userInputForm.get('Others');
expect(cpControl).toBeTruthy(); expect(cpControl).toBeTruthy();
expect(rttControl).toBeTruthy(); expect(rttControl).toBeTruthy();
expect(othersControl).toBeTruthy(); expect(othersControl).toBeTruthy();
}); });
}); });
+1 -1
View File
@@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, effect } from '@angular/core'; import { Component, OnInit, OnDestroy, effect } from '@angular/core';
import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; 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 { VacationsNumber } from '../../services/calendar-service';
import { debounceTime, takeUntil, Subject } from 'rxjs'; import { debounceTime, takeUntil, Subject } from 'rxjs';
@@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { UserSettings } from './user-settings'; import { UserSettings } from './user-settings';
describe('UserSettings', () => { describe('UserSettings', () => {
let component: UserSettings; let component: UserSettings;
let fixture: ComponentFixture<UserSettings>; let fixture: ComponentFixture<UserSettings>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [UserSettings], imports: [UserSettings],
providers: [provideZonelessChangeDetection()] providers: [provideZonelessChangeDetection()],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(UserSettings); fixture = TestBed.createComponent(UserSettings);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });
+13 -49
View File
@@ -1,11 +1,6 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core'; import { provideZonelessChangeDetection } from '@angular/core';
import { import { CalendarService, DayType, SelectedDate, SelectedDates } from './calendar-service';
CalendarService,
DayType,
SelectedDate,
SelectedDates,
} from './calendar-service';
import { DateRange } from '@angular/material/datepicker'; import { DateRange } from '@angular/material/datepicker';
describe('CalendarService', () => { describe('CalendarService', () => {
@@ -111,47 +106,32 @@ describe('CalendarService', () => {
describe('SelectedDate', () => { describe('SelectedDate', () => {
it('should check if date is selected', () => { it('should check if date is selected', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(date, date));
DayType.CP,
new DateRange<Date>(date, date),
);
expect(selectedDate.isSelected(date)).toBe(true); expect(selectedDate.isSelected(date)).toBe(true);
}); });
it('should check if date is start', () => { it('should check if date is start', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(date, date));
DayType.CP,
new DateRange<Date>(date, date),
);
expect(selectedDate.isStart(date)).toBe(true); expect(selectedDate.isStart(date)).toBe(true);
}); });
it('should check if date is end', () => { it('should check if date is end', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(date, date));
DayType.CP,
new DateRange<Date>(date, date),
);
expect(selectedDate.isEnd(date)).toBe(true); expect(selectedDate.isEnd(date)).toBe(true);
}); });
it('should return correct classes for date', () => { it('should return correct classes for date', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(date, date));
DayType.CP,
new DateRange<Date>(date, date),
);
const classes = selectedDate.getClasses(date); const classes = selectedDate.getClasses(date);
expect(classes).toContain(DayType.CP); expect(classes).toContain(DayType.CP);
}); });
it('should return tooltip for selected date', () => { it('should return tooltip for selected date', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(date, date));
DayType.CP,
new DateRange<Date>(date, date),
);
const tooltip = selectedDate.getTooltip(date); const tooltip = selectedDate.getTooltip(date);
expect(tooltip).toBeTruthy(); expect(tooltip).toBeTruthy();
}); });
@@ -159,19 +139,13 @@ describe('SelectedDate', () => {
it('should calculate length in days', () => { it('should calculate length in days', () => {
const startDate = new Date(2024, 0, 1); const startDate = new Date(2024, 0, 1);
const endDate = new Date(2024, 0, 3); const endDate = new Date(2024, 0, 3);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.CP, new DateRange<Date>(startDate, endDate));
DayType.CP,
new DateRange<Date>(startDate, endDate),
);
expect(selectedDate.getLengthInDays()).toBe(3); expect(selectedDate.getLengthInDays()).toBe(3);
}); });
it('should not mark TODAY as selected', () => { it('should not mark TODAY as selected', () => {
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
const selectedDate = new SelectedDate( const selectedDate = new SelectedDate(DayType.TODAY, new DateRange<Date>(date, date));
DayType.TODAY,
new DateRange<Date>(date, date),
);
expect(selectedDate.isSelected(date)).toBe(false); expect(selectedDate.isSelected(date)).toBe(false);
}); });
}); });
@@ -180,18 +154,14 @@ describe('SelectedDates', () => {
it('should push selected dates', () => { it('should push selected dates', () => {
const selectedDates = new SelectedDates(); const selectedDates = new SelectedDates();
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
selectedDates.push( selectedDates.push(new SelectedDate(DayType.CP, new DateRange<Date>(date, date)));
new SelectedDate(DayType.CP, new DateRange<Date>(date, date)),
);
expect(selectedDates._datesSelected.length).toBe(1); expect(selectedDates._datesSelected.length).toBe(1);
}); });
it('should check if date is selected', () => { it('should check if date is selected', () => {
const selectedDates = new SelectedDates(); const selectedDates = new SelectedDates();
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
selectedDates.push( selectedDates.push(new SelectedDate(DayType.CP, new DateRange<Date>(date, date)));
new SelectedDate(DayType.CP, new DateRange<Date>(date, date)),
);
expect(selectedDates.isSelected(date)).toBe(true); expect(selectedDates.isSelected(date)).toBe(true);
}); });
@@ -212,9 +182,7 @@ describe('SelectedDates', () => {
it('should compute heuristics', () => { it('should compute heuristics', () => {
const selectedDates = new SelectedDates(); const selectedDates = new SelectedDates();
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
selectedDates.push( selectedDates.push(new SelectedDate(DayType.CP, new DateRange<Date>(date, date)));
new SelectedDate(DayType.CP, new DateRange<Date>(date, date)),
);
const heuristic = selectedDates.computeHeuristics(); const heuristic = selectedDates.computeHeuristics();
expect(typeof heuristic).toBe('number'); expect(typeof heuristic).toBe('number');
}); });
@@ -222,9 +190,7 @@ describe('SelectedDates', () => {
it('should return correct classes', () => { it('should return correct classes', () => {
const selectedDates = new SelectedDates(); const selectedDates = new SelectedDates();
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
selectedDates.push( selectedDates.push(new SelectedDate(DayType.CP, new DateRange<Date>(date, date)));
new SelectedDate(DayType.CP, new DateRange<Date>(date, date)),
);
const classes = selectedDates.getClasses(date); const classes = selectedDates.getClasses(date);
expect(classes).toContain(DayType.CP); expect(classes).toContain(DayType.CP);
}); });
@@ -232,9 +198,7 @@ describe('SelectedDates', () => {
it('should return tooltip', () => { it('should return tooltip', () => {
const selectedDates = new SelectedDates(); const selectedDates = new SelectedDates();
const date = new Date(2024, 0, 1); const date = new Date(2024, 0, 1);
selectedDates.push( selectedDates.push(new SelectedDate(DayType.CP, new DateRange<Date>(date, date)));
new SelectedDate(DayType.CP, new DateRange<Date>(date, date)),
);
const tooltip = selectedDates.getTooltip(date); const tooltip = selectedDates.getTooltip(date);
expect(tooltip).toBeTruthy(); expect(tooltip).toBeTruthy();
}); });
+8 -5
View File
@@ -28,8 +28,8 @@ export class CalendarService {
// Get selected dates from current date to the next year // Get selected dates from current date to the next year
getSelectedDatesFromNow(): SelectedDates { getSelectedDatesFromNow(): SelectedDates {
const holidays = this.getHolidays(); const _holidays = this.getHolidays();
let selectedDates: SelectedDates = new SelectedDates(); const selectedDates: SelectedDates = new SelectedDates();
// Logic to populate selectedDates from now until the next year // Logic to populate selectedDates from now until the next year
return selectedDates; return selectedDates;
} }
@@ -37,7 +37,7 @@ export class CalendarService {
getSelectedDatesForYear(): SelectedDates { getSelectedDatesForYear(): SelectedDates {
const holidays = this.getHolidays(); const holidays = this.getHolidays();
const monthInAYear = this.monthsInAYearFromNow(); const monthInAYear = this.monthsInAYearFromNow();
let selectedDates: SelectedDates = new SelectedDates(); const selectedDates: SelectedDates = new SelectedDates();
// Use DateCacheService to get today's date components // Use DateCacheService to get today's date components
const { const {
@@ -519,7 +519,7 @@ export class SelectedDates implements SelectedDateInterface {
const daysInRange = selectedDate.getLengthInDays(); const daysInRange = selectedDate.getLengthInDays();
// For each day in the range, add it to the appropriate month // For each day in the range, add it to the appropriate month
let currentDate = new Date(selectedDate.range.start); const currentDate = new Date(selectedDate.range.start);
for (let i = 0; i < daysInRange; i++) { for (let i = 0; i < daysInRange; i++) {
const monthKey = currentDate.getFullYear() * 12 + currentDate.getMonth(); const monthKey = currentDate.getFullYear() * 12 + currentDate.getMonth();
monthCounts.set(monthKey, (monthCounts.get(monthKey) ?? 0) + 1); monthCounts.set(monthKey, (monthCounts.get(monthKey) ?? 0) + 1);
@@ -719,7 +719,10 @@ export class SelectedDates implements SelectedDateInterface {
} }
} }
lookForVacation(vacType: DayType, calendarService: CalendarService): { apply: () => void; heuristic: number } { lookForVacation(
vacType: DayType,
calendarService: CalendarService,
): { 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);
+30 -30
View File
@@ -3,40 +3,40 @@ import { provideZonelessChangeDetection } from '@angular/core';
import { ThemeService } from './theme-service'; import { ThemeService } from './theme-service';
describe('ThemeService', () => { describe('ThemeService', () => {
let service: ThemeService; let service: ThemeService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()], providers: [provideZonelessChangeDetection()],
}); });
service = TestBed.inject(ThemeService); service = TestBed.inject(ThemeService);
}); });
it('should be created', () => { it('should be created', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
it('should initialize with system preference', () => { it('should initialize with system preference', () => {
expect(service.isLightMode()).toBeDefined(); expect(service.isLightMode()).toBeDefined();
}); });
it('should toggle theme', () => { it('should toggle theme', () => {
const initialMode = service.isLightMode(); const initialMode = service.isLightMode();
service.toggleTheme(); service.toggleTheme();
expect(service.isLightMode()).toBe(!initialMode); expect(service.isLightMode()).toBe(!initialMode);
}); });
it('should force light mode when specified', () => { it('should force light mode when specified', () => {
service.toggleTheme(true); service.toggleTheme(true);
expect(service.isLightMode()).toBe(true); expect(service.isLightMode()).toBe(true);
expect(document.body.classList.contains('light-mode')).toBe(true); expect(document.body.classList.contains('light-mode')).toBe(true);
expect(document.body.classList.contains('dark-mode')).toBe(false); expect(document.body.classList.contains('dark-mode')).toBe(false);
}); });
it('should force dark mode when specified', () => { it('should force dark mode when specified', () => {
service.toggleTheme(false); service.toggleTheme(false);
expect(service.isLightMode()).toBe(false); expect(service.isLightMode()).toBe(false);
expect(document.body.classList.contains('light-mode')).toBe(false); expect(document.body.classList.contains('light-mode')).toBe(false);
expect(document.body.classList.contains('dark-mode')).toBe(true); expect(document.body.classList.contains('dark-mode')).toBe(true);
}); });
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
import { Injectable, signal, PLATFORM_ID, inject } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { VacationsNumber } from './calendar-service'; import { VacationsNumber } from './calendar-service';
@Injectable({ @Injectable({
+10 -9
View File
@@ -1,18 +1,19 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>VacOptimizer</title> <title>VacOptimizer</title>
<base href="/"> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>
</body> </body>
</html>
</html>
+1 -2
View File
@@ -2,7 +2,6 @@ import { BootstrapContext, bootstrapApplication } from '@angular/platform-browse
import { App } from './app/app'; import { App } from './app/app';
import { config } from './app/app.config.server'; import { config } from './app/app.config.server';
const bootstrap = (context: BootstrapContext) => const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context);
bootstrapApplication(App, config, context);
export default bootstrap; export default bootstrap;
+1 -2
View File
@@ -2,5 +2,4 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { App } from './app/app'; import { App } from './app/app';
bootstrapApplication(App, appConfig) bootstrapApplication(App, appConfig).catch((err) => console.error(err));
.catch((err) => console.error(err));
+1 -3
View File
@@ -41,9 +41,7 @@ app.use(
app.use((req, res, next) => { app.use((req, res, next) => {
angularApp angularApp
.handle(req) .handle(req)
.then((response) => .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
response ? writeResponseToNodeResponse(response, res) : next(),
)
.catch(next); .catch(next);
}); });